mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-21 10:17:56 +00:00
Convert and update Manage Storage Settings.
This commit is contained in:
committed by
Greyson Parrelli
parent
adef572abb
commit
6d657b449c
@@ -20,9 +20,4 @@ public class BaseSettingsAdapter extends MappingAdapter {
|
||||
registerFactory(SingleSelectSetting.Item.class,
|
||||
new LayoutFactory<>(v -> new SingleSelectSetting.ViewHolder(v, selectionChangedListener), R.layout.single_select_item));
|
||||
}
|
||||
|
||||
public void configureCustomizableSingleSelect(@NonNull CustomizableSingleSelectSetting.CustomizableSingleSelectionListener selectionListener) {
|
||||
registerFactory(CustomizableSingleSelectSetting.Item.class,
|
||||
new LayoutFactory<>(v -> new CustomizableSingleSelectSetting.ViewHolder(v, selectionListener), R.layout.customizable_single_select_item));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModelList;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A simple settings screen that takes its configuration via {@link Configuration}.
|
||||
*/
|
||||
public class BaseSettingsFragment extends Fragment {
|
||||
|
||||
private static final String CONFIGURATION_ARGUMENT = "current_selection";
|
||||
|
||||
private RecyclerView recycler;
|
||||
|
||||
public static @NonNull BaseSettingsFragment create(@NonNull Configuration configuration) {
|
||||
BaseSettingsFragment fragment = new BaseSettingsFragment();
|
||||
|
||||
Bundle arguments = new Bundle();
|
||||
arguments.putSerializable(CONFIGURATION_ARGUMENT, configuration);
|
||||
fragment.setArguments(arguments);
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.base_settings_fragment, container, false);
|
||||
|
||||
recycler = view.findViewById(R.id.base_settings_list);
|
||||
recycler.setItemAnimator(null);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
BaseSettingsAdapter adapter = new BaseSettingsAdapter();
|
||||
|
||||
recycler.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||
recycler.setAdapter(adapter);
|
||||
|
||||
Configuration configuration = (Configuration) Objects.requireNonNull(requireArguments().getSerializable(CONFIGURATION_ARGUMENT));
|
||||
configuration.configure(requireActivity(), adapter);
|
||||
configuration.setArguments(getArguments());
|
||||
configuration.configureAdapter(adapter);
|
||||
|
||||
adapter.submitList(configuration.getSettings());
|
||||
}
|
||||
|
||||
/**
|
||||
* A configuration for a settings screen. Utilizes serializable to hide
|
||||
* reflection of instantiating from a fragment argument.
|
||||
*/
|
||||
public static abstract class Configuration implements Serializable {
|
||||
protected transient FragmentActivity activity;
|
||||
protected transient BaseSettingsAdapter adapter;
|
||||
|
||||
public void configure(@NonNull FragmentActivity activity, @NonNull BaseSettingsAdapter adapter) {
|
||||
this.activity = activity;
|
||||
this.adapter = adapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve any runtime information from the fragment's arguments.
|
||||
*/
|
||||
public void setArguments(@Nullable Bundle arguments) {}
|
||||
|
||||
protected void updateSettingsList() {
|
||||
adapter.submitList(getSettings());
|
||||
}
|
||||
|
||||
public abstract void configureAdapter(@NonNull BaseSettingsAdapter adapter);
|
||||
|
||||
public abstract @NonNull MappingModelList getSettings();
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.settings;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.constraintlayout.widget.Group;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel;
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Adds ability to customize a value for a single select (radio) setting.
|
||||
*/
|
||||
public class CustomizableSingleSelectSetting {
|
||||
|
||||
public interface CustomizableSingleSelectionListener extends SingleSelectSetting.SingleSelectSelectionChangedListener {
|
||||
void onCustomizeClicked(@Nullable Item item);
|
||||
}
|
||||
|
||||
public static class ViewHolder extends MappingViewHolder<Item> {
|
||||
private final View customize;
|
||||
private final SingleSelectSetting.ViewHolder delegate;
|
||||
private final Group customizeGroup;
|
||||
private final CustomizableSingleSelectionListener selectionListener;
|
||||
|
||||
public ViewHolder(@NonNull View itemView, @NonNull CustomizableSingleSelectionListener selectionListener) {
|
||||
super(itemView);
|
||||
this.selectionListener = selectionListener;
|
||||
|
||||
customize = findViewById(R.id.customizable_single_select_customize);
|
||||
customizeGroup = findViewById(R.id.customizable_single_select_customize_group);
|
||||
|
||||
delegate = new SingleSelectSetting.ViewHolder(itemView, selectionListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull Item model) {
|
||||
delegate.bind(model.singleSelectItem);
|
||||
customizeGroup.setVisibility(model.singleSelectItem.isSelected() ? View.VISIBLE : View.GONE);
|
||||
customize.setOnClickListener(v -> selectionListener.onCustomizeClicked(model));
|
||||
}
|
||||
}
|
||||
|
||||
public static class Item implements MappingModel<Item> {
|
||||
private final SingleSelectSetting.Item singleSelectItem;
|
||||
private final Object customValue;
|
||||
|
||||
public <T> Item(@NonNull T item, @Nullable String text, boolean isSelected, @Nullable Object customValue, @Nullable String summaryText) {
|
||||
this.customValue = customValue;
|
||||
|
||||
singleSelectItem = new SingleSelectSetting.Item(item, text, summaryText, isSelected);
|
||||
}
|
||||
|
||||
public @Nullable Object getCustomValue() {
|
||||
return customValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull Item newItem) {
|
||||
return singleSelectItem.areItemsTheSame(newItem.singleSelectItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull Item newItem) {
|
||||
return singleSelectItem.areContentsTheSame(newItem.singleSelectItem) && Objects.equals(customValue, newItem.customValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,523 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.storage
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TextField
|
||||
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.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.res.integerArrayResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.dialog
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.navArgument
|
||||
import org.signal.core.ui.Animations
|
||||
import org.signal.core.ui.Dialogs
|
||||
import org.signal.core.ui.Dividers
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.ui.Rows
|
||||
import org.signal.core.ui.Rows.TextAndLabel
|
||||
import org.signal.core.ui.Scaffolds
|
||||
import org.signal.core.ui.SignalPreview
|
||||
import org.signal.core.ui.Texts
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.database.MediaTable
|
||||
import org.thoughtcrime.securesms.keyvalue.KeepMessagesDuration
|
||||
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity
|
||||
import org.thoughtcrime.securesms.preferences.widgets.StorageGraphView
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
import java.text.NumberFormat
|
||||
|
||||
/**
|
||||
* Manage settings related to on-device storage including viewing usage and auto-delete settings.
|
||||
*/
|
||||
class ManageStorageSettingsFragment : ComposeFragment() {
|
||||
|
||||
private val viewModel by viewModel<ManageStorageSettingsViewModel> { ManageStorageSettingsViewModel() }
|
||||
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val state by viewModel.state.collectAsState()
|
||||
|
||||
val navController = rememberNavController()
|
||||
|
||||
SignalTheme {
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = "manage",
|
||||
enterTransition = { Animations.navHostSlideInTransition { it } },
|
||||
exitTransition = { Animations.navHostSlideOutTransition { -it } },
|
||||
popEnterTransition = { Animations.navHostSlideInTransition { -it } },
|
||||
popExitTransition = { Animations.navHostSlideOutTransition { it } }
|
||||
) {
|
||||
composable("manage") {
|
||||
ManageStorageSettingsScreen(
|
||||
state = state,
|
||||
onNavigationClick = { findNavController().popBackStack() },
|
||||
onReviewStorage = { startActivity(MediaOverviewActivity.forAll(requireContext())) },
|
||||
onSetKeepMessages = { navController.navigate("set-keep-messages") },
|
||||
onSetChatLengthLimit = { navController.navigate("set-chat-length-limit") },
|
||||
onDeleteChatHistory = { navController.navigate("confirm-delete-chat-history") }
|
||||
)
|
||||
}
|
||||
|
||||
composable("set-keep-messages") {
|
||||
SetKeepMessagesScreen(
|
||||
selection = state.keepMessagesDuration,
|
||||
onNavigationClick = { navController.popBackStack() },
|
||||
onSelectionChanged = { newDuration ->
|
||||
if (viewModel.showConfirmKeepDurationChange(newDuration)) {
|
||||
navController.navigate("confirm-set-keep-messages/${newDuration.id}")
|
||||
} else {
|
||||
viewModel.setKeepMessagesDuration(newDuration)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable("set-chat-length-limit") {
|
||||
SetChatLengthLimitScreen(
|
||||
currentLimit = state.lengthLimit,
|
||||
onNavigationClick = { navController.popBackStack() },
|
||||
onOptionSelected = { newLengthLimit ->
|
||||
if (viewModel.showConfirmSetChatLengthLimit(newLengthLimit)) {
|
||||
navController.navigate("confirm-set-length-limit/$newLengthLimit")
|
||||
} else {
|
||||
viewModel.setChatLengthLimit(newLengthLimit)
|
||||
}
|
||||
},
|
||||
onCustomSelected = { navController.navigate("custom-set-length-limit") }
|
||||
)
|
||||
}
|
||||
|
||||
dialog("confirm-delete-chat-history") {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(id = R.string.preferences_storage__delete_message_history),
|
||||
body = stringResource(id = R.string.preferences_storage__this_will_delete_all_message_history_and_media_from_your_device),
|
||||
confirm = stringResource(id = R.string.delete),
|
||||
confirmColor = MaterialTheme.colorScheme.error,
|
||||
dismiss = stringResource(id = android.R.string.cancel),
|
||||
onConfirm = { navController.navigate("double-confirm-delete-chat-history") },
|
||||
onDismiss = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
|
||||
dialog("double-confirm-delete-chat-history", dialogProperties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = true)) {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(id = R.string.preferences_storage__are_you_sure_you_want_to_delete_all_message_history),
|
||||
body = stringResource(id = R.string.preferences_storage__all_message_history_will_be_permanently_removed_this_action_cannot_be_undone),
|
||||
confirm = stringResource(id = R.string.preferences_storage__delete_all_now),
|
||||
confirmColor = MaterialTheme.colorScheme.error,
|
||||
dismiss = stringResource(id = android.R.string.cancel),
|
||||
onConfirm = { viewModel.deleteChatHistory() },
|
||||
onDismiss = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
|
||||
dialog(
|
||||
route = "confirm-set-keep-messages/{keepMessagesId}",
|
||||
arguments = listOf(navArgument("keepMessagesId") { type = NavType.IntType })
|
||||
) {
|
||||
val newDuration = KeepMessagesDuration.fromId(it.arguments!!.getInt("keepMessagesId"))
|
||||
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(id = R.string.preferences_storage__delete_older_messages),
|
||||
body = stringResource(id = R.string.preferences_storage__this_will_permanently_delete_all_message_history_and_media, stringResource(id = newDuration.stringResource)),
|
||||
confirm = stringResource(id = R.string.delete),
|
||||
dismiss = stringResource(id = android.R.string.cancel),
|
||||
onConfirm = { viewModel.setKeepMessagesDuration(newDuration) },
|
||||
onDismiss = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
|
||||
dialog(
|
||||
route = "confirm-set-length-limit/{lengthLimit}",
|
||||
arguments = listOf(navArgument("lengthLimit") { type = NavType.IntType })
|
||||
) {
|
||||
val newLengthLimit = it.arguments!!.getInt("lengthLimit")
|
||||
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(id = R.string.preferences_storage__delete_older_messages),
|
||||
body = pluralStringResource(
|
||||
id = R.plurals.preferences_storage__this_will_permanently_trim_all_conversations_to_the_d_most_recent_messages,
|
||||
count = newLengthLimit,
|
||||
NumberFormat.getInstance().format(newLengthLimit)
|
||||
),
|
||||
confirm = stringResource(id = R.string.delete),
|
||||
dismiss = stringResource(id = android.R.string.cancel),
|
||||
onConfirm = { viewModel.setChatLengthLimit(newLengthLimit) },
|
||||
onDismiss = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
|
||||
dialog(
|
||||
route = "custom-set-length-limit"
|
||||
) {
|
||||
SetCustomLengthLimitDialog(
|
||||
currentLimit = if (state.lengthLimit != ManageStorageSettingsViewModel.ManageStorageState.NO_LIMIT) state.lengthLimit else null,
|
||||
onCustomLimitSet = { newLengthLimit ->
|
||||
if (viewModel.showConfirmSetChatLengthLimit(newLengthLimit)) {
|
||||
navController.navigate("confirm-set-length-limit/$newLengthLimit")
|
||||
} else {
|
||||
viewModel.setChatLengthLimit(newLengthLimit)
|
||||
}
|
||||
},
|
||||
onDismiss = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ManageStorageSettingsScreen(
|
||||
state: ManageStorageSettingsViewModel.ManageStorageState,
|
||||
onNavigationClick: () -> Unit = {},
|
||||
onReviewStorage: () -> Unit = {},
|
||||
onSetKeepMessages: () -> Unit = {},
|
||||
onSetChatLengthLimit: () -> Unit = {},
|
||||
onDeleteChatHistory: () -> Unit = {}
|
||||
) {
|
||||
Scaffolds.Settings(
|
||||
title = stringResource(id = R.string.preferences__storage),
|
||||
onNavigationClick = onNavigationClick,
|
||||
navigationIconPainter = painterResource(id = R.drawable.ic_arrow_left_24)
|
||||
) { contentPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(contentPadding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Texts.SectionHeader(text = stringResource(id = R.string.preferences_storage__storage_usage))
|
||||
|
||||
StorageOverview(state.breakdown, onReviewStorage)
|
||||
|
||||
Dividers.Default()
|
||||
|
||||
Texts.SectionHeader(text = stringResource(id = R.string.ManageStorageSettingsFragment_chat_limit))
|
||||
|
||||
Rows.TextRow(
|
||||
text = stringResource(id = R.string.preferences__keep_messages),
|
||||
label = stringResource(id = state.keepMessagesDuration.stringResource),
|
||||
onClick = onSetKeepMessages
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = stringResource(id = R.string.preferences__conversation_length_limit),
|
||||
label = if (state.lengthLimit != ManageStorageSettingsViewModel.ManageStorageState.NO_LIMIT) {
|
||||
pluralStringResource(
|
||||
id = R.plurals.preferences_storage__s_messages_plural,
|
||||
count = state.lengthLimit,
|
||||
NumberFormat.getInstance().format(state.lengthLimit)
|
||||
)
|
||||
} else {
|
||||
stringResource(id = R.string.preferences_storage__none)
|
||||
},
|
||||
onClick = onSetChatLengthLimit
|
||||
)
|
||||
|
||||
Dividers.Default()
|
||||
|
||||
Rows.TextRow(
|
||||
text = stringResource(id = R.string.ManageStorageSettingsFragment_delete_message_history),
|
||||
onClick = onDeleteChatHistory
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StorageOverview(
|
||||
breakdown: MediaTable.StorageBreakdown?,
|
||||
onReviewStorage: () -> Unit
|
||||
) {
|
||||
AndroidView(
|
||||
factory = {
|
||||
LayoutInflater.from(it).inflate(R.layout.preference_storage_category, null)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
if (breakdown != null) {
|
||||
val breakdownEntries = StorageGraphView.StorageBreakdown(
|
||||
listOf(
|
||||
StorageGraphView.Entry(ContextCompat.getColor(it.context, R.color.storage_color_photos), breakdown.photoSize),
|
||||
StorageGraphView.Entry(ContextCompat.getColor(it.context, R.color.storage_color_videos), breakdown.videoSize),
|
||||
StorageGraphView.Entry(ContextCompat.getColor(it.context, R.color.storage_color_files), breakdown.documentSize),
|
||||
StorageGraphView.Entry(ContextCompat.getColor(it.context, R.color.storage_color_audio), breakdown.audioSize)
|
||||
)
|
||||
)
|
||||
|
||||
it.findViewById<StorageGraphView>(R.id.storageGraphView).setStorageBreakdown(breakdownEntries)
|
||||
it.findViewById<TextView>(R.id.total_size).text = Util.getPrettyFileSize(breakdownEntries.totalSize)
|
||||
}
|
||||
|
||||
it.findViewById<View>(R.id.free_up_space).setOnClickListener {
|
||||
onReviewStorage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SetKeepMessagesScreen(
|
||||
selection: KeepMessagesDuration,
|
||||
onNavigationClick: () -> Unit = {},
|
||||
onSelectionChanged: (KeepMessagesDuration) -> Unit = {}
|
||||
) {
|
||||
Scaffolds.Settings(
|
||||
title = stringResource(id = R.string.preferences__keep_messages),
|
||||
onNavigationClick = onNavigationClick,
|
||||
navigationIconPainter = painterResource(id = R.drawable.ic_arrow_left_24)
|
||||
) { contentPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(contentPadding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
KeepMessagesDuration
|
||||
.values()
|
||||
.forEach {
|
||||
Rows.RadioRow(
|
||||
text = stringResource(id = it.stringResource),
|
||||
selected = it == selection,
|
||||
modifier = Modifier.clickable { onSelectionChanged(it) }
|
||||
)
|
||||
}
|
||||
|
||||
Rows.TextRow(
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.ManageStorageSettingsFragment_keep_messages_duration_warning),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SetChatLengthLimitScreen(
|
||||
currentLimit: Int,
|
||||
onNavigationClick: () -> Unit = {},
|
||||
onOptionSelected: (Int) -> Unit = {},
|
||||
onCustomSelected: (Int) -> Unit = {}
|
||||
) {
|
||||
Scaffolds.Settings(
|
||||
title = stringResource(id = R.string.preferences__conversation_length_limit),
|
||||
onNavigationClick = onNavigationClick,
|
||||
navigationIconPainter = painterResource(id = R.drawable.ic_arrow_left_24)
|
||||
) { contentPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(contentPadding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
val options = integerArrayResource(id = R.array.conversation_length_limit)
|
||||
var customSelected = true
|
||||
|
||||
for (option in options) {
|
||||
val isSelected = option == currentLimit
|
||||
|
||||
Rows.RadioRow(
|
||||
selected = isSelected,
|
||||
text = if (option == 0) {
|
||||
stringResource(id = R.string.preferences_storage__none)
|
||||
} else {
|
||||
pluralStringResource(id = R.plurals.preferences_storage__s_messages_plural, count = option, NumberFormat.getInstance().format(option.toLong()))
|
||||
},
|
||||
modifier = Modifier.clickable { onOptionSelected(option) }
|
||||
)
|
||||
|
||||
if (isSelected) {
|
||||
customSelected = false
|
||||
}
|
||||
}
|
||||
|
||||
Rows.RadioRow(
|
||||
content = {
|
||||
TextAndLabel(
|
||||
text = stringResource(id = R.string.preferences_storage__custom),
|
||||
label = if (customSelected) {
|
||||
pluralStringResource(id = R.plurals.preferences_storage__s_messages_plural, count = currentLimit, NumberFormat.getInstance().format(currentLimit))
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
|
||||
if (customSelected) {
|
||||
Dividers.Vertical(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
.height(48.dp)
|
||||
)
|
||||
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.symbol_settings_android_24),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.clickable { onCustomSelected(currentLimit) }
|
||||
.padding(12.dp)
|
||||
)
|
||||
}
|
||||
},
|
||||
selected = customSelected,
|
||||
modifier = Modifier.clickable { onCustomSelected(currentLimit) }
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.ManageStorageSettingsFragment_chat_length_limit_warning),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SetCustomLengthLimitDialog(
|
||||
currentLimit: Int? = 1000,
|
||||
onCustomLimitSet: (Int) -> Unit = {},
|
||||
onDismiss: () -> Unit = {}
|
||||
) {
|
||||
var lengthLimit by remember {
|
||||
mutableStateOf(
|
||||
TextFieldValue(
|
||||
text = currentLimit?.toString() ?: "",
|
||||
selection = TextRange(currentLimit.toString().length)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
androidx.compose.material3.AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(text = stringResource(id = R.string.preferences__conversation_length_limit)) },
|
||||
text = {
|
||||
TextField(
|
||||
value = lengthLimit,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
onValueChange = { value ->
|
||||
val cleaned = value.text.replace("\\D".toRegex(), "")
|
||||
lengthLimit = if (cleaned == value.text) {
|
||||
value
|
||||
} else {
|
||||
value.copy(text = cleaned)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.focusRequester(focusRequester)
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
enabled = lengthLimit.text.toIntOrNull() != null,
|
||||
onClick = {
|
||||
onDismiss()
|
||||
onCustomLimitSet(lengthLimit.text.toInt())
|
||||
}
|
||||
) {
|
||||
Text(text = stringResource(id = android.R.string.ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(text = stringResource(id = android.R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun ManageStorageSettingsScreenPreview() {
|
||||
Previews.Preview {
|
||||
ManageStorageSettingsScreen(
|
||||
state = ManageStorageSettingsViewModel.ManageStorageState(
|
||||
keepMessagesDuration = KeepMessagesDuration.FOREVER,
|
||||
lengthLimit = ManageStorageSettingsViewModel.ManageStorageState.NO_LIMIT
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun SetKeepMessagesScreenPreview() {
|
||||
Previews.Preview {
|
||||
SetKeepMessagesScreen(selection = KeepMessagesDuration.FOREVER)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun SetChatLengthLimitScreenPreview() {
|
||||
Previews.Preview {
|
||||
SetChatLengthLimitScreen(
|
||||
currentLimit = 1000
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun SetCustomLengthLimitDialogPreview() {
|
||||
Previews.Preview {
|
||||
SetCustomLengthLimitDialog(currentLimit = 123)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.storage
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.database.MediaTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.media
|
||||
import org.thoughtcrime.securesms.database.ThreadTable
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.KeepMessagesDuration
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
|
||||
class ManageStorageSettingsViewModel : ViewModel() {
|
||||
|
||||
private val store = MutableStateFlow(
|
||||
ManageStorageState(
|
||||
keepMessagesDuration = SignalStore.settings().keepMessagesDuration,
|
||||
lengthLimit = if (SignalStore.settings().isTrimByLengthEnabled) SignalStore.settings().threadTrimLength else ManageStorageState.NO_LIMIT
|
||||
)
|
||||
)
|
||||
val state = store.asStateFlow()
|
||||
|
||||
fun refresh() {
|
||||
viewModelScope.launch {
|
||||
val breakdown: MediaTable.StorageBreakdown = media.getStorageBreakdown()
|
||||
store.update { it.copy(breakdown = breakdown) }
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteChatHistory() {
|
||||
viewModelScope.launch {
|
||||
SignalDatabase.threads.deleteAllConversations()
|
||||
ApplicationDependencies.getMessageNotifier().updateNotification(ApplicationDependencies.getApplication())
|
||||
}
|
||||
}
|
||||
|
||||
fun setKeepMessagesDuration(newDuration: KeepMessagesDuration) {
|
||||
SignalStore.settings().setKeepMessagesForDuration(newDuration)
|
||||
ApplicationDependencies.getTrimThreadsByDateManager().scheduleIfNecessary()
|
||||
|
||||
store.update { it.copy(keepMessagesDuration = newDuration) }
|
||||
}
|
||||
|
||||
fun showConfirmKeepDurationChange(newDuration: KeepMessagesDuration): Boolean {
|
||||
return newDuration.ordinal > state.value.keepMessagesDuration.ordinal
|
||||
}
|
||||
|
||||
fun setChatLengthLimit(newLimit: Int) {
|
||||
val restrictingChange = isRestrictingLengthLimitChange(newLimit)
|
||||
|
||||
SignalStore.settings().setThreadTrimByLengthEnabled(newLimit != ManageStorageState.NO_LIMIT)
|
||||
SignalStore.settings().threadTrimLength = newLimit
|
||||
store.update { it.copy(lengthLimit = newLimit) }
|
||||
|
||||
if (SignalStore.settings().isTrimByLengthEnabled && restrictingChange) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val keepMessagesDuration = SignalStore.settings().keepMessagesDuration
|
||||
|
||||
val trimBeforeDate = if (keepMessagesDuration != KeepMessagesDuration.FOREVER) {
|
||||
System.currentTimeMillis() - keepMessagesDuration.duration
|
||||
} else {
|
||||
ThreadTable.NO_TRIM_BEFORE_DATE_SET
|
||||
}
|
||||
|
||||
SignalDatabase.threads.trimAllThreads(newLimit, trimBeforeDate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun showConfirmSetChatLengthLimit(newLimit: Int): Boolean {
|
||||
return isRestrictingLengthLimitChange(newLimit)
|
||||
}
|
||||
|
||||
private fun isRestrictingLengthLimitChange(newLimit: Int): Boolean {
|
||||
return state.value.lengthLimit == ManageStorageState.NO_LIMIT || (newLimit != ManageStorageState.NO_LIMIT && newLimit < state.value.lengthLimit)
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class ManageStorageState(
|
||||
val keepMessagesDuration: KeepMessagesDuration = KeepMessagesDuration.FOREVER,
|
||||
val lengthLimit: Int = NO_LIMIT,
|
||||
val breakdown: MediaTable.StorageBreakdown? = null
|
||||
) {
|
||||
companion object {
|
||||
const val NO_LIMIT = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.wrapped
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import org.thoughtcrime.securesms.preferences.StoragePreferenceFragment
|
||||
|
||||
class WrappedStoragePreferenceFragment : SettingsWrapperFragment() {
|
||||
override fun getFragment(): Fragment {
|
||||
return StoragePreferenceFragment()
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.database
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import androidx.compose.runtime.Immutable
|
||||
import org.signal.core.util.requireInt
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.requireNonNullString
|
||||
@@ -302,6 +303,7 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class StorageBreakdown(
|
||||
val photoSize: Long,
|
||||
val videoSize: Long,
|
||||
|
||||
@@ -35,7 +35,7 @@ public enum KeepMessagesDuration {
|
||||
return duration;
|
||||
}
|
||||
|
||||
static @NonNull KeepMessagesDuration fromId(int id) {
|
||||
public static @NonNull KeepMessagesDuration fromId(int id) {
|
||||
return values()[id];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
package org.thoughtcrime.securesms.preferences;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.MediaTable;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.preferences.widgets.StorageGraphView;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class ApplicationPreferencesViewModel extends ViewModel {
|
||||
|
||||
private final MutableLiveData<StorageGraphView.StorageBreakdown> storageBreakdown = new MutableLiveData<>();
|
||||
|
||||
LiveData<StorageGraphView.StorageBreakdown> getStorageBreakdown() {
|
||||
return storageBreakdown;
|
||||
}
|
||||
|
||||
static ApplicationPreferencesViewModel getApplicationPreferencesViewModel(@NonNull FragmentActivity activity) {
|
||||
return new ViewModelProvider(activity).get(ApplicationPreferencesViewModel.class);
|
||||
}
|
||||
|
||||
void refreshStorageBreakdown(@NonNull Context context) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
MediaTable.StorageBreakdown breakdown = SignalDatabase.media().getStorageBreakdown();
|
||||
|
||||
StorageGraphView.StorageBreakdown latestStorageBreakdown = new StorageGraphView.StorageBreakdown(Arrays.asList(
|
||||
new StorageGraphView.Entry(ContextCompat.getColor(context, R.color.storage_color_photos), breakdown.getPhotoSize()),
|
||||
new StorageGraphView.Entry(ContextCompat.getColor(context, R.color.storage_color_videos), breakdown.getVideoSize()),
|
||||
new StorageGraphView.Entry(ContextCompat.getColor(context, R.color.storage_color_files), breakdown.getDocumentSize()),
|
||||
new StorageGraphView.Entry(ContextCompat.getColor(context, R.color.storage_color_audio), breakdown.getAudioSize())
|
||||
));
|
||||
|
||||
storageBreakdown.postValue(latestStorageBreakdown);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,312 +0,0 @@
|
||||
package org.thoughtcrime.securesms.preferences;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.settings.BaseSettingsAdapter;
|
||||
import org.thoughtcrime.securesms.components.settings.BaseSettingsFragment;
|
||||
import org.thoughtcrime.securesms.components.settings.CustomizableSingleSelectSetting;
|
||||
import org.thoughtcrime.securesms.components.settings.SingleSelectSetting;
|
||||
import org.thoughtcrime.securesms.components.settings.app.wrapped.SettingsWrapperFragment;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadTable;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.keyvalue.KeepMessagesDuration;
|
||||
import org.thoughtcrime.securesms.keyvalue.SettingsValues;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.preferences.widgets.StoragePreferenceCategory;
|
||||
import org.signal.core.util.StringUtil;
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModelList;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
|
||||
public class StoragePreferenceFragment extends ListSummaryPreferenceFragment {
|
||||
|
||||
private Preference keepMessages;
|
||||
private Preference trimLength;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle paramBundle) {
|
||||
super.onCreate(paramBundle);
|
||||
|
||||
findPreference("pref_storage_clear_message_history")
|
||||
.setOnPreferenceClickListener(new ClearMessageHistoryClickListener());
|
||||
|
||||
trimLength = findPreference(SettingsValues.THREAD_TRIM_LENGTH);
|
||||
trimLength.setOnPreferenceClickListener(p -> {
|
||||
updateToolbarTitle(R.string.preferences__conversation_length_limit);
|
||||
pushFragment(BaseSettingsFragment.create(new ConversationLengthLimitConfiguration()));
|
||||
return true;
|
||||
});
|
||||
|
||||
keepMessages = findPreference(SettingsValues.KEEP_MESSAGES_DURATION);
|
||||
keepMessages.setOnPreferenceClickListener(p -> {
|
||||
updateToolbarTitle(R.string.preferences__keep_messages);
|
||||
pushFragment(BaseSettingsFragment.create(new KeepMessagesConfiguration()));
|
||||
return true;
|
||||
});
|
||||
|
||||
StoragePreferenceCategory storageCategory = (StoragePreferenceCategory) findPreference("pref_storage_category");
|
||||
FragmentActivity activity = requireActivity();
|
||||
ApplicationPreferencesViewModel viewModel = ApplicationPreferencesViewModel.getApplicationPreferencesViewModel(activity);
|
||||
|
||||
storageCategory.setOnFreeUpSpace(() -> activity.startActivity(MediaOverviewActivity.forAll(activity)));
|
||||
|
||||
viewModel.getStorageBreakdown().observe(activity, storageCategory::setStorage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
|
||||
addPreferencesFromResource(R.xml.preferences_storage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
updateToolbarTitle(R.string.preferences__storage);
|
||||
|
||||
FragmentActivity activity = requireActivity();
|
||||
ApplicationPreferencesViewModel viewModel = ApplicationPreferencesViewModel.getApplicationPreferencesViewModel(activity);
|
||||
|
||||
viewModel.refreshStorageBreakdown(activity.getApplicationContext());
|
||||
|
||||
keepMessages.setSummary(SignalStore.settings().getKeepMessagesDuration().getStringResource());
|
||||
|
||||
trimLength.setSummary(SignalStore.settings().isTrimByLengthEnabled() ? getResources().getQuantityString(R.plurals.preferences_storage__s_messages_plural, SignalStore.settings().getThreadTrimLength(), NumberFormat.getInstance().format(SignalStore.settings().getThreadTrimLength()))
|
||||
: getString(R.string.preferences_storage__none));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
private void updateToolbarTitle(@StringRes int title) {
|
||||
if (getParentFragment() instanceof SettingsWrapperFragment) {
|
||||
((SettingsWrapperFragment) getParentFragment()).setTitle(title);
|
||||
}
|
||||
}
|
||||
|
||||
private void pushFragment(@NonNull Fragment fragment) {
|
||||
getParentFragmentManager().beginTransaction()
|
||||
.replace(R.id.wrapped_fragment, fragment)
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
}
|
||||
|
||||
private class ClearMessageHistoryClickListener implements Preference.OnPreferenceClickListener {
|
||||
@Override
|
||||
public boolean onPreferenceClick(@NonNull Preference preference) {
|
||||
new MaterialAlertDialogBuilder(requireActivity())
|
||||
.setTitle(R.string.preferences_storage__clear_message_history)
|
||||
.setMessage(R.string.preferences_storage__this_will_delete_all_message_history_and_media_from_your_device)
|
||||
.setPositiveButton(R.string.delete, (d, w) -> showAreYouReallySure())
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void showAreYouReallySure() {
|
||||
new MaterialAlertDialogBuilder(requireActivity())
|
||||
.setTitle(R.string.preferences_storage__are_you_sure_you_want_to_delete_all_message_history)
|
||||
.setMessage(R.string.preferences_storage__all_message_history_will_be_permanently_removed_this_action_cannot_be_undone)
|
||||
.setPositiveButton(R.string.preferences_storage__delete_all_now, (d, w) -> {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
SignalDatabase.threads().deleteAllConversations();
|
||||
ApplicationDependencies.getMessageNotifier().updateNotification(requireContext());
|
||||
});
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
public static class KeepMessagesConfiguration extends BaseSettingsFragment.Configuration implements SingleSelectSetting.SingleSelectSelectionChangedListener {
|
||||
|
||||
@Override
|
||||
public void configureAdapter(@NonNull BaseSettingsAdapter adapter) {
|
||||
adapter.configureSingleSelect(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MappingModelList getSettings() {
|
||||
KeepMessagesDuration currentDuration = SignalStore.settings().getKeepMessagesDuration();
|
||||
return Stream.of(KeepMessagesDuration.values())
|
||||
.map(duration -> new SingleSelectSetting.Item(duration, activity.getString(duration.getStringResource()), null, duration.equals(currentDuration)))
|
||||
.collect(MappingModelList.toMappingModelList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectionChanged(@NonNull Object selection) {
|
||||
KeepMessagesDuration currentDuration = SignalStore.settings().getKeepMessagesDuration();
|
||||
KeepMessagesDuration newDuration = (KeepMessagesDuration) selection;
|
||||
|
||||
if (newDuration.ordinal() > currentDuration.ordinal()) {
|
||||
new MaterialAlertDialogBuilder(activity)
|
||||
.setTitle(R.string.preferences_storage__delete_older_messages)
|
||||
.setMessage(activity.getString(R.string.preferences_storage__this_will_permanently_delete_all_message_history_and_media, activity.getString(newDuration.getStringResource())))
|
||||
.setPositiveButton(R.string.delete, (d, w) -> updateTrimByTime(newDuration))
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
} else {
|
||||
updateTrimByTime(newDuration);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTrimByTime(@NonNull KeepMessagesDuration newDuration) {
|
||||
SignalStore.settings().setKeepMessagesForDuration(newDuration);
|
||||
updateSettingsList();
|
||||
ApplicationDependencies.getTrimThreadsByDateManager().scheduleIfNecessary();
|
||||
}
|
||||
}
|
||||
|
||||
public static class ConversationLengthLimitConfiguration extends BaseSettingsFragment.Configuration implements CustomizableSingleSelectSetting.CustomizableSingleSelectionListener {
|
||||
|
||||
private static final int CUSTOM_LENGTH = -1;
|
||||
|
||||
@Override
|
||||
public void configureAdapter(@NonNull BaseSettingsAdapter adapter) {
|
||||
adapter.configureSingleSelect(this);
|
||||
adapter.configureCustomizableSingleSelect(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MappingModelList getSettings() {
|
||||
int trimLength = SignalStore.settings().isTrimByLengthEnabled() ? SignalStore.settings().getThreadTrimLength() : 0;
|
||||
int[] options = activity.getResources().getIntArray(R.array.conversation_length_limit);
|
||||
boolean hasSelection = false;
|
||||
MappingModelList settings = new MappingModelList();
|
||||
|
||||
for (int option : options) {
|
||||
boolean isSelected = option == trimLength;
|
||||
String text = option == 0 ? activity.getString(R.string.preferences_storage__none)
|
||||
: activity.getResources().getQuantityString(R.plurals.preferences_storage__s_messages_plural, option, NumberFormat.getInstance().format(option));
|
||||
|
||||
settings.add(new SingleSelectSetting.Item(option, text, null, isSelected));
|
||||
|
||||
hasSelection = hasSelection || isSelected;
|
||||
}
|
||||
|
||||
int currentValue = SignalStore.settings().getThreadTrimLength();
|
||||
settings.add(new CustomizableSingleSelectSetting.Item(CUSTOM_LENGTH,
|
||||
activity.getString(R.string.preferences_storage__custom),
|
||||
!hasSelection,
|
||||
currentValue,
|
||||
activity.getResources().getQuantityString(R.plurals.preferences_storage__s_messages_plural, currentValue, NumberFormat.getInstance().format(currentValue))));
|
||||
return settings;
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
@Override
|
||||
public void onCustomizeClicked(@Nullable CustomizableSingleSelectSetting.Item item) {
|
||||
boolean trimLengthEnabled = SignalStore.settings().isTrimByLengthEnabled();
|
||||
int trimLength = trimLengthEnabled ? SignalStore.settings().getThreadTrimLength() : 0;
|
||||
|
||||
View view = LayoutInflater.from(activity).inflate(R.layout.customizable_setting_edit_text, null, false);
|
||||
EditText editText = view.findViewById(R.id.customizable_setting_edit_text);
|
||||
if (trimLength > 0) {
|
||||
editText.setText(String.valueOf(trimLength));
|
||||
}
|
||||
|
||||
AlertDialog dialog = new MaterialAlertDialogBuilder(activity)
|
||||
.setTitle(R.string.preferences__conversation_length_limit)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, (d, w) -> onSelectionChanged(Integer.parseInt(editText.getText().toString())))
|
||||
.setNegativeButton(android.R.string.cancel, (d, w) -> updateSettingsList())
|
||||
.create();
|
||||
|
||||
dialog.setOnShowListener(d -> {
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(!TextUtils.isEmpty(editText.getText()));
|
||||
editText.requestFocus();
|
||||
editText.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void afterTextChanged(@NonNull Editable sequence) {
|
||||
CharSequence trimmed = StringUtil.trimSequence(sequence);
|
||||
if (TextUtils.isEmpty(trimmed)) {
|
||||
sequence.replace(0, sequence.length(), "");
|
||||
} else {
|
||||
try {
|
||||
Integer.parseInt(trimmed.toString());
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
|
||||
return;
|
||||
} catch (NumberFormatException e) {
|
||||
String onlyDigits = trimmed.toString().replaceAll("[^\\d]", "");
|
||||
if (!onlyDigits.equals(trimmed.toString())) {
|
||||
sequence.replace(0, sequence.length(), onlyDigits);
|
||||
}
|
||||
}
|
||||
}
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(@NonNull CharSequence sequence, int start, int count, int after) {}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(@NonNull CharSequence sequence, int start, int before, int count) {}
|
||||
});
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectionChanged(@NonNull Object selection) {
|
||||
boolean trimLengthEnabled = SignalStore.settings().isTrimByLengthEnabled();
|
||||
int trimLength = trimLengthEnabled ? SignalStore.settings().getThreadTrimLength() : 0;
|
||||
int newTrimLength = (Integer) selection;
|
||||
|
||||
if (newTrimLength > 0 && (!trimLengthEnabled || newTrimLength < trimLength)) {
|
||||
new MaterialAlertDialogBuilder(activity)
|
||||
.setTitle(R.string.preferences_storage__delete_older_messages)
|
||||
.setMessage(activity.getResources().getQuantityString(R.plurals.preferences_storage__this_will_permanently_trim_all_conversations_to_the_d_most_recent_messages, newTrimLength, newTrimLength))
|
||||
.setPositiveButton(R.string.delete, (d, w) -> updateTrimByLength(newTrimLength))
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
} else if (newTrimLength == CUSTOM_LENGTH) {
|
||||
onCustomizeClicked(null);
|
||||
} else {
|
||||
updateTrimByLength(newTrimLength);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTrimByLength(int length) {
|
||||
boolean restrictingChange = !SignalStore.settings().isTrimByLengthEnabled() || length < SignalStore.settings().getThreadTrimLength();
|
||||
|
||||
SignalStore.settings().setThreadTrimByLengthEnabled(length > 0);
|
||||
SignalStore.settings().setThreadTrimLength(length);
|
||||
updateSettingsList();
|
||||
|
||||
if (SignalStore.settings().isTrimByLengthEnabled() && restrictingChange) {
|
||||
KeepMessagesDuration keepMessagesDuration = SignalStore.settings().getKeepMessagesDuration();
|
||||
|
||||
long trimBeforeDate = keepMessagesDuration != KeepMessagesDuration.FOREVER ? System.currentTimeMillis() - keepMessagesDuration.getDuration()
|
||||
: ThreadTable.NO_TRIM_BEFORE_DATE_SET;
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> SignalDatabase.threads().trimAllThreads(length, trimBeforeDate));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
package org.thoughtcrime.securesms.preferences.widgets;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.preference.PreferenceCategory;
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
public final class StoragePreferenceCategory extends PreferenceCategory {
|
||||
|
||||
private Runnable onFreeUpSpace;
|
||||
private TextView totalSize;
|
||||
private StorageGraphView storageGraphView;
|
||||
private StorageGraphView.StorageBreakdown storage;
|
||||
|
||||
public StoragePreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
initialize();
|
||||
}
|
||||
|
||||
public StoragePreferenceCategory(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initialize();
|
||||
}
|
||||
|
||||
public StoragePreferenceCategory(Context context) {
|
||||
super(context);
|
||||
initialize();
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
setLayoutResource(R.layout.preference_storage_category);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder view) {
|
||||
super.onBindViewHolder(view);
|
||||
|
||||
totalSize = (TextView) view.findViewById(R.id.total_size);
|
||||
storageGraphView = (StorageGraphView) view.findViewById(R.id.storageGraphView);
|
||||
|
||||
view.findViewById(R.id.free_up_space)
|
||||
.setOnClickListener(v -> {
|
||||
if (onFreeUpSpace != null) {
|
||||
onFreeUpSpace.run();
|
||||
}
|
||||
});
|
||||
|
||||
totalSize.setText(Util.getPrettyFileSize(0));
|
||||
|
||||
if (storage != null) {
|
||||
setStorage(storage);
|
||||
}
|
||||
}
|
||||
|
||||
public void setOnFreeUpSpace(Runnable onFreeUpSpace) {
|
||||
this.onFreeUpSpace = onFreeUpSpace;
|
||||
}
|
||||
|
||||
public void setStorage(StorageGraphView.StorageBreakdown storage) {
|
||||
this.storage = storage;
|
||||
if (totalSize != null) {
|
||||
totalSize.setText(Util.getPrettyFileSize(storage.getTotalSize()));
|
||||
}
|
||||
if (storageGraphView != null) {
|
||||
storageGraphView.setStorageBreakdown(storage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,21 +80,15 @@
|
||||
app:layout_constraintStart_toEndOf="@+id/legend_audio"
|
||||
app:layout_constraintTop_toTopOf="@+id/legend_audio" />
|
||||
|
||||
<TextView
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/free_up_space"
|
||||
android:layout_width="match_parent"
|
||||
style="@style/Signal.Widget.Button.Large.Tonal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?selectableItemBackground"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:gravity="center"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/preferences_storage__review_storage"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@color/core_ultramarine"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/legend_photos" />
|
||||
|
||||
|
||||
@@ -367,7 +367,7 @@
|
||||
|
||||
<fragment
|
||||
android:id="@+id/storagePreferenceFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.wrapped.WrappedStoragePreferenceFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.storage.ManageStorageSettingsFragment"
|
||||
android:label="storage_preference_fragment" />
|
||||
|
||||
<fragment
|
||||
|
||||
@@ -3279,7 +3279,8 @@
|
||||
<string name="preferences_storage__audio">Audio</string>
|
||||
<string name="preferences_storage__review_storage">Review storage</string>
|
||||
<string name="preferences_storage__delete_older_messages">Delete older messages?</string>
|
||||
<string name="preferences_storage__clear_message_history">Clear message history?</string>
|
||||
<!-- Dialog title shown when selecting delete message history from manage storage -->
|
||||
<string name="preferences_storage__delete_message_history">Delete message history?</string>
|
||||
<string name="preferences_storage__this_will_permanently_delete_all_message_history_and_media">This will permanently delete all message history and media from your device that are older than %1$s.</string>
|
||||
<!-- The body of an alert dialog that is shown when confirming a trim operation. Trimming will delete all but the most recent messages in a chat. The placeholder represents how many messages are kept in each chat. All older messages are deleted. -->
|
||||
<plurals name="preferences_storage__this_will_permanently_trim_all_conversations_to_the_d_most_recent_messages">
|
||||
@@ -6825,6 +6826,15 @@
|
||||
<!-- Displayed as the user\'s payment method as a label in a preference row -->
|
||||
<string name="BackupsTypeSettingsFragment__unknown">Unknown</string>
|
||||
|
||||
<!-- ManageStorageSettingsFragment -->
|
||||
<!-- Settings section title header show above manage settings options for automatic chat history management (deleting) -->
|
||||
<string name="ManageStorageSettingsFragment_chat_limit">Chat limits</string>
|
||||
<!-- Settings row title text that can be clicked to delete all messages and associated data from the device -->
|
||||
<string name="ManageStorageSettingsFragment_delete_message_history">Delete message history</string>
|
||||
<!-- Warning message at the bottom of a settings screen indicating how messages will be deleted based on user's selection (time is a range like 30 days or 1 year etc) -->
|
||||
<string name="ManageStorageSettingsFragment_keep_messages_duration_warning">Messages older than the selected time will be permanently deleted.</string>
|
||||
<!-- Warning message at the bottom of a settings screen indicating how messages will be deleted based on user's selection (limit is a number like 500 or 5,000) -->
|
||||
<string name="ManageStorageSettingsFragment_chat_length_limit_warning">Messages exceeding the selected length will be permanently deleted.</string>
|
||||
|
||||
<!-- EOF -->
|
||||
</resources>
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<PreferenceCategory android:title="@string/preferences_storage__storage_usage" />
|
||||
|
||||
<org.thoughtcrime.securesms.preferences.widgets.StoragePreferenceCategory android:key="pref_storage_category" />
|
||||
|
||||
<PreferenceCategory android:layout="@layout/preference_divider" />
|
||||
|
||||
<PreferenceCategory
|
||||
android:key="storage_limits"
|
||||
android:title="@string/preferences_chats__message_history">
|
||||
|
||||
<Preference
|
||||
android:key="settings.keep_messages_duration"
|
||||
android:title="@string/preferences__keep_messages"
|
||||
tools:summary="@string/preferences_storage__forever" />
|
||||
|
||||
<Preference
|
||||
android:inputType="number"
|
||||
android:key="pref_trim_length"
|
||||
android:title="@string/preferences__conversation_length_limit"
|
||||
tools:summary="None" />
|
||||
|
||||
<Preference
|
||||
android:key="pref_storage_clear_message_history"
|
||||
android:title="@string/preferences__clear_message_history" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
||||
@@ -86,8 +86,8 @@ object Dialogs {
|
||||
text = { Text(text = body) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onConfirm()
|
||||
onDismiss()
|
||||
onConfirm()
|
||||
}) {
|
||||
Text(text = confirm, color = confirmColor)
|
||||
}
|
||||
|
||||
@@ -26,16 +26,14 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.signal.core.ui.Rows.TextAndLabel
|
||||
|
||||
object Rows {
|
||||
|
||||
/**
|
||||
* A row consisting of a radio button and text, which takes up the full
|
||||
* width of the screen.
|
||||
* A row consisting of a radio button and [text] and optional [label] in a [TextAndLabel].
|
||||
*/
|
||||
@Composable
|
||||
fun RadioRow(
|
||||
@@ -44,6 +42,30 @@ object Rows {
|
||||
modifier: Modifier = Modifier,
|
||||
label: String? = null,
|
||||
enabled: Boolean = true
|
||||
) {
|
||||
RadioRow(
|
||||
content = {
|
||||
TextAndLabel(
|
||||
text = text,
|
||||
label = label,
|
||||
enabled = enabled
|
||||
)
|
||||
},
|
||||
selected = selected,
|
||||
modifier = modifier,
|
||||
enabled = enabled
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Customizable radio row that allows [content] to be provided as composable functions instead of primitives.
|
||||
*/
|
||||
@Composable
|
||||
fun RadioRow(
|
||||
content: @Composable RowScope.() -> Unit,
|
||||
selected: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
@@ -58,70 +80,66 @@ object Rows {
|
||||
modifier = Modifier.padding(end = 24.dp)
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.alpha(if (enabled) 1f else 0.4f)
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
|
||||
if (label != null) {
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Row that positions [text] and optional [label] in a [TextAndLabel] to the side of a [Switch].
|
||||
*/
|
||||
@Composable
|
||||
fun ToggleRow(
|
||||
checked: Boolean,
|
||||
text: String,
|
||||
textColor: Color = MaterialTheme.colorScheme.onSurface,
|
||||
onCheckChanged: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
modifier: Modifier = Modifier,
|
||||
label: String? = null,
|
||||
textColor: Color = MaterialTheme.colorScheme.onSurface,
|
||||
enabled: Boolean = true
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(defaultPadding())
|
||||
.padding(defaultPadding()),
|
||||
verticalAlignment = CenterVertically
|
||||
) {
|
||||
Text(
|
||||
TextAndLabel(
|
||||
text = text,
|
||||
color = textColor,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.align(CenterVertically)
|
||||
label = label,
|
||||
textColor = textColor,
|
||||
enabled = enabled,
|
||||
modifier = Modifier.padding(end = 16.dp)
|
||||
)
|
||||
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = onCheckChanged,
|
||||
modifier = Modifier.align(CenterVertically)
|
||||
enabled = enabled,
|
||||
onCheckedChange = onCheckChanged
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Text row that positions [text] and optional [label] in a [TextAndLabel] to the side of an optional [icon].
|
||||
*/
|
||||
@Composable
|
||||
fun TextRow(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
iconModifier: Modifier = Modifier,
|
||||
label: String? = null,
|
||||
icon: Painter? = null,
|
||||
foregroundTint: Color = MaterialTheme.colorScheme.onSurface,
|
||||
onClick: (() -> Unit)? = null
|
||||
onClick: (() -> Unit)? = null,
|
||||
enabled: Boolean = true
|
||||
) {
|
||||
TextRow(
|
||||
text = {
|
||||
Text(
|
||||
TextAndLabel(
|
||||
text = text,
|
||||
color = foregroundTint,
|
||||
modifier = Modifier.align(CenterVertically)
|
||||
label = label,
|
||||
textColor = foregroundTint,
|
||||
enabled = enabled
|
||||
)
|
||||
},
|
||||
icon = if (icon != null) {
|
||||
@@ -137,22 +155,28 @@ object Rows {
|
||||
null
|
||||
},
|
||||
modifier = modifier,
|
||||
onClick = onClick
|
||||
onClick = onClick,
|
||||
enabled = enabled
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Customizable text row that allows [text] and [icon] to be provided as composable functions instead of primitives.
|
||||
*/
|
||||
@Composable
|
||||
fun TextRow(
|
||||
text: @Composable RowScope.() -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
icon: (@Composable RowScope.() -> Unit)? = null,
|
||||
onClick: (() -> Unit)? = null
|
||||
onClick: (() -> Unit)? = null,
|
||||
enabled: Boolean = true
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(enabled = onClick != null, onClick = onClick ?: {})
|
||||
.padding(defaultPadding())
|
||||
.clickable(enabled = enabled && onClick != null, onClick = onClick ?: {})
|
||||
.padding(defaultPadding()),
|
||||
verticalAlignment = CenterVertically
|
||||
) {
|
||||
if (icon != null) {
|
||||
icon()
|
||||
@@ -169,12 +193,45 @@ object Rows {
|
||||
vertical = 16.dp
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Row component to position text above an optional label.
|
||||
*/
|
||||
@Composable
|
||||
fun RowScope.TextAndLabel(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
label: String? = null,
|
||||
enabled: Boolean = true,
|
||||
textColor: Color = MaterialTheme.colorScheme.onSurface,
|
||||
textStyle: TextStyle = MaterialTheme.typography.bodyLarge
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.alpha(if (enabled) 1f else 0.4f)
|
||||
.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = textStyle,
|
||||
color = textColor
|
||||
)
|
||||
|
||||
if (label != null) {
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun RadioRowPreview() {
|
||||
SignalTheme(isDarkMode = false) {
|
||||
Previews.Preview {
|
||||
var selected by remember { mutableStateOf(true) }
|
||||
|
||||
Rows.RadioRow(
|
||||
@@ -188,15 +245,16 @@ private fun RadioRowPreview() {
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun ToggleRowPreview() {
|
||||
SignalTheme(isDarkMode = false) {
|
||||
Previews.Preview {
|
||||
var checked by remember { mutableStateOf(false) }
|
||||
|
||||
Rows.ToggleRow(
|
||||
checked = checked,
|
||||
text = "ToggleRow",
|
||||
label = "ToggleRow label",
|
||||
onCheckChanged = {
|
||||
checked = it
|
||||
}
|
||||
@@ -210,7 +268,26 @@ private fun TextRowPreview() {
|
||||
Previews.Preview {
|
||||
Rows.TextRow(
|
||||
text = "TextRow",
|
||||
icon = painterResource(id = android.R.drawable.ic_menu_camera)
|
||||
icon = painterResource(id = android.R.drawable.ic_menu_camera),
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun TextAndLabelPreview() {
|
||||
Previews.Preview {
|
||||
Row {
|
||||
TextAndLabel(
|
||||
text = "TextAndLabel Text",
|
||||
label = "TextAndLabel Label"
|
||||
)
|
||||
TextAndLabel(
|
||||
text = "TextAndLabel Text",
|
||||
label = "TextAndLabel Label",
|
||||
enabled = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user