mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 14:27:57 +01:00 
			
		
		
		
	Migrate History screen to Compose (#6922)
* Migrate History screen to Compose - Migrate screen - Strip logic from presenter into use cases and repository - Setup for other screen being able to migrate to Compose with Theme * Changes from review comments
This commit is contained in:
		| @@ -0,0 +1,43 @@ | ||||
| package eu.kanade.data.history.local | ||||
|  | ||||
| import androidx.paging.PagingSource | ||||
| import androidx.paging.PagingState | ||||
| import eu.kanade.domain.history.repository.HistoryRepository | ||||
| import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory | ||||
| import logcat.logcat | ||||
|  | ||||
| class HistoryPagingSource( | ||||
|     private val repository: HistoryRepository, | ||||
|     private val query: String | ||||
| ) : PagingSource<Int, MangaChapterHistory>() { | ||||
|  | ||||
|     override fun getRefreshKey(state: PagingState<Int, MangaChapterHistory>): Int? { | ||||
|         return state.anchorPosition?.let { anchorPosition -> | ||||
|             val anchorPage = state.closestPageToPosition(anchorPosition) | ||||
|             anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override suspend fun load(params: LoadParams<Int>): LoadResult.Page<Int, MangaChapterHistory> { | ||||
|         val nextPageNumber = params.key ?: 0 | ||||
|         logcat { "Loading page $nextPageNumber" } | ||||
|  | ||||
|         val response = repository.getHistory(PAGE_SIZE, nextPageNumber, query) | ||||
|  | ||||
|         val nextKey = if (response.size == 25) { | ||||
|             nextPageNumber + 1 | ||||
|         } else { | ||||
|             null | ||||
|         } | ||||
|  | ||||
|         return LoadResult.Page( | ||||
|             data = response, | ||||
|             prevKey = null, | ||||
|             nextKey = nextKey | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     companion object { | ||||
|         const val PAGE_SIZE = 25 | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,137 @@ | ||||
| package eu.kanade.data.history.repository | ||||
|  | ||||
| import eu.kanade.data.history.local.HistoryPagingSource | ||||
| import eu.kanade.domain.history.repository.HistoryRepository | ||||
| import eu.kanade.tachiyomi.data.database.DatabaseHelper | ||||
| import eu.kanade.tachiyomi.data.database.models.Chapter | ||||
| import eu.kanade.tachiyomi.data.database.models.History | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.data.database.tables.HistoryTable | ||||
| import eu.kanade.tachiyomi.util.system.logcat | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.coroutineScope | ||||
| import kotlinx.coroutines.withContext | ||||
| import rx.Subscription | ||||
| import rx.schedulers.Schedulers | ||||
| import java.util.* | ||||
|  | ||||
| class HistoryRepositoryImpl( | ||||
|     private val db: DatabaseHelper | ||||
| ) : HistoryRepository { | ||||
|  | ||||
|     /** | ||||
|      * Used to observe changes in the History table | ||||
|      * as RxJava isn't supported in Paging 3 | ||||
|      */ | ||||
|     private var subscription: Subscription? = null | ||||
|  | ||||
|     /** | ||||
|      * Paging Source for history table | ||||
|      */ | ||||
|     override fun getHistory(query: String): HistoryPagingSource { | ||||
|         subscription?.unsubscribe() | ||||
|         val pagingSource = HistoryPagingSource(this, query) | ||||
|         subscription = db.db | ||||
|             .observeChangesInTable(HistoryTable.TABLE) | ||||
|             .observeOn(Schedulers.io()) | ||||
|             .subscribe { | ||||
|                 pagingSource.invalidate() | ||||
|             } | ||||
|         return pagingSource | ||||
|     } | ||||
|  | ||||
|     override suspend fun getHistory(limit: Int, page: Int, query: String) = coroutineScope { | ||||
|         withContext(Dispatchers.IO) { | ||||
|             // Set date limit for recent manga | ||||
|             val calendar = Calendar.getInstance().apply { | ||||
|                 time = Date() | ||||
|                 add(Calendar.YEAR, -50) | ||||
|             } | ||||
|  | ||||
|             db.getRecentManga(calendar.time, limit, page * limit, query) | ||||
|                 .executeAsBlocking() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override suspend fun getNextChapterForManga(manga: Manga, chapter: Chapter): Chapter? = coroutineScope { | ||||
|         withContext(Dispatchers.IO) { | ||||
|             if (!chapter.read) { | ||||
|                 return@withContext chapter | ||||
|             } | ||||
|  | ||||
|             val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) { | ||||
|                 Manga.CHAPTER_SORTING_SOURCE -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) } | ||||
|                 Manga.CHAPTER_SORTING_NUMBER -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) } | ||||
|                 Manga.CHAPTER_SORTING_UPLOAD_DATE -> { c1, c2 -> c1.date_upload.compareTo(c2.date_upload) } | ||||
|                 else -> throw NotImplementedError("Unknown sorting method") | ||||
|             } | ||||
|  | ||||
|             val chapters = db.getChapters(manga) | ||||
|                 .executeAsBlocking() | ||||
|                 .sortedWith { c1, c2 -> sortFunction(c1, c2) } | ||||
|  | ||||
|             val currChapterIndex = chapters.indexOfFirst { chapter.id == it.id } | ||||
|             return@withContext when (manga.sorting) { | ||||
|                 Manga.CHAPTER_SORTING_SOURCE -> chapters.getOrNull(currChapterIndex + 1) | ||||
|                 Manga.CHAPTER_SORTING_NUMBER -> { | ||||
|                     val chapterNumber = chapter.chapter_number | ||||
|  | ||||
|                     ((currChapterIndex + 1) until chapters.size) | ||||
|                         .map { chapters[it] } | ||||
|                         .firstOrNull { | ||||
|                             it.chapter_number > chapterNumber && | ||||
|                                 it.chapter_number <= chapterNumber + 1 | ||||
|                         } | ||||
|                 } | ||||
|                 Manga.CHAPTER_SORTING_UPLOAD_DATE -> { | ||||
|                     chapters.drop(currChapterIndex + 1) | ||||
|                         .firstOrNull { it.date_upload >= chapter.date_upload } | ||||
|                 } | ||||
|                 else -> throw NotImplementedError("Unknown sorting method") | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override suspend fun resetHistory(history: History): Boolean = coroutineScope { | ||||
|         withContext(Dispatchers.IO) { | ||||
|             try { | ||||
|                 history.last_read = 0 | ||||
|                 db.upsertHistoryLastRead(history) | ||||
|                     .executeAsBlocking() | ||||
|                 true | ||||
|             } catch (e: Throwable) { | ||||
|                 logcat(throwable = e) | ||||
|                 false | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override suspend fun resetHistoryByMangaId(mangaId: Long): Boolean = coroutineScope { | ||||
|         withContext(Dispatchers.IO) { | ||||
|             try { | ||||
|                 val history = db.getHistoryByMangaId(mangaId) | ||||
|                     .executeAsBlocking() | ||||
|                 history.forEach { it.last_read = 0 } | ||||
|                 db.upsertHistoryLastRead(history) | ||||
|                     .executeAsBlocking() | ||||
|                 true | ||||
|             } catch (e: Throwable) { | ||||
|                 logcat(throwable = e) | ||||
|                 false | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override suspend fun deleteAllHistory(): Boolean = coroutineScope { | ||||
|         withContext(Dispatchers.IO) { | ||||
|             try { | ||||
|                 db.dropHistoryTable() | ||||
|                     .executeAsBlocking() | ||||
|                 true | ||||
|             } catch (e: Throwable) { | ||||
|                 logcat(throwable = e) | ||||
|                 false | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										26
									
								
								app/src/main/java/eu/kanade/domain/DomainModule.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								app/src/main/java/eu/kanade/domain/DomainModule.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| package eu.kanade.domain | ||||
|  | ||||
| import eu.kanade.data.history.repository.HistoryRepositoryImpl | ||||
| import eu.kanade.domain.history.interactor.DeleteHistoryTable | ||||
| import eu.kanade.domain.history.interactor.GetHistory | ||||
| import eu.kanade.domain.history.interactor.GetNextChapterForManga | ||||
| import eu.kanade.domain.history.interactor.RemoveHistoryById | ||||
| import eu.kanade.domain.history.interactor.RemoveHistoryByMangaId | ||||
| import eu.kanade.domain.history.repository.HistoryRepository | ||||
| import uy.kohesive.injekt.api.InjektModule | ||||
| import uy.kohesive.injekt.api.InjektRegistrar | ||||
| import uy.kohesive.injekt.api.addFactory | ||||
| import uy.kohesive.injekt.api.addSingletonFactory | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| class DomainModule : InjektModule { | ||||
|  | ||||
|     override fun InjektRegistrar.registerInjectables() { | ||||
|         addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) } | ||||
|         addFactory { DeleteHistoryTable(get()) } | ||||
|         addFactory { GetHistory(get()) } | ||||
|         addFactory { GetNextChapterForManga(get()) } | ||||
|         addFactory { RemoveHistoryById(get()) } | ||||
|         addFactory { RemoveHistoryByMangaId(get()) } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,12 @@ | ||||
| package eu.kanade.domain.history.interactor | ||||
|  | ||||
| import eu.kanade.domain.history.repository.HistoryRepository | ||||
|  | ||||
| class DeleteHistoryTable( | ||||
|     private val repository: HistoryRepository | ||||
| ) { | ||||
|  | ||||
|     suspend fun await(): Boolean { | ||||
|         return repository.deleteAllHistory() | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,22 @@ | ||||
| package eu.kanade.domain.history.interactor | ||||
|  | ||||
| import androidx.paging.Pager | ||||
| import androidx.paging.PagingConfig | ||||
| import androidx.paging.PagingData | ||||
| import eu.kanade.data.history.local.HistoryPagingSource | ||||
| import eu.kanade.domain.history.repository.HistoryRepository | ||||
| import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory | ||||
| import kotlinx.coroutines.flow.Flow | ||||
|  | ||||
| class GetHistory( | ||||
|     private val repository: HistoryRepository | ||||
| ) { | ||||
|  | ||||
|     fun subscribe(query: String): Flow<PagingData<MangaChapterHistory>> { | ||||
|         return Pager( | ||||
|             PagingConfig(pageSize = HistoryPagingSource.PAGE_SIZE) | ||||
|         ) { | ||||
|             repository.getHistory(query) | ||||
|         }.flow | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,14 @@ | ||||
| package eu.kanade.domain.history.interactor | ||||
|  | ||||
| import eu.kanade.domain.history.repository.HistoryRepository | ||||
| import eu.kanade.tachiyomi.data.database.models.Chapter | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
|  | ||||
| class GetNextChapterForManga( | ||||
|     private val repository: HistoryRepository | ||||
| ) { | ||||
|  | ||||
|     suspend fun await(manga: Manga, chapter: Chapter): Chapter? { | ||||
|         return repository.getNextChapterForManga(manga, chapter) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,21 @@ | ||||
| package eu.kanade.domain.history.interactor | ||||
|  | ||||
| import eu.kanade.domain.history.repository.HistoryRepository | ||||
| import eu.kanade.tachiyomi.data.database.models.History | ||||
| import eu.kanade.tachiyomi.data.database.models.HistoryImpl | ||||
|  | ||||
| class RemoveHistoryById( | ||||
|     private val repository: HistoryRepository | ||||
| ) { | ||||
|  | ||||
|     suspend fun await(history: History): Boolean { | ||||
|         // Workaround for list not freaking out when changing reference varaible | ||||
|         val history = HistoryImpl().apply { | ||||
|             id = history.id | ||||
|             chapter_id = history.chapter_id | ||||
|             last_read = history.last_read | ||||
|             time_read = history.time_read | ||||
|         } | ||||
|         return repository.resetHistory(history) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,12 @@ | ||||
| package eu.kanade.domain.history.interactor | ||||
|  | ||||
| import eu.kanade.domain.history.repository.HistoryRepository | ||||
|  | ||||
| class RemoveHistoryByMangaId( | ||||
|     private val repository: HistoryRepository | ||||
| ) { | ||||
|  | ||||
|     suspend fun await(mangaId: Long): Boolean { | ||||
|         return repository.resetHistoryByMangaId(mangaId) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,22 @@ | ||||
| package eu.kanade.domain.history.repository | ||||
|  | ||||
| import eu.kanade.data.history.local.HistoryPagingSource | ||||
| import eu.kanade.tachiyomi.data.database.models.Chapter | ||||
| import eu.kanade.tachiyomi.data.database.models.History | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory | ||||
|  | ||||
| interface HistoryRepository { | ||||
|  | ||||
|     fun getHistory(query: String): HistoryPagingSource | ||||
|  | ||||
|     suspend fun getHistory(limit: Int, page: Int, query: String): List<MangaChapterHistory> | ||||
|  | ||||
|     suspend fun getNextChapterForManga(manga: Manga, chapter: Chapter): Chapter? | ||||
|  | ||||
|     suspend fun resetHistory(history: History): Boolean | ||||
|  | ||||
|     suspend fun resetHistoryByMangaId(mangaId: Long): Boolean | ||||
|  | ||||
|     suspend fun deleteAllHistory(): Boolean | ||||
| } | ||||
| @@ -0,0 +1,49 @@ | ||||
| package eu.kanade.presentation.components | ||||
|  | ||||
| import android.view.ViewGroup | ||||
| import androidx.annotation.StringRes | ||||
| import androidx.compose.foundation.layout.Box | ||||
| import androidx.compose.foundation.layout.fillMaxSize | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import androidx.compose.ui.viewinterop.AndroidView | ||||
| import eu.kanade.tachiyomi.widget.EmptyView | ||||
|  | ||||
| @Composable | ||||
| fun EmptyScreen( | ||||
|     @StringRes textResource: Int, | ||||
|     actions: List<EmptyView.Action>? = null, | ||||
| ) { | ||||
|     EmptyScreen( | ||||
|         message = stringResource(id = textResource), | ||||
|         actions = actions, | ||||
|     ) | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| fun EmptyScreen( | ||||
|     message: String, | ||||
|     actions: List<EmptyView.Action>? = null, | ||||
| ) { | ||||
|     Box( | ||||
|         modifier = Modifier | ||||
|             .fillMaxSize() | ||||
|     ) { | ||||
|         AndroidView( | ||||
|             factory = { context -> | ||||
|                 EmptyView(context).apply { | ||||
|                     layoutParams = ViewGroup.LayoutParams( | ||||
|                         ViewGroup.LayoutParams.WRAP_CONTENT, | ||||
|                         ViewGroup.LayoutParams.WRAP_CONTENT, | ||||
|                     ) | ||||
|                 } | ||||
|             }, | ||||
|             modifier = Modifier | ||||
|                 .align(Alignment.Center), | ||||
|         ) { view -> | ||||
|             view.show(message, actions) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,35 @@ | ||||
| package eu.kanade.presentation.components | ||||
|  | ||||
| import androidx.compose.foundation.layout.aspectRatio | ||||
| import androidx.compose.foundation.shape.RoundedCornerShape | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.draw.clip | ||||
| import androidx.compose.ui.graphics.Shape | ||||
| import androidx.compose.ui.layout.ContentScale | ||||
| import androidx.compose.ui.unit.dp | ||||
| import coil.compose.AsyncImage | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
|  | ||||
| enum class MangaCoverAspect(val ratio: Float) { | ||||
|     SQUARE(1f / 1f), | ||||
|     COVER(2f / 3f) | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| fun MangaCover( | ||||
|     modifier: Modifier = Modifier, | ||||
|     manga: Manga, | ||||
|     aspect: MangaCoverAspect, | ||||
|     contentDescription: String = "", | ||||
|     shape: Shape = RoundedCornerShape(4.dp) | ||||
| ) { | ||||
|     AsyncImage( | ||||
|         model = manga, | ||||
|         contentDescription = contentDescription, | ||||
|         modifier = modifier | ||||
|             .aspectRatio(aspect.ratio) | ||||
|             .clip(shape), | ||||
|         contentScale = ContentScale.Crop | ||||
|     ) | ||||
| } | ||||
| @@ -0,0 +1,298 @@ | ||||
| package eu.kanade.presentation.history | ||||
|  | ||||
| import androidx.compose.foundation.clickable | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.Row | ||||
| import androidx.compose.foundation.layout.Spacer | ||||
| import androidx.compose.foundation.layout.fillMaxHeight | ||||
| import androidx.compose.foundation.layout.height | ||||
| import androidx.compose.foundation.layout.navigationBarsPadding | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.foundation.lazy.rememberLazyListState | ||||
| import androidx.compose.foundation.selection.toggleable | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.filled.PlayArrow | ||||
| import androidx.compose.material.icons.outlined.Delete | ||||
| import androidx.compose.material3.AlertDialog | ||||
| import androidx.compose.material3.Checkbox | ||||
| import androidx.compose.material3.CircularProgressIndicator | ||||
| import androidx.compose.material3.Icon | ||||
| import androidx.compose.material3.IconButton | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.material3.TextButton | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.collectAsState | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.input.nestedscroll.NestedScrollConnection | ||||
| import androidx.compose.ui.input.nestedscroll.nestedScroll | ||||
| import androidx.compose.ui.platform.ComposeView | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import androidx.compose.ui.platform.rememberNestedScrollInteropConnection | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import androidx.compose.ui.text.font.FontWeight | ||||
| import androidx.compose.ui.text.style.TextOverflow | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.core.text.buildSpannedString | ||||
| import androidx.paging.compose.LazyPagingItems | ||||
| import androidx.paging.compose.collectAsLazyPagingItems | ||||
| import androidx.paging.compose.items | ||||
| import eu.kanade.presentation.components.EmptyScreen | ||||
| import eu.kanade.presentation.components.MangaCover | ||||
| import eu.kanade.presentation.components.MangaCoverAspect | ||||
| import eu.kanade.presentation.theme.TachiyomiTheme | ||||
| import eu.kanade.presentation.util.horizontalPadding | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.ui.recent.history.HistoryPresenter | ||||
| import eu.kanade.tachiyomi.ui.recent.history.UiModel | ||||
| import eu.kanade.tachiyomi.util.lang.toRelativeString | ||||
| import eu.kanade.tachiyomi.util.lang.toTimestampString | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.text.DateFormat | ||||
| import java.text.DecimalFormat | ||||
| import java.text.DecimalFormatSymbols | ||||
| import java.util.* | ||||
|  | ||||
| val chapterFormatter = DecimalFormat( | ||||
|     "#.###", | ||||
|     DecimalFormatSymbols() | ||||
|         .apply { decimalSeparator = '.' }, | ||||
| ) | ||||
|  | ||||
| @Composable | ||||
| fun HistoryScreen( | ||||
|     composeView: ComposeView, | ||||
|     presenter: HistoryPresenter, | ||||
|     onClickItem: (MangaChapterHistory) -> Unit, | ||||
|     onClickResume: (MangaChapterHistory) -> Unit, | ||||
|     onClickDelete: (MangaChapterHistory, Boolean) -> Unit, | ||||
| ) { | ||||
|     val nestedSrollInterop = rememberNestedScrollInteropConnection(composeView) | ||||
|     TachiyomiTheme { | ||||
|         val state by presenter.state.collectAsState() | ||||
|         val history = state.list?.collectAsLazyPagingItems() | ||||
|         when { | ||||
|             history == null -> { | ||||
|                 CircularProgressIndicator() | ||||
|             } | ||||
|             history.itemCount == 0 -> { | ||||
|                 EmptyScreen( | ||||
|                     textResource = R.string.information_no_recent_manga | ||||
|                 ) | ||||
|             } | ||||
|             else -> { | ||||
|                 HistoryContent( | ||||
|                     nestedScroll = nestedSrollInterop, | ||||
|                     history = history, | ||||
|                     onClickItem = onClickItem, | ||||
|                     onClickResume = onClickResume, | ||||
|                     onClickDelete = onClickDelete, | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| fun HistoryContent( | ||||
|     history: LazyPagingItems<UiModel>, | ||||
|     onClickItem: (MangaChapterHistory) -> Unit, | ||||
|     onClickResume: (MangaChapterHistory) -> Unit, | ||||
|     onClickDelete: (MangaChapterHistory, Boolean) -> Unit, | ||||
|     preferences: PreferencesHelper = Injekt.get(), | ||||
|     nestedScroll: NestedScrollConnection | ||||
| ) { | ||||
|     val relativeTime: Int = remember { preferences.relativeTime().get() } | ||||
|     val dateFormat: DateFormat = remember { preferences.dateFormat() } | ||||
|  | ||||
|     val (removeState, setRemoveState) = remember { mutableStateOf<MangaChapterHistory?>(null) } | ||||
|  | ||||
|     val scrollState = rememberLazyListState() | ||||
|     LazyColumn( | ||||
|         modifier = Modifier | ||||
|             .nestedScroll(nestedScroll), | ||||
|         state = scrollState, | ||||
|     ) { | ||||
|         items(history) { item -> | ||||
|             when (item) { | ||||
|                 is UiModel.Header -> { | ||||
|                     HistoryHeader( | ||||
|                         modifier = Modifier | ||||
|                             .animateItemPlacement(), | ||||
|                         date = item.date, | ||||
|                         relativeTime = relativeTime, | ||||
|                         dateFormat = dateFormat | ||||
|                     ) | ||||
|                 } | ||||
|                 is UiModel.History -> { | ||||
|                     val value = item.item | ||||
|                     HistoryItem( | ||||
|                         modifier = Modifier.animateItemPlacement(), | ||||
|                         history = value, | ||||
|                         onClickItem = { onClickItem(value) }, | ||||
|                         onClickResume = { onClickResume(value) }, | ||||
|                         onClickDelete = { setRemoveState(value) }, | ||||
|                     ) | ||||
|                 } | ||||
|                 null -> {} | ||||
|             } | ||||
|         } | ||||
|         item { | ||||
|             Spacer( | ||||
|                 modifier = Modifier | ||||
|                     .navigationBarsPadding() | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     if (removeState != null) { | ||||
|         RemoveHistoryDialog( | ||||
|             onPositive = { all -> | ||||
|                 onClickDelete(removeState, all) | ||||
|                 setRemoveState(null) | ||||
|             }, | ||||
|             onNegative = { setRemoveState(null) } | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| fun HistoryHeader( | ||||
|     modifier: Modifier = Modifier, | ||||
|     date: Date, | ||||
|     relativeTime: Int, | ||||
|     dateFormat: DateFormat, | ||||
| ) { | ||||
|     Text( | ||||
|         modifier = modifier | ||||
|             .padding(horizontal = horizontalPadding, vertical = 8.dp), | ||||
|         text = date.toRelativeString( | ||||
|             LocalContext.current, | ||||
|             relativeTime, | ||||
|             dateFormat | ||||
|         ), | ||||
|         style = MaterialTheme.typography.bodyMedium.copy( | ||||
|             color = MaterialTheme.colorScheme.onSurfaceVariant, | ||||
|             fontWeight = FontWeight.SemiBold, | ||||
|         ) | ||||
|     ) | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| fun HistoryItem( | ||||
|     modifier: Modifier = Modifier, | ||||
|     history: MangaChapterHistory, | ||||
|     onClickItem: () -> Unit, | ||||
|     onClickResume: () -> Unit, | ||||
|     onClickDelete: () -> Unit, | ||||
| ) { | ||||
|     Row( | ||||
|         modifier = modifier | ||||
|             .clickable(onClick = onClickItem) | ||||
|             .height(96.dp) | ||||
|             .padding(horizontal = horizontalPadding, vertical = 8.dp), | ||||
|         verticalAlignment = Alignment.CenterVertically, | ||||
|     ) { | ||||
|         MangaCover( | ||||
|             modifier = Modifier.fillMaxHeight(), | ||||
|             manga = history.manga, | ||||
|             aspect = MangaCoverAspect.COVER | ||||
|         ) | ||||
|         Column( | ||||
|             modifier = Modifier | ||||
|                 .weight(1f) | ||||
|                 .padding(start = horizontalPadding, end = 8.dp), | ||||
|         ) { | ||||
|             val textStyle = MaterialTheme.typography.bodyMedium.copy( | ||||
|                 color = MaterialTheme.colorScheme.onSurface, | ||||
|             ) | ||||
|             Text( | ||||
|                 text = history.manga.title, | ||||
|                 maxLines = 2, | ||||
|                 overflow = TextOverflow.Ellipsis, | ||||
|                 style = textStyle.copy(fontWeight = FontWeight.SemiBold) | ||||
|             ) | ||||
|             Row { | ||||
|                 Text( | ||||
|                     text = buildSpannedString { | ||||
|                         if (history.chapter.chapter_number > -1) { | ||||
|                             append( | ||||
|                                 stringResource( | ||||
|                                     R.string.history_prefix, | ||||
|                                     chapterFormatter.format(history.chapter.chapter_number) | ||||
|                                 ) | ||||
|                             ) | ||||
|                         } | ||||
|                         append(Date(history.history.last_read).toTimestampString()) | ||||
|                     }.toString(), | ||||
|                     modifier = Modifier.padding(top = 2.dp), | ||||
|                     style = textStyle | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|         IconButton(onClick = onClickDelete) { | ||||
|             Icon( | ||||
|                 imageVector = Icons.Outlined.Delete, | ||||
|                 contentDescription = stringResource(id = R.string.action_delete), | ||||
|                 tint = MaterialTheme.colorScheme.onSurface, | ||||
|             ) | ||||
|         } | ||||
|         IconButton(onClick = onClickResume) { | ||||
|             Icon( | ||||
|                 imageVector = Icons.Filled.PlayArrow, | ||||
|                 contentDescription = stringResource(id = R.string.action_resume), | ||||
|                 tint = MaterialTheme.colorScheme.onSurface, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| fun RemoveHistoryDialog( | ||||
|     onPositive: (Boolean) -> Unit, | ||||
|     onNegative: () -> Unit | ||||
| ) { | ||||
|     val (removeEverything, removeEverythingState) = remember { mutableStateOf(false) } | ||||
|  | ||||
|     AlertDialog( | ||||
|         title = { | ||||
|             Text(text = stringResource(id = R.string.action_remove)) | ||||
|         }, | ||||
|         text = { | ||||
|             Column { | ||||
|                 Text(text = stringResource(id = R.string.dialog_with_checkbox_remove_description)) | ||||
|                 Row( | ||||
|                     modifier = Modifier.toggleable(value = removeEverything, onValueChange = removeEverythingState), | ||||
|                     verticalAlignment = Alignment.CenterVertically | ||||
|                 ) { | ||||
|                     Checkbox( | ||||
|                         checked = removeEverything, | ||||
|                         onCheckedChange = removeEverythingState, | ||||
|                     ) | ||||
|                     Text( | ||||
|                         text = stringResource(id = R.string.dialog_with_checkbox_reset) | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         onDismissRequest = onNegative, | ||||
|         confirmButton = { | ||||
|             TextButton(onClick = { onPositive(removeEverything) }) { | ||||
|                 Text(text = stringResource(id = R.string.action_remove)) | ||||
|             } | ||||
|         }, | ||||
|         dismissButton = { | ||||
|             TextButton(onClick = onNegative) { | ||||
|                 Text(text = stringResource(id = R.string.action_cancel)) | ||||
|             } | ||||
|         }, | ||||
|     ) | ||||
| } | ||||
| @@ -0,0 +1,20 @@ | ||||
| package eu.kanade.presentation.theme | ||||
|  | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import com.google.android.material.composethemeadapter3.createMdc3Theme | ||||
|  | ||||
| @Composable | ||||
| fun TachiyomiTheme(content: @Composable () -> Unit) { | ||||
|     val context = LocalContext.current | ||||
|     var (colorScheme, typography) = createMdc3Theme( | ||||
|         context = context | ||||
|     ) | ||||
|  | ||||
|     MaterialTheme( | ||||
|         colorScheme = colorScheme!!, | ||||
|         typography = typography!!, | ||||
|         content = content | ||||
|     ) | ||||
| } | ||||
| @@ -0,0 +1,5 @@ | ||||
| package eu.kanade.presentation.util | ||||
|  | ||||
| import androidx.compose.ui.unit.dp | ||||
|  | ||||
| val horizontalPadding = 16.dp | ||||
| @@ -0,0 +1,5 @@ | ||||
| package eu.kanade.presentation.util | ||||
|  | ||||
| import androidx.compose.foundation.lazy.LazyListState | ||||
|  | ||||
| fun LazyListState.isScrolledToEnd() = layoutInfo.visibleItemsInfo.lastOrNull()?.index == layoutInfo.totalItemsCount - 1 | ||||
| @@ -24,6 +24,7 @@ import coil.decode.GifDecoder | ||||
| import coil.decode.ImageDecoderDecoder | ||||
| import coil.disk.DiskCache | ||||
| import coil.util.DebugLogger | ||||
| import eu.kanade.domain.DomainModule | ||||
| import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher | ||||
| import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer | ||||
| import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder | ||||
| @@ -74,6 +75,7 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory { | ||||
|         } | ||||
|  | ||||
|         Injekt.importModule(AppModule(this)) | ||||
|         Injekt.importModule(DomainModule()) | ||||
|  | ||||
|         setupAcra() | ||||
|         setupNotificationChannels() | ||||
|   | ||||
| @@ -294,7 +294,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         databaseHelper.updateHistoryLastRead(historyToBeUpdated).executeAsBlocking() | ||||
|         databaseHelper.upsertHistoryLastRead(historyToBeUpdated).executeAsBlocking() | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -168,7 +168,7 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         databaseHelper.updateHistoryLastRead(historyToBeUpdated).executeAsBlocking() | ||||
|         databaseHelper.upsertHistoryLastRead(historyToBeUpdated).executeAsBlocking() | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -5,7 +5,7 @@ import com.pushtorefresh.storio.sqlite.queries.RawQuery | ||||
| import eu.kanade.tachiyomi.data.database.DbProvider | ||||
| import eu.kanade.tachiyomi.data.database.models.History | ||||
| import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory | ||||
| import eu.kanade.tachiyomi.data.database.resolvers.HistoryLastReadPutResolver | ||||
| import eu.kanade.tachiyomi.data.database.resolvers.HistoryUpsertResolver | ||||
| import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterHistoryGetResolver | ||||
| import eu.kanade.tachiyomi.data.database.tables.HistoryTable | ||||
| import java.util.Date | ||||
| @@ -64,9 +64,9 @@ interface HistoryQueries : DbProvider { | ||||
|      * Inserts history object if not yet in database | ||||
|      * @param history history object | ||||
|      */ | ||||
|     fun updateHistoryLastRead(history: History) = db.put() | ||||
|     fun upsertHistoryLastRead(history: History) = db.put() | ||||
|         .`object`(history) | ||||
|         .withPutResolver(HistoryLastReadPutResolver()) | ||||
|         .withPutResolver(HistoryUpsertResolver()) | ||||
|         .prepare() | ||||
|  | ||||
|     /** | ||||
| @@ -74,12 +74,40 @@ interface HistoryQueries : DbProvider { | ||||
|      * Inserts history object if not yet in database | ||||
|      * @param historyList history object list | ||||
|      */ | ||||
|     fun updateHistoryLastRead(historyList: List<History>) = db.put() | ||||
|     fun upsertHistoryLastRead(historyList: List<History>) = db.put() | ||||
|         .objects(historyList) | ||||
|         .withPutResolver(HistoryLastReadPutResolver()) | ||||
|         .withPutResolver(HistoryUpsertResolver()) | ||||
|         .prepare() | ||||
|  | ||||
|     fun deleteHistory() = db.delete() | ||||
|     fun resetHistoryLastRead(historyId: Long) = db.executeSQL() | ||||
|         .withQuery( | ||||
|             RawQuery.builder() | ||||
|                 .query( | ||||
|                     """ | ||||
|                 UPDATE ${HistoryTable.TABLE}  | ||||
|                 SET history_last_read = 0 | ||||
|                 WHERE ${HistoryTable.COL_ID} = $historyId   | ||||
|                     """.trimIndent() | ||||
|                 ) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun resetHistoryLastRead(historyIds: List<Long>) = db.executeSQL() | ||||
|         .withQuery( | ||||
|             RawQuery.builder() | ||||
|                 .query( | ||||
|                     """ | ||||
|                 UPDATE ${HistoryTable.TABLE}  | ||||
|                 SET history_last_read = 0 | ||||
|                 WHERE ${HistoryTable.COL_ID} in ${historyIds.joinToString(",", "(", ")")}   | ||||
|                     """.trimIndent() | ||||
|                 ) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun dropHistoryTable() = db.delete() | ||||
|         .byQuery( | ||||
|             DeleteQuery.builder() | ||||
|                 .table(HistoryTable.TABLE) | ||||
|   | ||||
| @@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.data.database.mappers.HistoryPutResolver | ||||
| import eu.kanade.tachiyomi.data.database.models.History | ||||
| import eu.kanade.tachiyomi.data.database.tables.HistoryTable | ||||
| 
 | ||||
| class HistoryLastReadPutResolver : HistoryPutResolver() { | ||||
| class HistoryUpsertResolver : HistoryPutResolver() { | ||||
| 
 | ||||
|     /** | ||||
|      * Updates last_read time of chapter | ||||
| @@ -97,14 +97,19 @@ import kotlin.math.max | ||||
| class ReaderActivity : BaseRxActivity<ReaderPresenter>() { | ||||
|  | ||||
|     companion object { | ||||
|         fun newIntent(context: Context, manga: Manga, chapter: Chapter): Intent { | ||||
|  | ||||
|         fun newIntent(context: Context, mangaId: Long?, chapterId: Long?): Intent { | ||||
|             return Intent(context, ReaderActivity::class.java).apply { | ||||
|                 putExtra("manga", manga.id) | ||||
|                 putExtra("chapter", chapter.id) | ||||
|                 putExtra("manga", mangaId) | ||||
|                 putExtra("chapter", chapterId) | ||||
|                 addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         fun newIntent(context: Context, manga: Manga, chapter: Chapter): Intent { | ||||
|             return newIntent(context, manga.id, chapter.id) | ||||
|         } | ||||
|  | ||||
|         private const val ENABLED_BUTTON_IMAGE_ALPHA = 255 | ||||
|         private const val DISABLED_BUTTON_IMAGE_ALPHA = 64 | ||||
|  | ||||
|   | ||||
| @@ -449,7 +449,7 @@ class ReaderPresenter( | ||||
|     private fun saveChapterHistory(chapter: ReaderChapter) { | ||||
|         if (!incognitoMode) { | ||||
|             val history = History.create(chapter.chapter).apply { last_read = Date().time } | ||||
|             db.updateHistoryLastRead(history).asRxCompletable() | ||||
|             db.upsertHistoryLastRead(history).asRxCompletable() | ||||
|                 .onErrorComplete() | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .subscribe() | ||||
|   | ||||
| @@ -0,0 +1,21 @@ | ||||
| package eu.kanade.tachiyomi.ui.recent.history | ||||
|  | ||||
| import android.app.Dialog | ||||
| import android.os.Bundle | ||||
| import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.ui.base.controller.DialogController | ||||
|  | ||||
| class ClearHistoryDialogController : DialogController() { | ||||
|     override fun onCreateDialog(savedViewState: Bundle?): Dialog { | ||||
|         return MaterialAlertDialogBuilder(activity!!) | ||||
|             .setMessage(R.string.clear_history_confirmation) | ||||
|             .setPositiveButton(android.R.string.ok) { _, _ -> | ||||
|                 (targetController as? HistoryController) | ||||
|                     ?.presenter | ||||
|                     ?.deleteAllHistory() | ||||
|             } | ||||
|             .setNegativeButton(android.R.string.cancel, null) | ||||
|             .create() | ||||
|     } | ||||
| } | ||||
| @@ -1,51 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.ui.recent.history | ||||
|  | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.davidea.flexibleadapter.items.IFlexible | ||||
| import eu.kanade.tachiyomi.source.SourceManager | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.text.DecimalFormat | ||||
| import java.text.DecimalFormatSymbols | ||||
|  | ||||
| /** | ||||
|  * Adapter of HistoryHolder. | ||||
|  * Connection between Fragment and Holder | ||||
|  * Holder updates should be called from here. | ||||
|  * | ||||
|  * @param controller a HistoryController object | ||||
|  * @constructor creates an instance of the adapter. | ||||
|  */ | ||||
| class HistoryAdapter(controller: HistoryController) : | ||||
|     FlexibleAdapter<IFlexible<*>>(null, controller, true) { | ||||
|  | ||||
|     val sourceManager: SourceManager by injectLazy() | ||||
|  | ||||
|     val resumeClickListener: OnResumeClickListener = controller | ||||
|     val removeClickListener: OnRemoveClickListener = controller | ||||
|     val itemClickListener: OnItemClickListener = controller | ||||
|  | ||||
|     /** | ||||
|      * DecimalFormat used to display correct chapter number | ||||
|      */ | ||||
|     val decimalFormat = DecimalFormat( | ||||
|         "#.###", | ||||
|         DecimalFormatSymbols() | ||||
|             .apply { decimalSeparator = '.' }, | ||||
|     ) | ||||
|  | ||||
|     init { | ||||
|         setDisplayHeadersAtStartUp(true) | ||||
|     } | ||||
|  | ||||
|     interface OnResumeClickListener { | ||||
|         fun onResumeClick(position: Int) | ||||
|     } | ||||
|  | ||||
|     interface OnRemoveClickListener { | ||||
|         fun onRemoveClick(position: Int) | ||||
|     } | ||||
|  | ||||
|     interface OnItemClickListener { | ||||
|         fun onItemClick(position: Int) | ||||
|     } | ||||
| } | ||||
| @@ -1,192 +1,65 @@ | ||||
| package eu.kanade.tachiyomi.ui.recent.history | ||||
|  | ||||
| import android.app.Dialog | ||||
| import android.os.Bundle | ||||
| import android.view.LayoutInflater | ||||
| import android.view.Menu | ||||
| import android.view.MenuInflater | ||||
| import android.view.MenuItem | ||||
| import android.view.View | ||||
| import androidx.appcompat.widget.SearchView | ||||
| import androidx.recyclerview.widget.LinearLayoutManager | ||||
| import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||||
| import dev.chrisbanes.insetter.applyInsetter | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.kanade.presentation.history.HistoryScreen | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.backup.BackupRestoreService | ||||
| import eu.kanade.tachiyomi.data.database.DatabaseHelper | ||||
| import eu.kanade.tachiyomi.data.database.models.History | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.databinding.HistoryControllerBinding | ||||
| import eu.kanade.tachiyomi.ui.base.controller.DialogController | ||||
| import eu.kanade.tachiyomi.data.database.models.Chapter | ||||
| import eu.kanade.tachiyomi.databinding.ComposeControllerBinding | ||||
| import eu.kanade.tachiyomi.ui.base.controller.NucleusController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.RootController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction | ||||
| import eu.kanade.tachiyomi.ui.browse.source.browse.ProgressItem | ||||
| 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.logcat | ||||
| import eu.kanade.tachiyomi.util.system.toast | ||||
| import eu.kanade.tachiyomi.util.view.onAnimationsFinished | ||||
| import kotlinx.coroutines.flow.drop | ||||
| import kotlinx.coroutines.flow.filter | ||||
| import kotlinx.coroutines.flow.launchIn | ||||
| import kotlinx.coroutines.flow.onEach | ||||
| import logcat.LogPriority | ||||
| import reactivecircus.flowbinding.appcompat.queryTextChanges | ||||
| import uy.kohesive.injekt.injectLazy | ||||
|  | ||||
| /** | ||||
|  * Fragment that shows recently read manga. | ||||
|  */ | ||||
| class HistoryController : | ||||
|     NucleusController<HistoryControllerBinding, HistoryPresenter>(), | ||||
|     RootController, | ||||
|     FlexibleAdapter.OnUpdateListener, | ||||
|     FlexibleAdapter.EndlessScrollListener, | ||||
|     HistoryAdapter.OnRemoveClickListener, | ||||
|     HistoryAdapter.OnResumeClickListener, | ||||
|     HistoryAdapter.OnItemClickListener, | ||||
|     RemoveHistoryDialog.Listener { | ||||
|     NucleusController<ComposeControllerBinding, HistoryPresenter>(), | ||||
|     RootController { | ||||
|  | ||||
|     private val db: DatabaseHelper by injectLazy() | ||||
|  | ||||
|     /** | ||||
|      * Adapter containing the recent manga. | ||||
|      */ | ||||
|     var adapter: HistoryAdapter? = null | ||||
|         private set | ||||
|  | ||||
|     /** | ||||
|      * Endless loading item. | ||||
|      */ | ||||
|     private var progressItem: ProgressItem? = null | ||||
|  | ||||
|     /** | ||||
|      * Search query. | ||||
|      */ | ||||
|     private var query = "" | ||||
|  | ||||
|     override fun getTitle(): String? { | ||||
|         return resources?.getString(R.string.label_recent_manga) | ||||
|     } | ||||
|     override fun getTitle(): String? = resources?.getString(R.string.label_recent_manga) | ||||
|  | ||||
|     override fun createPresenter(): HistoryPresenter { | ||||
|         return HistoryPresenter() | ||||
|     } | ||||
|     override fun createPresenter(): HistoryPresenter = HistoryPresenter() | ||||
|  | ||||
|     override fun createBinding(inflater: LayoutInflater) = HistoryControllerBinding.inflate(inflater) | ||||
|     override fun createBinding(inflater: LayoutInflater): ComposeControllerBinding = | ||||
|         ComposeControllerBinding.inflate(inflater) | ||||
|  | ||||
|     override fun onViewCreated(view: View) { | ||||
|         super.onViewCreated(view) | ||||
|  | ||||
|         binding.recycler.applyInsetter { | ||||
|             type(navigationBars = true) { | ||||
|                 padding() | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Initialize adapter | ||||
|         binding.recycler.layoutManager = LinearLayoutManager(view.context) | ||||
|         adapter = HistoryAdapter(this@HistoryController) | ||||
|         binding.recycler.setHasFixedSize(true) | ||||
|         binding.recycler.adapter = adapter | ||||
|         adapter?.fastScroller = binding.fastScroller | ||||
|     } | ||||
|  | ||||
|     override fun onDestroyView(view: View) { | ||||
|         adapter = null | ||||
|         super.onDestroyView(view) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Populate adapter with chapters | ||||
|      * | ||||
|      * @param mangaHistory list of manga history | ||||
|      */ | ||||
|     fun onNextManga(mangaHistory: List<HistoryItem>, cleanBatch: Boolean = false) { | ||||
|         if (adapter?.itemCount ?: 0 == 0) { | ||||
|             resetProgressItem() | ||||
|         } | ||||
|         if (cleanBatch) { | ||||
|             adapter?.updateDataSet(mangaHistory) | ||||
|         } else { | ||||
|             adapter?.onLoadMoreComplete(mangaHistory) | ||||
|         } | ||||
|         binding.recycler.onAnimationsFinished { | ||||
|             (activity as? MainActivity)?.ready = true | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Safely error if next page load fails | ||||
|      */ | ||||
|     fun onAddPageError(error: Throwable) { | ||||
|         adapter?.onLoadMoreComplete(null) | ||||
|         adapter?.endlessTargetCount = 1 | ||||
|         logcat(LogPriority.ERROR, error) | ||||
|     } | ||||
|  | ||||
|     override fun onUpdateEmptyView(size: Int) { | ||||
|         if (size > 0) { | ||||
|             binding.emptyView.hide() | ||||
|         } else { | ||||
|             binding.emptyView.show(R.string.information_no_recent_manga) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Sets a new progress item and reenables the scroll listener. | ||||
|      */ | ||||
|     private fun resetProgressItem() { | ||||
|         progressItem = ProgressItem() | ||||
|         adapter?.endlessTargetCount = 0 | ||||
|         adapter?.setEndlessScrollListener(this, progressItem!!) | ||||
|     } | ||||
|  | ||||
|     override fun onLoadMore(lastPosition: Int, currentPage: Int) { | ||||
|         val view = view ?: return | ||||
|         if (BackupRestoreService.isRunning(view.context.applicationContext)) { | ||||
|             onAddPageError(Throwable()) | ||||
|             return | ||||
|         } | ||||
|         val adapter = adapter ?: return | ||||
|         presenter.requestNext(adapter.itemCount - adapter.headerItems.size, query) | ||||
|     } | ||||
|  | ||||
|     override fun noMoreLoad(newItemsSize: Int) {} | ||||
|  | ||||
|     override fun onResumeClick(position: Int) { | ||||
|         val activity = activity ?: return | ||||
|         val (manga, chapter, _) = (adapter?.getItem(position) as? HistoryItem)?.mch ?: return | ||||
|  | ||||
|         val nextChapter = presenter.getNextChapter(chapter, manga) | ||||
|         if (nextChapter != null) { | ||||
|             val intent = ReaderActivity.newIntent(activity, manga, nextChapter) | ||||
|             startActivity(intent) | ||||
|         } else { | ||||
|             activity.toast(R.string.no_next_chapter) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun onRemoveClick(position: Int) { | ||||
|         val (manga, _, history) = (adapter?.getItem(position) as? HistoryItem)?.mch ?: return | ||||
|         RemoveHistoryDialog(this, manga, history).showDialog(router) | ||||
|     } | ||||
|  | ||||
|     override fun onItemClick(position: Int) { | ||||
|         val manga = (adapter?.getItem(position) as? HistoryItem)?.mch?.manga ?: return | ||||
|         router.pushController(MangaController(manga).withFadeTransaction()) | ||||
|     } | ||||
|  | ||||
|     override fun removeHistory(manga: Manga, history: History, all: Boolean) { | ||||
|         if (all) { | ||||
|             // Reset last read of chapter to 0L | ||||
|             presenter.removeAllFromHistory(manga.id!!) | ||||
|         } else { | ||||
|             // Remove all chapters belonging to manga from library | ||||
|             presenter.removeFromHistory(history) | ||||
|         binding.root.setContent { | ||||
|             HistoryScreen( | ||||
|                 composeView = binding.root, | ||||
|                 presenter = presenter, | ||||
|                 onClickItem = { (manga, _, _) -> | ||||
|                     router.pushController(MangaController(manga).withFadeTransaction()) | ||||
|                 }, | ||||
|                 onClickResume = { (manga, chapter, _) -> | ||||
|                     presenter.getNextChapterForManga(manga, chapter) | ||||
|                 }, | ||||
|                 onClickDelete = { (manga, _, history), all -> | ||||
|                     if (all) { | ||||
|                         // Reset last read of chapter to 0L | ||||
|                         presenter.removeAllFromHistory(manga.id!!) | ||||
|                     } else { | ||||
|                         // Remove all chapters belonging to manga from library | ||||
|                         presenter.removeFromHistory(history) | ||||
|                     } | ||||
|                 }, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -201,46 +74,33 @@ class HistoryController : | ||||
|             searchView.clearFocus() | ||||
|         } | ||||
|         searchView.queryTextChanges() | ||||
|             .drop(1) // Drop first event after subscribed | ||||
|             .filter { router.backstack.lastOrNull()?.controller == this } | ||||
|             .onEach { | ||||
|                 query = it.toString() | ||||
|                 presenter.updateList(query) | ||||
|                 presenter.search(query) | ||||
|             } | ||||
|             .launchIn(viewScope) | ||||
|  | ||||
|         // Fixes problem with the overflow icon showing up in lieu of search | ||||
|         searchItem.fixExpand( | ||||
|             onExpand = { invalidateMenuOnExpand() }, | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     override fun onOptionsItemSelected(item: MenuItem): Boolean { | ||||
|         when (item.itemId) { | ||||
|         return when (item.itemId) { | ||||
|             R.id.action_clear_history -> { | ||||
|                 val ctrl = ClearHistoryDialogController() | ||||
|                 ctrl.targetController = this@HistoryController | ||||
|                 ctrl.showDialog(router) | ||||
|                 val dialog = ClearHistoryDialogController() | ||||
|                 dialog.targetController = this@HistoryController | ||||
|                 dialog.showDialog(router) | ||||
|                 true | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return super.onOptionsItemSelected(item) | ||||
|     } | ||||
|  | ||||
|     class ClearHistoryDialogController : DialogController() { | ||||
|         override fun onCreateDialog(savedViewState: Bundle?): Dialog { | ||||
|             return MaterialAlertDialogBuilder(activity!!) | ||||
|                 .setMessage(R.string.clear_history_confirmation) | ||||
|                 .setPositiveButton(android.R.string.ok) { _, _ -> | ||||
|                     (targetController as? HistoryController)?.clearHistory() | ||||
|                 } | ||||
|                 .setNegativeButton(android.R.string.cancel, null) | ||||
|                 .create() | ||||
|             else -> super.onOptionsItemSelected(item) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun clearHistory() { | ||||
|         db.deleteHistory().executeAsBlocking() | ||||
|         activity?.toast(R.string.clear_history_completed) | ||||
|     fun openChapter(chapter: Chapter?) { | ||||
|         val activity = activity ?: return | ||||
|         if (chapter != null) { | ||||
|             val intent = ReaderActivity.newIntent(activity, chapter.manga_id, chapter.id) | ||||
|             startActivity(intent) | ||||
|         } else { | ||||
|             activity.toast(R.string.no_next_chapter) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,71 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.ui.recent.history | ||||
|  | ||||
| import android.view.View | ||||
| import coil.dispose | ||||
| import coil.load | ||||
| import eu.davidea.viewholders.FlexibleViewHolder | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory | ||||
| import eu.kanade.tachiyomi.databinding.HistoryItemBinding | ||||
| import eu.kanade.tachiyomi.util.lang.toTimestampString | ||||
| import java.util.Date | ||||
|  | ||||
| /** | ||||
|  * Holder that contains recent manga item | ||||
|  * Uses R.layout.item_recently_read. | ||||
|  * UI related actions should be called from here. | ||||
|  * | ||||
|  * @param view the inflated view for this holder. | ||||
|  * @param adapter the adapter handling this holder. | ||||
|  * @constructor creates a new recent chapter holder. | ||||
|  */ | ||||
| class HistoryHolder( | ||||
|     view: View, | ||||
|     val adapter: HistoryAdapter, | ||||
| ) : FlexibleViewHolder(view, adapter) { | ||||
|  | ||||
|     private val binding = HistoryItemBinding.bind(view) | ||||
|  | ||||
|     init { | ||||
|         binding.holder.setOnClickListener { | ||||
|             adapter.itemClickListener.onItemClick(bindingAdapterPosition) | ||||
|         } | ||||
|  | ||||
|         binding.remove.setOnClickListener { | ||||
|             adapter.removeClickListener.onRemoveClick(bindingAdapterPosition) | ||||
|         } | ||||
|  | ||||
|         binding.resume.setOnClickListener { | ||||
|             adapter.resumeClickListener.onResumeClick(bindingAdapterPosition) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Set values of view | ||||
|      * | ||||
|      * @param item item containing history information | ||||
|      */ | ||||
|     fun bind(item: MangaChapterHistory) { | ||||
|         // Retrieve objects | ||||
|         val (manga, chapter, history) = item | ||||
|  | ||||
|         // Set manga title | ||||
|         binding.mangaTitle.text = manga.title | ||||
|  | ||||
|         // Set chapter number + timestamp | ||||
|         if (chapter.chapter_number > -1f) { | ||||
|             val formattedNumber = adapter.decimalFormat.format(chapter.chapter_number.toDouble()) | ||||
|             binding.mangaSubtitle.text = itemView.context.getString( | ||||
|                 R.string.recent_manga_time, | ||||
|                 formattedNumber, | ||||
|                 Date(history.last_read).toTimestampString(), | ||||
|             ) | ||||
|         } else { | ||||
|             binding.mangaSubtitle.text = Date(history.last_read).toTimestampString() | ||||
|         } | ||||
|  | ||||
|         // Set cover | ||||
|         binding.cover.dispose() | ||||
|         binding.cover.load(item.manga) | ||||
|     } | ||||
| } | ||||
| @@ -1,42 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.ui.recent.history | ||||
|  | ||||
| import android.view.View | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.davidea.flexibleadapter.items.AbstractSectionableItem | ||||
| import eu.davidea.flexibleadapter.items.IFlexible | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory | ||||
| import eu.kanade.tachiyomi.ui.recent.DateSectionItem | ||||
|  | ||||
| class HistoryItem(val mch: MangaChapterHistory, header: DateSectionItem) : | ||||
|     AbstractSectionableItem<HistoryHolder, DateSectionItem>(header) { | ||||
|  | ||||
|     override fun getLayoutRes(): Int { | ||||
|         return R.layout.history_item | ||||
|     } | ||||
|  | ||||
|     override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): HistoryHolder { | ||||
|         return HistoryHolder(view, adapter as HistoryAdapter) | ||||
|     } | ||||
|  | ||||
|     override fun bindViewHolder( | ||||
|         adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, | ||||
|         holder: HistoryHolder, | ||||
|         position: Int, | ||||
|         payloads: List<Any?>?, | ||||
|     ) { | ||||
|         holder.bind(mch) | ||||
|     } | ||||
|  | ||||
|     override fun equals(other: Any?): Boolean { | ||||
|         if (other is HistoryItem) { | ||||
|             return mch.manga.id == other.mch.manga.id | ||||
|         } | ||||
|         return false | ||||
|     } | ||||
|  | ||||
|     override fun hashCode(): Int { | ||||
|         return mch.manga.id!!.hashCode() | ||||
|     } | ||||
| } | ||||
| @@ -1,157 +1,135 @@ | ||||
| package eu.kanade.tachiyomi.ui.recent.history | ||||
|  | ||||
| import android.os.Bundle | ||||
| import eu.kanade.tachiyomi.data.database.DatabaseHelper | ||||
| import androidx.paging.PagingData | ||||
| import androidx.paging.cachedIn | ||||
| import androidx.paging.insertSeparators | ||||
| import androidx.paging.map | ||||
| import eu.kanade.domain.history.interactor.DeleteHistoryTable | ||||
| import eu.kanade.domain.history.interactor.GetHistory | ||||
| import eu.kanade.domain.history.interactor.GetNextChapterForManga | ||||
| import eu.kanade.domain.history.interactor.RemoveHistoryById | ||||
| import eu.kanade.domain.history.interactor.RemoveHistoryByMangaId | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.database.models.Chapter | ||||
| import eu.kanade.tachiyomi.data.database.models.History | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter | ||||
| import eu.kanade.tachiyomi.ui.recent.DateSectionItem | ||||
| import eu.kanade.tachiyomi.util.lang.launchIO | ||||
| import eu.kanade.tachiyomi.util.lang.launchUI | ||||
| import eu.kanade.tachiyomi.util.lang.toDateKey | ||||
| import rx.Observable | ||||
| import rx.Subscription | ||||
| import rx.android.schedulers.AndroidSchedulers | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.text.DateFormat | ||||
| import java.util.Calendar | ||||
| import java.util.Date | ||||
| import java.util.TreeMap | ||||
| import eu.kanade.tachiyomi.util.system.toast | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.MutableStateFlow | ||||
| import kotlinx.coroutines.flow.StateFlow | ||||
| import kotlinx.coroutines.flow.flatMapLatest | ||||
| import kotlinx.coroutines.flow.map | ||||
| import kotlinx.coroutines.flow.update | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.util.* | ||||
|  | ||||
| /** | ||||
|  * Presenter of HistoryFragment. | ||||
|  * Contains information and data for fragment. | ||||
|  * Observable updates should be called from here. | ||||
|  */ | ||||
| class HistoryPresenter : BasePresenter<HistoryController>() { | ||||
| class HistoryPresenter( | ||||
|     private val getHistory: GetHistory = Injekt.get(), | ||||
|     private val getNextChapterForManga: GetNextChapterForManga = Injekt.get(), | ||||
|     private val deleteHistoryTable: DeleteHistoryTable = Injekt.get(), | ||||
|     private val removeHistoryById: RemoveHistoryById = Injekt.get(), | ||||
|     private val removeHistoryByMangaId: RemoveHistoryByMangaId = Injekt.get(), | ||||
| ) : BasePresenter<HistoryController>() { | ||||
|  | ||||
|     private val db: DatabaseHelper by injectLazy() | ||||
|     private val preferences: PreferencesHelper by injectLazy() | ||||
|  | ||||
|     private val relativeTime: Int = preferences.relativeTime().get() | ||||
|     private val dateFormat: DateFormat = preferences.dateFormat() | ||||
|  | ||||
|     private var recentMangaSubscription: Subscription? = null | ||||
|     private var _query: MutableStateFlow<String> = MutableStateFlow("") | ||||
|     private var _state: MutableStateFlow<HistoryState> = MutableStateFlow(HistoryState.EMPTY) | ||||
|     val state: StateFlow<HistoryState> = _state | ||||
|  | ||||
|     override fun onCreate(savedState: Bundle?) { | ||||
|         super.onCreate(savedState) | ||||
|  | ||||
|         // Used to get a list of recently read manga | ||||
|         updateList() | ||||
|     } | ||||
|  | ||||
|     fun requestNext(offset: Int, search: String = "") { | ||||
|         getRecentMangaObservable(offset = offset, search = search) | ||||
|             .subscribeLatestCache( | ||||
|                 { view, mangas -> | ||||
|                     view.onNextManga(mangas) | ||||
|                 }, | ||||
|                 HistoryController::onAddPageError, | ||||
|             ) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get recent manga observable | ||||
|      * @return list of history | ||||
|      */ | ||||
|     private fun getRecentMangaObservable(limit: Int = 25, offset: Int = 0, search: String = ""): Observable<List<HistoryItem>> { | ||||
|         // Set date limit for recent manga | ||||
|         val cal = Calendar.getInstance().apply { | ||||
|             time = Date() | ||||
|             add(Calendar.YEAR, -50) | ||||
|         } | ||||
|  | ||||
|         return db.getRecentManga(cal.time, limit, offset, search).asRxObservable() | ||||
|             .map { recents -> | ||||
|                 val map = TreeMap<Date, MutableList<MangaChapterHistory>> { d1, d2 -> d2.compareTo(d1) } | ||||
|                 val byDay = recents | ||||
|                     .groupByTo(map) { it.history.last_read.toDateKey() } | ||||
|                 byDay.flatMap { entry -> | ||||
|                     val dateItem = DateSectionItem(entry.key, relativeTime, dateFormat) | ||||
|                     entry.value.map { HistoryItem(it, dateItem) } | ||||
|                 } | ||||
|             } | ||||
|             .observeOn(AndroidSchedulers.mainThread()) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Reset last read of chapter to 0L | ||||
|      * @param history history belonging to chapter | ||||
|      */ | ||||
|     fun removeFromHistory(history: History) { | ||||
|         history.last_read = 0L | ||||
|         db.updateHistoryLastRead(history).asRxObservable() | ||||
|             .subscribe() | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Pull a list of history from the db | ||||
|      * @param search a search query to use for filtering | ||||
|      */ | ||||
|     fun updateList(search: String = "") { | ||||
|         recentMangaSubscription?.unsubscribe() | ||||
|         recentMangaSubscription = getRecentMangaObservable(search = search) | ||||
|             .subscribeLatestCache( | ||||
|                 { view, mangas -> | ||||
|                     view.onNextManga(mangas, true) | ||||
|                 }, | ||||
|                 HistoryController::onAddPageError, | ||||
|             ) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Removes all chapters belonging to manga from history. | ||||
|      * @param mangaId id of manga | ||||
|      */ | ||||
|     fun removeAllFromHistory(mangaId: Long) { | ||||
|         db.getHistoryByMangaId(mangaId).asRxSingle() | ||||
|             .map { list -> | ||||
|                 list.forEach { it.last_read = 0L } | ||||
|                 db.updateHistoryLastRead(list).executeAsBlocking() | ||||
|             } | ||||
|             .subscribe() | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Retrieves the next chapter of the given one. | ||||
|      * | ||||
|      * @param chapter the chapter of the history object. | ||||
|      * @param manga the manga of the chapter. | ||||
|      */ | ||||
|     fun getNextChapter(chapter: Chapter, manga: Manga): Chapter? { | ||||
|         if (!chapter.read) { | ||||
|             return chapter | ||||
|         } | ||||
|  | ||||
|         val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) { | ||||
|             Manga.CHAPTER_SORTING_SOURCE -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) } | ||||
|             Manga.CHAPTER_SORTING_NUMBER -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) } | ||||
|             Manga.CHAPTER_SORTING_UPLOAD_DATE -> { c1, c2 -> c1.date_upload.compareTo(c2.date_upload) } | ||||
|             else -> throw NotImplementedError("Unknown sorting method") | ||||
|         } | ||||
|  | ||||
|         val chapters = db.getChapters(manga).executeAsBlocking() | ||||
|             .sortedWith { c1, c2 -> sortFunction(c1, c2) } | ||||
|  | ||||
|         val currChapterIndex = chapters.indexOfFirst { chapter.id == it.id } | ||||
|         return when (manga.sorting) { | ||||
|             Manga.CHAPTER_SORTING_SOURCE -> chapters.getOrNull(currChapterIndex + 1) | ||||
|             Manga.CHAPTER_SORTING_NUMBER -> { | ||||
|                 val chapterNumber = chapter.chapter_number | ||||
|  | ||||
|                 ((currChapterIndex + 1) until chapters.size) | ||||
|                     .map { chapters[it] } | ||||
|                     .firstOrNull { | ||||
|                         it.chapter_number > chapterNumber && | ||||
|                             it.chapter_number <= chapterNumber + 1 | ||||
|         presenterScope.launchIO { | ||||
|             _state.update { state -> | ||||
|                 state.copy( | ||||
|                     list = _query.flatMapLatest { query -> | ||||
|                         getHistory.subscribe(query) | ||||
|                             .map { pagingData -> | ||||
|                                 pagingData | ||||
|                                     .map { | ||||
|                                         UiModel.History(it) | ||||
|                                     } | ||||
|                                     .insertSeparators { before, after -> | ||||
|                                         val beforeDate = | ||||
|                                             before?.item?.history?.last_read?.toDateKey() | ||||
|                                         val afterDate = | ||||
|                                             after?.item?.history?.last_read?.toDateKey() | ||||
|                                         when { | ||||
|                                             beforeDate == null && afterDate != null -> UiModel.Header( | ||||
|                                                 afterDate, | ||||
|                                             ) | ||||
|                                             beforeDate != null && afterDate != null -> UiModel.Header( | ||||
|                                                 afterDate, | ||||
|                                             ) | ||||
|                                             // Return null to avoid adding a separator between two items. | ||||
|                                             else -> null | ||||
|                                         } | ||||
|                                     } | ||||
|                             } | ||||
|                     } | ||||
|                         .cachedIn(presenterScope), | ||||
|                 ) | ||||
|             } | ||||
|             Manga.CHAPTER_SORTING_UPLOAD_DATE -> { | ||||
|                 chapters.drop(currChapterIndex + 1) | ||||
|                     .firstOrNull { it.date_upload >= chapter.date_upload } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun search(query: String) { | ||||
|         presenterScope.launchIO { | ||||
|             _query.emit(query) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun removeFromHistory(history: History) { | ||||
|         presenterScope.launchIO { | ||||
|             removeHistoryById.await(history) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun removeAllFromHistory(mangaId: Long) { | ||||
|         presenterScope.launchIO { | ||||
|             removeHistoryByMangaId.await(mangaId) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun getNextChapterForManga(manga: Manga, chapter: Chapter) { | ||||
|         presenterScope.launchIO { | ||||
|             val chapter = getNextChapterForManga.await(manga, chapter) | ||||
|             view?.openChapter(chapter) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun deleteAllHistory() { | ||||
|         presenterScope.launchIO { | ||||
|             val result = deleteHistoryTable.await() | ||||
|             if (!result) return@launchIO | ||||
|             launchUI { | ||||
|                 view?.activity?.toast(R.string.clear_history_completed) | ||||
|             } | ||||
|             else -> throw NotImplementedError("Unknown sorting method") | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| sealed class UiModel { | ||||
|     data class History(val item: MangaChapterHistory) : UiModel() | ||||
|     data class Header(val date: Date) : UiModel() | ||||
| } | ||||
|  | ||||
| data class HistoryState( | ||||
|     val list: Flow<PagingData<UiModel>>? = null, | ||||
| ) { | ||||
|  | ||||
|     companion object { | ||||
|         val EMPTY = HistoryState(null) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,54 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.ui.recent.history | ||||
|  | ||||
| import android.app.Dialog | ||||
| import android.os.Bundle | ||||
| import com.bluelinelabs.conductor.Controller | ||||
| import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.database.models.History | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.ui.base.controller.DialogController | ||||
| import eu.kanade.tachiyomi.widget.DialogCheckboxView | ||||
|  | ||||
| class RemoveHistoryDialog<T>(bundle: Bundle? = null) : DialogController(bundle) | ||||
|         where T : Controller, T : RemoveHistoryDialog.Listener { | ||||
|  | ||||
|     private var manga: Manga? = null | ||||
|  | ||||
|     private var history: History? = null | ||||
|  | ||||
|     constructor(target: T, manga: Manga, history: History) : this() { | ||||
|         this.manga = manga | ||||
|         this.history = history | ||||
|         targetController = target | ||||
|     } | ||||
|  | ||||
|     override fun onCreateDialog(savedViewState: Bundle?): Dialog { | ||||
|         val activity = activity!! | ||||
|  | ||||
|         // Create custom view | ||||
|         val dialogCheckboxView = DialogCheckboxView(activity).apply { | ||||
|             setDescription(R.string.dialog_with_checkbox_remove_description) | ||||
|             setOptionDescription(R.string.dialog_with_checkbox_reset) | ||||
|         } | ||||
|  | ||||
|         return MaterialAlertDialogBuilder(activity) | ||||
|             .setTitle(R.string.action_remove) | ||||
|             .setView(dialogCheckboxView) | ||||
|             .setPositiveButton(R.string.action_remove) { _, _ -> onPositive(dialogCheckboxView.isChecked()) } | ||||
|             .setNegativeButton(android.R.string.cancel, null) | ||||
|             .create() | ||||
|     } | ||||
|  | ||||
|     private fun onPositive(checked: Boolean) { | ||||
|         val target = targetController as? Listener ?: return | ||||
|         val manga = manga ?: return | ||||
|         val history = history ?: return | ||||
|  | ||||
|         target.removeHistory(manga, history, checked) | ||||
|     } | ||||
|  | ||||
|     interface Listener { | ||||
|         fun removeHistory(manga: Manga, history: History, all: Boolean) | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user