mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 14:27:57 +01:00 
			
		
		
		
	Use Stable interface for Updates screen + Cleanup (#7627)
* Use Stable interface for Updates screen + Cleanup Co-Authored-By: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com> * Disable swipe refresh in selection mode * Review Changes Co-Authored-By: Andreas <6576096+ghostbear@users.noreply.github.com> * Review Changes 2 Co-authored-by: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com> Co-authored-by: Andreas <6576096+ghostbear@users.noreply.github.com>
This commit is contained in:
		| @@ -0,0 +1,34 @@ | ||||
| package eu.kanade.presentation.updates | ||||
|  | ||||
| import androidx.compose.material3.AlertDialog | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.material3.TextButton | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import eu.kanade.tachiyomi.R | ||||
|  | ||||
| @Composable | ||||
| fun UpdatesDeleteConfirmationDialog( | ||||
|     onDismissRequest: () -> Unit, | ||||
|     onConfirm: () -> Unit, | ||||
| ) { | ||||
|     AlertDialog( | ||||
|         text = { | ||||
|             Text(text = stringResource(R.string.confirm_delete_chapters)) | ||||
|         }, | ||||
|         onDismissRequest = onDismissRequest, | ||||
|         confirmButton = { | ||||
|             TextButton(onClick = { | ||||
|                 onConfirm() | ||||
|                 onDismissRequest() | ||||
|             },) { | ||||
|                 Text(text = stringResource(android.R.string.ok)) | ||||
|             } | ||||
|         }, | ||||
|         dismissButton = { | ||||
|             TextButton(onClick = onDismissRequest) { | ||||
|                 Text(text = stringResource(android.R.string.cancel)) | ||||
|             } | ||||
|         }, | ||||
|     ) | ||||
| } | ||||
| @@ -1,6 +1,7 @@ | ||||
| package eu.kanade.presentation.updates | ||||
|  | ||||
| import androidx.activity.compose.BackHandler | ||||
| import androidx.compose.foundation.layout.PaddingValues | ||||
| import androidx.compose.foundation.layout.WindowInsets | ||||
| import androidx.compose.foundation.layout.WindowInsetsSides | ||||
| import androidx.compose.foundation.layout.asPaddingValues | ||||
| @@ -20,9 +21,9 @@ import androidx.compose.material.icons.filled.SelectAll | ||||
| import androidx.compose.material3.Icon | ||||
| import androidx.compose.material3.IconButton | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.toMutableStateList | ||||
| import androidx.compose.runtime.LaunchedEffect | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import androidx.compose.ui.platform.LocalLayoutDirection | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import com.google.accompanist.swiperefresh.SwipeRefresh | ||||
| @@ -38,97 +39,78 @@ import eu.kanade.presentation.util.bottomNavPaddingValues | ||||
| import eu.kanade.presentation.util.plus | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.download.model.Download | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.data.library.LibraryUpdateService | ||||
| import eu.kanade.tachiyomi.ui.reader.ReaderActivity | ||||
| import eu.kanade.tachiyomi.ui.recent.updates.UpdatesItem | ||||
| import eu.kanade.tachiyomi.ui.recent.updates.UpdatesState | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.text.DateFormat | ||||
| import eu.kanade.tachiyomi.ui.recent.updates.UpdatesPresenter | ||||
| import eu.kanade.tachiyomi.ui.recent.updates.UpdatesPresenter.Dialog | ||||
| import eu.kanade.tachiyomi.ui.recent.updates.UpdatesPresenter.Event | ||||
| import eu.kanade.tachiyomi.util.system.toast | ||||
| import kotlinx.coroutines.flow.collectLatest | ||||
| import java.util.Date | ||||
|  | ||||
| @Composable | ||||
| fun UpdateScreen( | ||||
|     state: UpdatesState.Success, | ||||
|     presenter: UpdatesPresenter, | ||||
|     onClickCover: (UpdatesItem) -> Unit, | ||||
|     onClickUpdate: (UpdatesItem) -> Unit, | ||||
|     onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit, | ||||
|     onUpdateLibrary: () -> Unit, | ||||
|     onBackClicked: () -> Unit, | ||||
|     // For bottom action menu | ||||
|     onMultiBookmarkClicked: (List<UpdatesItem>, bookmark: Boolean) -> Unit, | ||||
|     onMultiMarkAsReadClicked: (List<UpdatesItem>, read: Boolean) -> Unit, | ||||
|     onMultiDeleteClicked: (List<UpdatesItem>) -> Unit, | ||||
|     // Miscellaneous | ||||
|     preferences: PreferencesHelper = Injekt.get(), | ||||
|     onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit, | ||||
| ) { | ||||
|     val updatesListState = rememberLazyListState() | ||||
|     val insetPaddingValue = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues() | ||||
|  | ||||
|     val relativeTime: Int = remember { preferences.relativeTime().get() } | ||||
|     val dateFormat: DateFormat = remember { preferences.dateFormat() } | ||||
|  | ||||
|     val uiModels = remember(state) { | ||||
|         state.uiModels | ||||
|     } | ||||
|     val itemUiModels = remember(uiModels) { | ||||
|         uiModels.filterIsInstance<UpdatesUiModel.Item>() | ||||
|     } | ||||
|     // To prevent selection from getting removed during an update to a item in list | ||||
|     val updateIdList = remember(itemUiModels) { | ||||
|         itemUiModels.map { it.item.update.chapterId } | ||||
|     } | ||||
|     val selected = remember(updateIdList) { | ||||
|         emptyList<UpdatesUiModel.Item>().toMutableStateList() | ||||
|     } | ||||
|     // First and last selected index in list | ||||
|     val selectedPositions = remember(uiModels) { arrayOf(-1, -1) } | ||||
|  | ||||
|     val internalOnBackPressed = { | ||||
|         if (selected.isNotEmpty()) { | ||||
|             selected.clear() | ||||
|         if (presenter.selectionMode) { | ||||
|             presenter.toggleAllSelection(false) | ||||
|         } else { | ||||
|             onBackClicked() | ||||
|         } | ||||
|     } | ||||
|     BackHandler(onBack = internalOnBackPressed) | ||||
|  | ||||
|     val context = LocalContext.current | ||||
|  | ||||
|     val onUpdateLibrary = { | ||||
|         if (LibraryUpdateService.start(context)) { | ||||
|             context.toast(R.string.updating_library) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     Scaffold( | ||||
|         modifier = Modifier | ||||
|             .padding(insetPaddingValue), | ||||
|         topBar = { | ||||
|             UpdatesAppBar( | ||||
|                 selected = selected, | ||||
|                 incognitoMode = state.isIncognitoMode, | ||||
|                 downloadedOnlyMode = state.isDownloadedOnlyMode, | ||||
|                 incognitoMode = presenter.isIncognitoMode, | ||||
|                 downloadedOnlyMode = presenter.isDownloadOnly, | ||||
|                 onUpdateLibrary = onUpdateLibrary, | ||||
|                 actionModeCounter = selected.size, | ||||
|                 onSelectAll = { | ||||
|                     selected.clear() | ||||
|                     selected.addAll(itemUiModels) | ||||
|                 }, | ||||
|                 onInvertSelection = { | ||||
|                     val toSelect = itemUiModels - selected | ||||
|                     selected.clear() | ||||
|                     selected.addAll(toSelect) | ||||
|                 }, | ||||
|                 actionModeCounter = presenter.selected.size, | ||||
|                 onSelectAll = { presenter.toggleAllSelection(true) }, | ||||
|                 onInvertSelection = { presenter.invertSelection() }, | ||||
|                 onCancelActionMode = { presenter.toggleAllSelection(false) }, | ||||
|             ) | ||||
|         }, | ||||
|         bottomBar = { | ||||
|             UpdatesBottomBar( | ||||
|                 selected = selected, | ||||
|                 selected = presenter.selected, | ||||
|                 onDownloadChapter = onDownloadChapter, | ||||
|                 onMultiBookmarkClicked = onMultiBookmarkClicked, | ||||
|                 onMultiMarkAsReadClicked = onMultiMarkAsReadClicked, | ||||
|                 onMultiDeleteClicked = onMultiDeleteClicked, | ||||
|                 onMultiBookmarkClicked = presenter::bookmarkUpdates, | ||||
|                 onMultiMarkAsReadClicked = presenter::markUpdatesRead, | ||||
|                 onMultiDeleteClicked = { | ||||
|                     val updateItems = presenter.selected.map { it.item } | ||||
|                     presenter.dialog = Dialog.DeleteConfirmation(updateItems) | ||||
|                 }, | ||||
|             ) | ||||
|         }, | ||||
|     ) { contentPadding -> | ||||
|         val contentPaddingWithNavBar = bottomNavPaddingValues + contentPadding + | ||||
|             WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues() | ||||
|         // During selection mode bottom nav is not visible | ||||
|         val contentPaddingWithNavBar = (if (presenter.selectionMode) PaddingValues() else bottomNavPaddingValues) + | ||||
|             contentPadding + WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues() | ||||
|  | ||||
|         SwipeRefresh( | ||||
|             state = rememberSwipeRefreshState(state.showSwipeRefreshIndicator), | ||||
|             state = rememberSwipeRefreshState(isRefreshing = false), | ||||
|             onRefresh = onUpdateLibrary, | ||||
|             swipeEnabled = presenter.selectionMode.not(), | ||||
|             indicatorPadding = contentPaddingWithNavBar, | ||||
|             indicator = { s, trigger -> | ||||
|                 SwipeRefreshIndicator( | ||||
| @@ -137,7 +119,7 @@ fun UpdateScreen( | ||||
|                 ) | ||||
|             }, | ||||
|         ) { | ||||
|             if (uiModels.isEmpty()) { | ||||
|             if (presenter.uiModels.isEmpty()) { | ||||
|                 EmptyScreen(textResource = R.string.information_no_recent) | ||||
|             } else { | ||||
|                 VerticalFastScroller( | ||||
| @@ -152,27 +134,49 @@ fun UpdateScreen( | ||||
|                         contentPadding = contentPaddingWithNavBar, | ||||
|                     ) { | ||||
|                         updatesUiItems( | ||||
|                             uiModels = uiModels, | ||||
|                             itemUiModels = itemUiModels, | ||||
|                             selected = selected, | ||||
|                             selectedPositions = selectedPositions, | ||||
|                             uiModels = presenter.uiModels, | ||||
|                             selectionMode = presenter.selectionMode, | ||||
|                             onUpdateSelected = presenter::toggleSelection, | ||||
|                             onClickCover = onClickCover, | ||||
|                             onClickUpdate = onClickUpdate, | ||||
|                             onClickUpdate = { | ||||
|                                 val intent = ReaderActivity.newIntent(context, it.update.mangaId, it.update.chapterId) | ||||
|                                 context.startActivity(intent) | ||||
|                             }, | ||||
|                             onDownloadChapter = onDownloadChapter, | ||||
|                             relativeTime = relativeTime, | ||||
|                             dateFormat = dateFormat, | ||||
|                             relativeTime = presenter.relativeTime, | ||||
|                             dateFormat = presenter.dateFormat, | ||||
|                         ) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     val onDismissDialog = { presenter.dialog = null } | ||||
|     when (val dialog = presenter.dialog) { | ||||
|         is Dialog.DeleteConfirmation -> { | ||||
|             UpdatesDeleteConfirmationDialog( | ||||
|                 onDismissRequest = onDismissDialog, | ||||
|                 onConfirm = { | ||||
|                     presenter.deleteChapters(dialog.toDelete) | ||||
|                     presenter.toggleAllSelection(false) | ||||
|                 }, | ||||
|             ) | ||||
|         } | ||||
|         null -> {} | ||||
|     } | ||||
|     LaunchedEffect(Unit) { | ||||
|         presenter.events.collectLatest { event -> | ||||
|             when (event) { | ||||
|                 Event.InternalError -> context.toast(R.string.internal_error) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| fun UpdatesAppBar( | ||||
|     modifier: Modifier = Modifier, | ||||
|     selected: MutableList<UpdatesUiModel.Item>, | ||||
|     incognitoMode: Boolean, | ||||
|     downloadedOnlyMode: Boolean, | ||||
|     onUpdateLibrary: () -> Unit, | ||||
| @@ -180,6 +184,7 @@ fun UpdatesAppBar( | ||||
|     actionModeCounter: Int, | ||||
|     onSelectAll: () -> Unit, | ||||
|     onInvertSelection: () -> Unit, | ||||
|     onCancelActionMode: () -> Unit, | ||||
| ) { | ||||
|     AppBar( | ||||
|         modifier = modifier, | ||||
| @@ -193,7 +198,7 @@ fun UpdatesAppBar( | ||||
|             } | ||||
|         }, | ||||
|         actionModeCounter = actionModeCounter, | ||||
|         onCancelActionMode = { selected.clear() }, | ||||
|         onCancelActionMode = onCancelActionMode, | ||||
|         actionModeActions = { | ||||
|             IconButton(onClick = onSelectAll) { | ||||
|                 Icon( | ||||
| @@ -215,7 +220,7 @@ fun UpdatesAppBar( | ||||
|  | ||||
| @Composable | ||||
| fun UpdatesBottomBar( | ||||
|     selected: MutableList<UpdatesUiModel.Item>, | ||||
|     selected: List<UpdatesUiModel.Item>, | ||||
|     onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit, | ||||
|     onMultiBookmarkClicked: (List<UpdatesItem>, bookmark: Boolean) -> Unit, | ||||
|     onMultiMarkAsReadClicked: (List<UpdatesItem>, read: Boolean) -> Unit, | ||||
| @@ -226,29 +231,23 @@ fun UpdatesBottomBar( | ||||
|         modifier = Modifier.fillMaxWidth(), | ||||
|         onBookmarkClicked = { | ||||
|             onMultiBookmarkClicked.invoke(selected.map { it.item }, true) | ||||
|             selected.clear() | ||||
|         }.takeIf { selected.any { !it.item.update.bookmark } }, | ||||
|         onRemoveBookmarkClicked = { | ||||
|             onMultiBookmarkClicked.invoke(selected.map { it.item }, false) | ||||
|             selected.clear() | ||||
|         }.takeIf { selected.all { it.item.update.bookmark } }, | ||||
|         onMarkAsReadClicked = { | ||||
|             onMultiMarkAsReadClicked(selected.map { it.item }, true) | ||||
|             selected.clear() | ||||
|         }.takeIf { selected.any { !it.item.update.read } }, | ||||
|         onMarkAsUnreadClicked = { | ||||
|             onMultiMarkAsReadClicked(selected.map { it.item }, false) | ||||
|             selected.clear() | ||||
|         }.takeIf { selected.any { it.item.update.read } }, | ||||
|         onDownloadClicked = { | ||||
|             onDownloadChapter(selected.map { it.item }, ChapterDownloadAction.START) | ||||
|             selected.clear() | ||||
|         }.takeIf { | ||||
|             selected.any { it.item.downloadStateProvider() != Download.State.DOWNLOADED } | ||||
|         }, | ||||
|         onDeleteClicked = { | ||||
|             onMultiDeleteClicked(selected.map { it.item }) | ||||
|             selected.clear() | ||||
|         }.takeIf { selected.any { it.item.downloadStateProvider() == Download.State.DOWNLOADED } }, | ||||
|     ) | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,28 @@ | ||||
| package eu.kanade.presentation.updates | ||||
|  | ||||
| import androidx.compose.runtime.Stable | ||||
| import androidx.compose.runtime.derivedStateOf | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.setValue | ||||
| import eu.kanade.tachiyomi.ui.recent.updates.UpdatesPresenter | ||||
|  | ||||
| @Stable | ||||
| interface UpdatesState { | ||||
|     val isLoading: Boolean | ||||
|     val uiModels: List<UpdatesUiModel> | ||||
|     val selected: List<UpdatesUiModel.Item> | ||||
|     val selectionMode: Boolean | ||||
|     var dialog: UpdatesPresenter.Dialog? | ||||
| } | ||||
| fun UpdatesState(): UpdatesState = UpdatesStateImpl() | ||||
| class UpdatesStateImpl : UpdatesState { | ||||
|     override var isLoading: Boolean by mutableStateOf(true) | ||||
|     override var uiModels: List<UpdatesUiModel> by mutableStateOf(emptyList()) | ||||
|     override val selected: List<UpdatesUiModel.Item> by derivedStateOf { | ||||
|         uiModels.filterIsInstance<UpdatesUiModel.Item>() | ||||
|             .filter { it.item.selected } | ||||
|     } | ||||
|     override val selectionMode: Boolean by derivedStateOf { selected.isNotEmpty() } | ||||
|     override var dialog: UpdatesPresenter.Dialog? by mutableStateOf(null) | ||||
| } | ||||
| @@ -26,7 +26,9 @@ import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.draw.alpha | ||||
| import androidx.compose.ui.graphics.Color | ||||
| import androidx.compose.ui.hapticfeedback.HapticFeedbackType | ||||
| import androidx.compose.ui.platform.LocalDensity | ||||
| import androidx.compose.ui.platform.LocalHapticFeedback | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import androidx.compose.ui.text.style.TextOverflow | ||||
| import androidx.compose.ui.unit.dp | ||||
| @@ -44,9 +46,8 @@ import java.text.DateFormat | ||||
|  | ||||
| fun LazyListScope.updatesUiItems( | ||||
|     uiModels: List<UpdatesUiModel>, | ||||
|     itemUiModels: List<UpdatesUiModel.Item>, | ||||
|     selected: MutableList<UpdatesUiModel.Item>, | ||||
|     selectedPositions: Array<Int>, | ||||
|     selectionMode: Boolean, | ||||
|     onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit, | ||||
|     onClickCover: (UpdatesItem) -> Unit, | ||||
|     onClickUpdate: (UpdatesItem) -> Unit, | ||||
|     onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit, | ||||
| @@ -78,35 +79,27 @@ fun LazyListScope.updatesUiItems( | ||||
|                 ) | ||||
|             } | ||||
|             is UpdatesUiModel.Item -> { | ||||
|                 val value = item.item | ||||
|                 val update = value.update | ||||
|                 val updatesItem = item.item | ||||
|                 val update = updatesItem.update | ||||
|                 UpdatesUiItem( | ||||
|                     modifier = Modifier.animateItemPlacement(), | ||||
|                     update = update, | ||||
|                     selected = selected.contains(item), | ||||
|                     onClick = { | ||||
|                         onUpdatesItemClick( | ||||
|                             updatesItem = item, | ||||
|                             selected = selected, | ||||
|                             updates = itemUiModels, | ||||
|                             selectedPositions = selectedPositions, | ||||
|                             onUpdateClicked = onClickUpdate, | ||||
|                         ) | ||||
|                     }, | ||||
|                     selected = updatesItem.selected, | ||||
|                     onLongClick = { | ||||
|                         onUpdatesItemLongClick( | ||||
|                             updatesItem = item, | ||||
|                             selected = selected, | ||||
|                             updates = itemUiModels, | ||||
|                             selectedPositions = selectedPositions, | ||||
|                         ) | ||||
|                         onUpdateSelected(updatesItem, !updatesItem.selected, true, true) | ||||
|                     }, | ||||
|                     onClickCover = { if (selected.size == 0) onClickCover(value) }, | ||||
|                     onClick = { | ||||
|                         when { | ||||
|                             selectionMode -> onUpdateSelected(updatesItem, !updatesItem.selected, true, false) | ||||
|                             else -> onClickUpdate(updatesItem) | ||||
|                         } | ||||
|                     }, | ||||
|                     onClickCover = { if (selectionMode.not()) onClickCover(updatesItem) }, | ||||
|                     onDownloadChapter = { | ||||
|                         if (selected.size == 0) onDownloadChapter(listOf(value), it) | ||||
|                         if (selectionMode.not()) onDownloadChapter(listOf(updatesItem), it) | ||||
|                     }, | ||||
|                     downloadStateProvider = value.downloadStateProvider, | ||||
|                     downloadProgressProvider = value.downloadProgressProvider, | ||||
|                     downloadStateProvider = updatesItem.downloadStateProvider, | ||||
|                     downloadProgressProvider = updatesItem.downloadProgressProvider, | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
| @@ -126,12 +119,16 @@ fun UpdatesUiItem( | ||||
|     downloadStateProvider: () -> Download.State, | ||||
|     downloadProgressProvider: () -> Int, | ||||
| ) { | ||||
|     val haptic = LocalHapticFeedback.current | ||||
|     Row( | ||||
|         modifier = modifier | ||||
|             .background(if (selected) MaterialTheme.colorScheme.surfaceVariant else Color.Transparent) | ||||
|             .combinedClickable( | ||||
|                 onClick = onClick, | ||||
|                 onLongClick = onLongClick, | ||||
|                 onLongClick = { | ||||
|                     onLongClick() | ||||
|                     haptic.performHapticFeedback(HapticFeedbackType.LongPress) | ||||
|                 }, | ||||
|             ) | ||||
|             .height(56.dp) | ||||
|             .padding(horizontal = horizontalPadding), | ||||
| @@ -198,73 +195,3 @@ fun UpdatesUiItem( | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| private fun onUpdatesItemLongClick( | ||||
|     updatesItem: UpdatesUiModel.Item, | ||||
|     selected: MutableList<UpdatesUiModel.Item>, | ||||
|     updates: List<UpdatesUiModel.Item>, | ||||
|     selectedPositions: Array<Int>, | ||||
| ): Boolean { | ||||
|     if (!selected.contains(updatesItem)) { | ||||
|         val selectedIndex = updates.indexOf(updatesItem) | ||||
|         if (selected.isEmpty()) { | ||||
|             selected.add(updatesItem) | ||||
|             selectedPositions[0] = selectedIndex | ||||
|             selectedPositions[1] = selectedIndex | ||||
|             return true | ||||
|         } | ||||
|  | ||||
|         // Try to select the items in-between when possible | ||||
|         val range: IntRange | ||||
|         if (selectedIndex < selectedPositions[0]) { | ||||
|             range = selectedIndex until selectedPositions[0] | ||||
|             selectedPositions[0] = selectedIndex | ||||
|         } else if (selectedIndex > selectedPositions[1]) { | ||||
|             range = (selectedPositions[1] + 1)..selectedIndex | ||||
|             selectedPositions[1] = selectedIndex | ||||
|         } else { | ||||
|             // Just select itself | ||||
|             range = selectedIndex..selectedIndex | ||||
|         } | ||||
|  | ||||
|         range.forEach { | ||||
|             val toAdd = updates[it] | ||||
|             if (!selected.contains(toAdd)) { | ||||
|                 selected.add(toAdd) | ||||
|             } | ||||
|         } | ||||
|         return true | ||||
|     } | ||||
|     return false | ||||
| } | ||||
|  | ||||
| private fun onUpdatesItemClick( | ||||
|     updatesItem: UpdatesUiModel.Item, | ||||
|     selected: MutableList<UpdatesUiModel.Item>, | ||||
|     updates: List<UpdatesUiModel.Item>, | ||||
|     selectedPositions: Array<Int>, | ||||
|     onUpdateClicked: (UpdatesItem) -> Unit, | ||||
| ) { | ||||
|     val selectedIndex = updates.indexOf(updatesItem) | ||||
|     when { | ||||
|         selected.contains(updatesItem) -> { | ||||
|             val removedIndex = updates.indexOf(updatesItem) | ||||
|             selected.remove(updatesItem) | ||||
|  | ||||
|             if (removedIndex == selectedPositions[0]) { | ||||
|                 selectedPositions[0] = updates.indexOfFirst { selected.contains(it) } | ||||
|             } else if (removedIndex == selectedPositions[1]) { | ||||
|                 selectedPositions[1] = updates.indexOfLast { selected.contains(it) } | ||||
|             } | ||||
|         } | ||||
|         selected.isNotEmpty() -> { | ||||
|             if (selectedIndex < selectedPositions[0]) { | ||||
|                 selectedPositions[0] = selectedIndex | ||||
|             } else if (selectedIndex > selectedPositions[1]) { | ||||
|                 selectedPositions[1] = selectedIndex | ||||
|             } | ||||
|             selected.add(updatesItem) | ||||
|         } | ||||
|         else -> onUpdateClicked(updatesItem.item) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,28 +1,19 @@ | ||||
| package eu.kanade.tachiyomi.ui.recent.updates | ||||
|  | ||||
| import androidx.activity.OnBackPressedDispatcherOwner | ||||
| import androidx.appcompat.app.AlertDialog | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.animation.Crossfade | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.LaunchedEffect | ||||
| import androidx.compose.runtime.collectAsState | ||||
| import androidx.compose.runtime.getValue | ||||
| import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||||
| import eu.kanade.presentation.components.ChapterDownloadAction | ||||
| import eu.kanade.presentation.components.LoadingScreen | ||||
| import eu.kanade.presentation.updates.UpdateScreen | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.download.DownloadService | ||||
| import eu.kanade.tachiyomi.data.download.model.Download | ||||
| import eu.kanade.tachiyomi.data.library.LibraryUpdateService | ||||
| import eu.kanade.tachiyomi.ui.base.controller.FullComposeController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.RootController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.pushController | ||||
| import eu.kanade.tachiyomi.ui.main.MainActivity | ||||
| import eu.kanade.tachiyomi.ui.manga.MangaController | ||||
| import eu.kanade.tachiyomi.ui.reader.ReaderActivity | ||||
| import eu.kanade.tachiyomi.util.system.toast | ||||
| import eu.kanade.tachiyomi.widget.materialdialogs.await | ||||
| import kotlinx.coroutines.launch | ||||
|  | ||||
| /** | ||||
| @@ -36,39 +27,27 @@ class UpdatesController : | ||||
|  | ||||
|     @Composable | ||||
|     override fun ComposeContent() { | ||||
|         val state by presenter.state.collectAsState() | ||||
|         when (state) { | ||||
|             is UpdatesState.Loading -> LoadingScreen() | ||||
|             is UpdatesState.Error -> Text(text = (state as UpdatesState.Error).error.message.orEmpty()) | ||||
|             is UpdatesState.Success -> | ||||
|         Crossfade(targetState = presenter.isLoading) { isLoading -> | ||||
|             if (isLoading) { | ||||
|                 LoadingScreen() | ||||
|             } else { | ||||
|                 UpdateScreen( | ||||
|                     state = (state as UpdatesState.Success), | ||||
|                     onClickCover = this::openManga, | ||||
|                     onClickUpdate = this::openChapter, | ||||
|                     onDownloadChapter = this::downloadChapters, | ||||
|                     onUpdateLibrary = this::updateLibrary, | ||||
|                     presenter = presenter, | ||||
|                     onClickCover = { item -> | ||||
|                         router.pushController(MangaController(item.update.mangaId)) | ||||
|                     }, | ||||
|                     onBackClicked = this::onBackClicked, | ||||
|                     // For bottom action menu | ||||
|                     onMultiBookmarkClicked = { updatesItems, bookmark -> | ||||
|                         presenter.bookmarkUpdates(updatesItems, bookmark) | ||||
|                     }, | ||||
|                     onMultiMarkAsReadClicked = { updatesItems, read -> | ||||
|                         presenter.markUpdatesRead(updatesItems, read) | ||||
|                     }, | ||||
|                     onMultiDeleteClicked = this::deleteChaptersWithConfirmation, | ||||
|                     onDownloadChapter = this::downloadChapters, | ||||
|                 ) | ||||
|         } | ||||
|         LaunchedEffect(state) { | ||||
|             if (state !is UpdatesState.Loading) { | ||||
|                 (activity as? MainActivity)?.ready = true | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun updateLibrary() { | ||||
|         activity?.let { | ||||
|             if (LibraryUpdateService.start(it)) { | ||||
|                 it.toast(R.string.updating_library) | ||||
|         LaunchedEffect(presenter.selectionMode) { | ||||
|             val activity = (activity as? MainActivity) ?: return@LaunchedEffect | ||||
|             activity.showBottomNav(presenter.selectionMode.not()) | ||||
|         } | ||||
|         LaunchedEffect(presenter.isLoading) { | ||||
|             if (presenter.isLoading.not()) { | ||||
|                 (activity as? MainActivity)?.ready = true | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| @@ -105,26 +84,7 @@ class UpdatesController : | ||||
|                     presenter.deleteChapters(items) | ||||
|                 } | ||||
|             } | ||||
|             presenter.toggleAllSelection(false) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun deleteChaptersWithConfirmation(items: List<UpdatesItem>) { | ||||
|         if (items.isEmpty()) return | ||||
|         viewScope.launch { | ||||
|             val result = MaterialAlertDialogBuilder(activity!!) | ||||
|                 .setMessage(R.string.confirm_delete_chapters) | ||||
|                 .await(android.R.string.ok, android.R.string.cancel) | ||||
|             if (result == AlertDialog.BUTTON_POSITIVE) presenter.deleteChapters(items) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun openChapter(item: UpdatesItem) { | ||||
|         val activity = activity ?: return | ||||
|         val intent = ReaderActivity.newIntent(activity, item.update.mangaId, item.update.chapterId) | ||||
|         startActivity(intent) | ||||
|     } | ||||
|  | ||||
|     private fun openManga(item: UpdatesItem) { | ||||
|         router.pushController(MangaController(item.update.mangaId)) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -2,6 +2,8 @@ package eu.kanade.tachiyomi.ui.recent.updates | ||||
|  | ||||
| import android.os.Bundle | ||||
| import androidx.compose.runtime.Immutable | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import eu.kanade.core.util.insertSeparators | ||||
| import eu.kanade.domain.chapter.interactor.GetChapter | ||||
| import eu.kanade.domain.chapter.interactor.SetReadStatus | ||||
| @@ -11,6 +13,8 @@ import eu.kanade.domain.chapter.model.toDbChapter | ||||
| import eu.kanade.domain.manga.interactor.GetManga | ||||
| import eu.kanade.domain.updates.interactor.GetUpdates | ||||
| import eu.kanade.domain.updates.model.UpdatesWithRelations | ||||
| import eu.kanade.presentation.updates.UpdatesState | ||||
| import eu.kanade.presentation.updates.UpdatesStateImpl | ||||
| import eu.kanade.presentation.updates.UpdatesUiModel | ||||
| import eu.kanade.tachiyomi.data.download.DownloadManager | ||||
| import eu.kanade.tachiyomi.data.download.model.Download | ||||
| @@ -20,23 +24,22 @@ import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter | ||||
| import eu.kanade.tachiyomi.util.lang.launchIO | ||||
| import eu.kanade.tachiyomi.util.lang.toDateKey | ||||
| import eu.kanade.tachiyomi.util.lang.withUIContext | ||||
| import eu.kanade.tachiyomi.util.preference.asHotFlow | ||||
| import eu.kanade.tachiyomi.util.system.logcat | ||||
| import kotlinx.coroutines.Job | ||||
| import kotlinx.coroutines.flow.MutableStateFlow | ||||
| import kotlinx.coroutines.flow.StateFlow | ||||
| import kotlinx.coroutines.flow.asStateFlow | ||||
| import kotlinx.coroutines.channels.Channel | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.catch | ||||
| import kotlinx.coroutines.flow.collectLatest | ||||
| import kotlinx.coroutines.flow.launchIn | ||||
| import kotlinx.coroutines.flow.update | ||||
| import kotlinx.coroutines.flow.receiveAsFlow | ||||
| import logcat.LogPriority | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.text.DateFormat | ||||
| import java.util.Calendar | ||||
| import java.util.Date | ||||
|  | ||||
| class UpdatesPresenter( | ||||
|     private val state: UpdatesStateImpl = UpdatesState() as UpdatesStateImpl, | ||||
|     private val updateChapter: UpdateChapter = Injekt.get(), | ||||
|     private val setReadStatus: SetReadStatus = Injekt.get(), | ||||
|     private val getUpdates: GetUpdates = Injekt.get(), | ||||
| @@ -44,29 +47,22 @@ class UpdatesPresenter( | ||||
|     private val sourceManager: SourceManager = Injekt.get(), | ||||
|     private val downloadManager: DownloadManager = Injekt.get(), | ||||
|     private val getChapter: GetChapter = Injekt.get(), | ||||
|     private val preferences: PreferencesHelper = Injekt.get(), | ||||
| ) : BasePresenter<UpdatesController>() { | ||||
|     preferences: PreferencesHelper = Injekt.get(), | ||||
| ) : BasePresenter<UpdatesController>(), UpdatesState by state { | ||||
|  | ||||
|     private val _state: MutableStateFlow<UpdatesState> = MutableStateFlow(UpdatesState.Loading) | ||||
|     val state: StateFlow<UpdatesState> = _state.asStateFlow() | ||||
|     val isDownloadOnly: Boolean by preferences.downloadedOnly().asState() | ||||
|  | ||||
|     /** | ||||
|      * Helper function to update the UI state only if it's currently in success state | ||||
|      */ | ||||
|     private fun updateSuccessState(func: (UpdatesState.Success) -> UpdatesState.Success) { | ||||
|         _state.update { if (it is UpdatesState.Success) func(it) else it } | ||||
|     } | ||||
|     val isIncognitoMode: Boolean by preferences.incognitoMode().asState() | ||||
|  | ||||
|     private var incognitoMode = false | ||||
|         set(value) { | ||||
|             updateSuccessState { it.copy(isIncognitoMode = value) } | ||||
|             field = value | ||||
|         } | ||||
|     private var downloadOnlyMode = false | ||||
|         set(value) { | ||||
|             updateSuccessState { it.copy(isDownloadedOnlyMode = value) } | ||||
|             field = value | ||||
|         } | ||||
|     val relativeTime: Int by preferences.relativeTime().asState() | ||||
|  | ||||
|     val dateFormat: DateFormat by mutableStateOf(preferences.dateFormat()) | ||||
|  | ||||
|     private val _events: Channel<Event> = Channel(Int.MAX_VALUE) | ||||
|     val events: Flow<Event> = _events.receiveAsFlow() | ||||
|  | ||||
|     // First and last selected index in list | ||||
|     private val selectedPositions: Array<Int> = arrayOf(-1, -1) | ||||
|  | ||||
|     /** | ||||
|      * Subscription to observe download status changes. | ||||
| @@ -85,38 +81,17 @@ class UpdatesPresenter( | ||||
|             } | ||||
|  | ||||
|             getUpdates.subscribe(calendar) | ||||
|                 .catch { exception -> | ||||
|                     _state.value = UpdatesState.Error(exception) | ||||
|                 .catch { | ||||
|                     logcat(LogPriority.ERROR, it) | ||||
|                     _events.send(Event.InternalError) | ||||
|                 } | ||||
|                 .collectLatest { updates -> | ||||
|                     val uiModels = updates.toUpdateUiModels() | ||||
|                     _state.update { currentState -> | ||||
|                         when (currentState) { | ||||
|                             is UpdatesState.Success -> currentState.copy(uiModels) | ||||
|                             is UpdatesState.Loading, is UpdatesState.Error -> | ||||
|                                 UpdatesState.Success( | ||||
|                                     uiModels = uiModels, | ||||
|                                     isIncognitoMode = incognitoMode, | ||||
|                                     isDownloadedOnlyMode = downloadOnlyMode, | ||||
|                                 ) | ||||
|                         } | ||||
|                     } | ||||
|                     state.uiModels = updates.toUpdateUiModels() | ||||
|                     state.isLoading = false | ||||
|  | ||||
|                     observeDownloads() | ||||
|                 } | ||||
|         } | ||||
|  | ||||
|         preferences.incognitoMode() | ||||
|             .asHotFlow { incognito -> | ||||
|                 incognitoMode = incognito | ||||
|             } | ||||
|             .launchIn(presenterScope) | ||||
|  | ||||
|         preferences.downloadedOnly() | ||||
|             .asHotFlow { downloadedOnly -> | ||||
|                 downloadOnlyMode = downloadedOnly | ||||
|             } | ||||
|             .launchIn(presenterScope) | ||||
|     } | ||||
|  | ||||
|     private fun List<UpdatesWithRelations>.toUpdateUiModels(): List<UpdatesUiModel> { | ||||
| @@ -182,24 +157,22 @@ class UpdatesPresenter( | ||||
|      * @param download download object containing progress. | ||||
|      */ | ||||
|     private fun updateDownloadState(download: Download) { | ||||
|         updateSuccessState { successState -> | ||||
|             val modifiedIndex = successState.uiModels.indexOfFirst { | ||||
|                 it is UpdatesUiModel.Item && it.item.update.chapterId == download.chapter.id | ||||
|             } | ||||
|             if (modifiedIndex < 0) return@updateSuccessState successState | ||||
|         val uiModels = state.uiModels | ||||
|         val modifiedIndex = uiModels.indexOfFirst { | ||||
|             it is UpdatesUiModel.Item && it.item.update.chapterId == download.chapter.id | ||||
|         } | ||||
|         if (modifiedIndex < 0) return | ||||
|  | ||||
|             val newUiModels = successState.uiModels.toMutableList().apply { | ||||
|                 var uiModel = removeAt(modifiedIndex) | ||||
|                 if (uiModel is UpdatesUiModel.Item) { | ||||
|                     val item = uiModel.item.copy( | ||||
|                         downloadStateProvider = { download.status }, | ||||
|                         downloadProgressProvider = { download.progress }, | ||||
|                     ) | ||||
|                     uiModel = UpdatesUiModel.Item(item) | ||||
|                 } | ||||
|                 add(modifiedIndex, uiModel) | ||||
|         state.uiModels = uiModels.toMutableList().apply { | ||||
|             var uiModel = removeAt(modifiedIndex) | ||||
|             if (uiModel is UpdatesUiModel.Item) { | ||||
|                 val item = uiModel.item.copy( | ||||
|                     downloadStateProvider = { download.status }, | ||||
|                     downloadProgressProvider = { download.progress }, | ||||
|                 ) | ||||
|                 uiModel = UpdatesUiModel.Item(item) | ||||
|             } | ||||
|             successState.copy(uiModels = newUiModels) | ||||
|             add(modifiedIndex, uiModel) | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -275,42 +248,131 @@ class UpdatesPresenter( | ||||
|                 val chapters = updates.mapNotNull { getChapter.await(it.update.chapterId)?.toDbChapter() } | ||||
|                 downloadManager.deleteChapters(chapters, manga, source).mapNotNull { it.id } | ||||
|             } | ||||
|             updateSuccessState { successState -> | ||||
|                 val deletedUpdates = successState.uiModels.filter { | ||||
|                     it is UpdatesUiModel.Item && deletedIds.contains(it.item.update.chapterId) | ||||
|                 } | ||||
|                 if (deletedUpdates.isEmpty()) return@updateSuccessState successState | ||||
|  | ||||
|                 // TODO: Don't do this fake status update | ||||
|                 val newUiModels = successState.uiModels.toMutableList().apply { | ||||
|                     deletedUpdates.forEach { deletedUpdate -> | ||||
|                         val modifiedIndex = indexOf(deletedUpdate) | ||||
|                         var uiModel = removeAt(modifiedIndex) | ||||
|                         if (uiModel is UpdatesUiModel.Item) { | ||||
|                             val item = uiModel.item.copy( | ||||
|                                 downloadStateProvider = { Download.State.NOT_DOWNLOADED }, | ||||
|                                 downloadProgressProvider = { 0 }, | ||||
|                             ) | ||||
|                             uiModel = UpdatesUiModel.Item(item) | ||||
|                         } | ||||
|                         add(modifiedIndex, uiModel) | ||||
|             val uiModels = state.uiModels | ||||
|             val deletedUpdates = uiModels.filter { | ||||
|                 it is UpdatesUiModel.Item && deletedIds.contains(it.item.update.chapterId) | ||||
|             } | ||||
|             if (deletedUpdates.isEmpty()) return@launchIO | ||||
|  | ||||
|             // TODO: Don't do this fake status update | ||||
|             state.uiModels = uiModels.toMutableList().apply { | ||||
|                 deletedUpdates.forEach { deletedUpdate -> | ||||
|                     val modifiedIndex = indexOf(deletedUpdate) | ||||
|                     var uiModel = removeAt(modifiedIndex) | ||||
|                     if (uiModel is UpdatesUiModel.Item) { | ||||
|                         val item = uiModel.item.copy( | ||||
|                             downloadStateProvider = { Download.State.NOT_DOWNLOADED }, | ||||
|                             downloadProgressProvider = { 0 }, | ||||
|                         ) | ||||
|                         uiModel = UpdatesUiModel.Item(item) | ||||
|                     } | ||||
|                     add(modifiedIndex, uiModel) | ||||
|                 } | ||||
|                 successState.copy(uiModels = newUiModels) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| sealed class UpdatesState { | ||||
|     object Loading : UpdatesState() | ||||
|     data class Error(val error: Throwable) : UpdatesState() | ||||
|     data class Success( | ||||
|         val uiModels: List<UpdatesUiModel>, | ||||
|         val isIncognitoMode: Boolean = false, | ||||
|         val isDownloadedOnlyMode: Boolean = false, | ||||
|         val showSwipeRefreshIndicator: Boolean = false, | ||||
|     ) : UpdatesState() | ||||
|     fun toggleSelection( | ||||
|         item: UpdatesItem, | ||||
|         selected: Boolean, | ||||
|         userSelected: Boolean = false, | ||||
|         fromLongPress: Boolean = false, | ||||
|     ) { | ||||
|         val uiModels = state.uiModels | ||||
|         val modifiedIndex = uiModels.indexOfFirst { | ||||
|             it is UpdatesUiModel.Item && it.item.update.chapterId == item.update.chapterId | ||||
|         } | ||||
|         if (modifiedIndex < 0) return | ||||
|  | ||||
|         val oldItem = (uiModels[modifiedIndex] as? UpdatesUiModel.Item)?.item ?: return | ||||
|         if ((oldItem.selected && selected) || (!oldItem.selected && !selected)) return | ||||
|  | ||||
|         state.uiModels = uiModels.toMutableList().apply { | ||||
|             val firstSelection = none { it is UpdatesUiModel.Item && it.item.selected } | ||||
|             var newItem = (removeAt(modifiedIndex) as? UpdatesUiModel.Item)?.item?.copy(selected = selected) ?: return@apply | ||||
|             add(modifiedIndex, UpdatesUiModel.Item(newItem)) | ||||
|  | ||||
|             if (selected && userSelected && fromLongPress) { | ||||
|                 if (firstSelection) { | ||||
|                     selectedPositions[0] = modifiedIndex | ||||
|                     selectedPositions[1] = modifiedIndex | ||||
|                 } else { | ||||
|                     // Try to select the items in-between when possible | ||||
|                     val range: IntRange | ||||
|                     if (modifiedIndex < selectedPositions[0]) { | ||||
|                         range = modifiedIndex + 1 until selectedPositions[0] | ||||
|                         selectedPositions[0] = modifiedIndex | ||||
|                     } else if (modifiedIndex > selectedPositions[1]) { | ||||
|                         range = (selectedPositions[1] + 1) until modifiedIndex | ||||
|                         selectedPositions[1] = modifiedIndex | ||||
|                     } else { | ||||
|                         // Just select itself | ||||
|                         range = IntRange.EMPTY | ||||
|                     } | ||||
|  | ||||
|                     range.forEach { | ||||
|                         var uiModel = removeAt(it) | ||||
|                         if (uiModel is UpdatesUiModel.Item) { | ||||
|                             newItem = uiModel.item.copy(selected = true) | ||||
|                             uiModel = UpdatesUiModel.Item(newItem) | ||||
|                         } | ||||
|                         add(it, uiModel) | ||||
|                     } | ||||
|                 } | ||||
|             } else if (userSelected && !fromLongPress) { | ||||
|                 if (!selected) { | ||||
|                     if (modifiedIndex == selectedPositions[0]) { | ||||
|                         selectedPositions[0] = indexOfFirst { it is UpdatesUiModel.Item && it.item.selected } | ||||
|                     } else if (modifiedIndex == selectedPositions[1]) { | ||||
|                         selectedPositions[1] = indexOfLast { it is UpdatesUiModel.Item && it.item.selected } | ||||
|                     } | ||||
|                 } else { | ||||
|                     if (modifiedIndex < selectedPositions[0]) { | ||||
|                         selectedPositions[0] = modifiedIndex | ||||
|                     } else if (modifiedIndex > selectedPositions[1]) { | ||||
|                         selectedPositions[1] = modifiedIndex | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun toggleAllSelection(selected: Boolean) { | ||||
|         state.uiModels = state.uiModels.map { | ||||
|             when (it) { | ||||
|                 is UpdatesUiModel.Header -> it | ||||
|                 is UpdatesUiModel.Item -> { | ||||
|                     val newItem = it.item.copy(selected = selected) | ||||
|                     UpdatesUiModel.Item(newItem) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         selectedPositions[0] = -1 | ||||
|         selectedPositions[1] = -1 | ||||
|     } | ||||
|  | ||||
|     fun invertSelection() { | ||||
|         state.uiModels = state.uiModels.map { | ||||
|             when (it) { | ||||
|                 is UpdatesUiModel.Header -> it | ||||
|                 is UpdatesUiModel.Item -> { | ||||
|                     val newItem = it.item.let { item -> item.copy(selected = !item.selected) } | ||||
|                     UpdatesUiModel.Item(newItem) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         selectedPositions[0] = -1 | ||||
|         selectedPositions[1] = -1 | ||||
|     } | ||||
|  | ||||
|     sealed class Dialog { | ||||
|         data class DeleteConfirmation(val toDelete: List<UpdatesItem>) : Dialog() | ||||
|     } | ||||
|  | ||||
|     sealed class Event { | ||||
|         object InternalError : Event() | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Immutable | ||||
| @@ -318,4 +380,5 @@ data class UpdatesItem( | ||||
|     val update: UpdatesWithRelations, | ||||
|     val downloadStateProvider: () -> Download.State, | ||||
|     val downloadProgressProvider: () -> Int, | ||||
|     val selected: Boolean = false, | ||||
| ) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user