mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 06:17:57 +01:00 
			
		
		
		
	Use Compose in Migrate tab (#7008)
* Use Compose in Migrate tab * Add missing header * Remove unused files * Fix build after rebase * Changes from review comments
This commit is contained in:
		| @@ -3,11 +3,15 @@ package eu.kanade.data.source | ||||
| import eu.kanade.domain.source.model.Source | ||||
| import eu.kanade.tachiyomi.source.CatalogueSource | ||||
|  | ||||
| val sourceMapper: (CatalogueSource) -> Source = { source -> | ||||
| val sourceMapper: (eu.kanade.tachiyomi.source.Source) -> Source = { source -> | ||||
|     Source( | ||||
|         source.id, | ||||
|         source.lang, | ||||
|         source.name, | ||||
|         source.supportsLatest | ||||
|         false | ||||
|     ) | ||||
| } | ||||
|  | ||||
| val catalogueSourceMapper: (CatalogueSource) -> Source = { source -> | ||||
|     sourceMapper(source).copy(supportsLatest = source.supportsLatest) | ||||
| } | ||||
|   | ||||
| @@ -1,18 +1,35 @@ | ||||
| package eu.kanade.data.source | ||||
|  | ||||
| import eu.kanade.data.DatabaseHandler | ||||
| import eu.kanade.domain.source.model.Source | ||||
| import eu.kanade.domain.source.repository.SourceRepository | ||||
| import eu.kanade.tachiyomi.source.LocalSource | ||||
| import eu.kanade.tachiyomi.source.SourceManager | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.map | ||||
|  | ||||
| class SourceRepositoryImpl( | ||||
|     private val sourceManager: SourceManager | ||||
|     private val sourceManager: SourceManager, | ||||
|     private val handler: DatabaseHandler | ||||
| ) : SourceRepository { | ||||
|  | ||||
|     override fun getSources(): Flow<List<Source>> { | ||||
|         return sourceManager.catalogueSources.map { sources -> | ||||
|             sources.map(sourceMapper) | ||||
|             sources.map(catalogueSourceMapper) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun getSourcesWithFavoriteCount(): Flow<List<Pair<Source, Long>>> { | ||||
|         val sourceIdWithFavoriteCount = handler.subscribeToList { mangasQueries.getSourceIdWithFavoriteCount() } | ||||
|         return sourceIdWithFavoriteCount.map { sourceIdsWithCount -> | ||||
|             sourceIdsWithCount | ||||
|                 .map { (sourceId, count) -> | ||||
|                     val source = sourceManager.getOrStub(sourceId).run { | ||||
|                         sourceMapper(this) | ||||
|                     } | ||||
|                     source to count | ||||
|                 } | ||||
|                 .filterNot { it.first.id == LocalSource.ID } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -10,6 +10,8 @@ import eu.kanade.domain.history.interactor.RemoveHistoryByMangaId | ||||
| import eu.kanade.domain.history.repository.HistoryRepository | ||||
| import eu.kanade.domain.source.interactor.DisableSource | ||||
| import eu.kanade.domain.source.interactor.GetEnabledSources | ||||
| import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount | ||||
| import eu.kanade.domain.source.interactor.SetMigrateSorting | ||||
| import eu.kanade.domain.source.interactor.ToggleSourcePin | ||||
| import eu.kanade.domain.source.repository.SourceRepository | ||||
| import uy.kohesive.injekt.api.InjektModule | ||||
| @@ -29,9 +31,11 @@ class DomainModule : InjektModule { | ||||
|         addFactory { RemoveHistoryById(get()) } | ||||
|         addFactory { RemoveHistoryByMangaId(get()) } | ||||
|  | ||||
|         addSingletonFactory<SourceRepository> { SourceRepositoryImpl(get()) } | ||||
|         addSingletonFactory<SourceRepository> { SourceRepositoryImpl(get(), get()) } | ||||
|         addFactory { GetEnabledSources(get(), get()) } | ||||
|         addFactory { DisableSource(get()) } | ||||
|         addFactory { ToggleSourcePin(get()) } | ||||
|         addFactory { GetSourcesWithFavoriteCount(get(), get()) } | ||||
|         addFactory { SetMigrateSorting(get()) } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,58 @@ | ||||
| package eu.kanade.domain.source.interactor | ||||
|  | ||||
| import eu.kanade.domain.source.model.Source | ||||
| import eu.kanade.domain.source.repository.SourceRepository | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.combine | ||||
| import java.text.Collator | ||||
| import java.util.* | ||||
| import kotlin.Comparator | ||||
|  | ||||
| class GetSourcesWithFavoriteCount( | ||||
|     private val repository: SourceRepository, | ||||
|     private val preferences: PreferencesHelper | ||||
| ) { | ||||
|  | ||||
|     fun subscribe(): Flow<List<Pair<Source, Long>>> { | ||||
|         return combine( | ||||
|             preferences.migrationSortingDirection().asFlow(), | ||||
|             preferences.migrationSortingMode().asFlow(), | ||||
|             repository.getSourcesWithFavoriteCount() | ||||
|         ) { direction, mode, list -> | ||||
|             list.sortedWith(sortFn(direction, mode)) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun sortFn( | ||||
|         direction: SetMigrateSorting.Direction, | ||||
|         sorting: SetMigrateSorting.Mode | ||||
|     ): java.util.Comparator<Pair<Source, Long>> { | ||||
|         val locale = Locale.getDefault() | ||||
|         val collator = Collator.getInstance(locale).apply { | ||||
|             strength = Collator.PRIMARY | ||||
|         } | ||||
|         val sortFn: (Pair<Source, Long>, Pair<Source, Long>) -> Int = { a, b -> | ||||
|             val id1 = a.first.name.toLongOrNull() | ||||
|             val id2 = b.first.name.toLongOrNull() | ||||
|             when (sorting) { | ||||
|                 SetMigrateSorting.Mode.ALPHABETICAL -> { | ||||
|                     collator.compare(a.first.name.lowercase(locale), b.first.name.lowercase(locale)) | ||||
|                 } | ||||
|                 SetMigrateSorting.Mode.TOTAL -> { | ||||
|                     when { | ||||
|                         id1 != null && id2 != null -> a.second.compareTo(b.second) | ||||
|                         id1 != null && id2 == null -> -1 | ||||
|                         id2 != null && id1 == null -> 1 | ||||
|                         else -> a.second.compareTo(b.second) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return when (direction) { | ||||
|             SetMigrateSorting.Direction.ASCENDING -> Comparator(sortFn) | ||||
|             SetMigrateSorting.Direction.DESCENDING -> Collections.reverseOrder(sortFn) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,24 @@ | ||||
| package eu.kanade.domain.source.interactor | ||||
|  | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
|  | ||||
| class SetMigrateSorting( | ||||
|     private val preferences: PreferencesHelper | ||||
| ) { | ||||
|  | ||||
|     fun await(mode: Mode, isAscending: Boolean) { | ||||
|         val direction = if (isAscending) Direction.ASCENDING else Direction.DESCENDING | ||||
|         preferences.migrationSortingDirection().set(direction) | ||||
|         preferences.migrationSortingMode().set(mode) | ||||
|     } | ||||
|  | ||||
|     enum class Mode { | ||||
|         ALPHABETICAL, | ||||
|         TOTAL; | ||||
|     } | ||||
|  | ||||
|     enum class Direction { | ||||
|         ASCENDING, | ||||
|         DESCENDING; | ||||
|     } | ||||
| } | ||||
| @@ -6,4 +6,6 @@ import kotlinx.coroutines.flow.Flow | ||||
| interface SourceRepository { | ||||
|  | ||||
|     fun getSources(): Flow<List<Source>> | ||||
|  | ||||
|     fun getSourcesWithFavoriteCount(): Flow<List<Pair<Source, Long>>> | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,16 @@ | ||||
| package eu.kanade.presentation.components | ||||
|  | ||||
| import androidx.compose.foundation.layout.Box | ||||
| import androidx.compose.foundation.layout.fillMaxSize | ||||
| import androidx.compose.foundation.layout.size | ||||
| import androidx.compose.material3.CircularProgressIndicator | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.unit.dp | ||||
|  | ||||
| @Composable | ||||
| fun LoadingScreen() { | ||||
|     Box(modifier = Modifier.fillMaxSize()) { | ||||
|         CircularProgressIndicator(modifier = Modifier.size(64.dp)) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,117 @@ | ||||
| package eu.kanade.presentation.source | ||||
|  | ||||
| import androidx.compose.foundation.background | ||||
| import androidx.compose.foundation.layout.WindowInsets | ||||
| import androidx.compose.foundation.layout.asPaddingValues | ||||
| import androidx.compose.foundation.layout.navigationBars | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.foundation.lazy.items | ||||
| import androidx.compose.foundation.shape.RoundedCornerShape | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.collectAsState | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.draw.clip | ||||
| import androidx.compose.ui.input.nestedscroll.NestedScrollConnection | ||||
| import androidx.compose.ui.input.nestedscroll.nestedScroll | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import androidx.compose.ui.unit.dp | ||||
| import eu.kanade.domain.source.model.Source | ||||
| import eu.kanade.presentation.components.EmptyScreen | ||||
| import eu.kanade.presentation.components.LoadingScreen | ||||
| import eu.kanade.presentation.source.components.BaseSourceItem | ||||
| import eu.kanade.presentation.theme.header | ||||
| import eu.kanade.presentation.util.horizontalPadding | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesPresenter | ||||
|  | ||||
| @Composable | ||||
| fun MigrateSourceScreen( | ||||
|     nestedScrollInterop: NestedScrollConnection, | ||||
|     presenter: MigrationSourcesPresenter, | ||||
|     onClickItem: (Source) -> Unit, | ||||
|     onLongClickItem: (Source) -> Unit, | ||||
| ) { | ||||
|     val state by presenter.state.collectAsState() | ||||
|     when { | ||||
|         state.isLoading -> LoadingScreen() | ||||
|         state.isEmpty -> EmptyScreen(textResource = R.string.information_empty_library) | ||||
|         else -> { | ||||
|             MigrateSourceList( | ||||
|                 nestedScrollInterop = nestedScrollInterop, | ||||
|                 list = state.sources!!, | ||||
|                 onClickItem = onClickItem, | ||||
|                 onLongClickItem = onLongClickItem, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| fun MigrateSourceList( | ||||
|     nestedScrollInterop: NestedScrollConnection, | ||||
|     list: List<Pair<Source, Long>>, | ||||
|     onClickItem: (Source) -> Unit, | ||||
|     onLongClickItem: (Source) -> Unit, | ||||
| ) { | ||||
|     LazyColumn( | ||||
|         modifier = Modifier.nestedScroll(nestedScrollInterop), | ||||
|         contentPadding = WindowInsets.navigationBars.asPaddingValues(), | ||||
|     ) { | ||||
|         item(key = "title") { | ||||
|             Text( | ||||
|                 text = stringResource(id = R.string.migration_selection_prompt), | ||||
|                 modifier = Modifier | ||||
|                     .animateItemPlacement() | ||||
|                     .padding(horizontal = horizontalPadding, vertical = 8.dp), | ||||
|                 style = MaterialTheme.typography.header | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|         items( | ||||
|             items = list, | ||||
|             key = { (source, _) -> | ||||
|                 source.id | ||||
|             } | ||||
|         ) { (source, count) -> | ||||
|             MigrateSourceItem( | ||||
|                 modifier = Modifier.animateItemPlacement(), | ||||
|                 source = source, | ||||
|                 count = count, | ||||
|                 onClickItem = { onClickItem(source) }, | ||||
|                 onLongClickItem = { onLongClickItem(source) } | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| fun MigrateSourceItem( | ||||
|     modifier: Modifier = Modifier, | ||||
|     source: Source, | ||||
|     count: Long, | ||||
|     onClickItem: () -> Unit, | ||||
|     onLongClickItem: () -> Unit, | ||||
| ) { | ||||
|     BaseSourceItem( | ||||
|         modifier = modifier, | ||||
|         source = source, | ||||
|         onClickItem = onClickItem, | ||||
|         onLongClickItem = onLongClickItem, | ||||
|         action = { | ||||
|             Text( | ||||
|                 text = "$count", | ||||
|                 modifier = Modifier | ||||
|                     .clip(RoundedCornerShape(4.dp)) | ||||
|                     .background(MaterialTheme.colorScheme.primary) | ||||
|                     .padding(horizontal = 8.dp, vertical = 2.dp), | ||||
|                 style = MaterialTheme.typography.bodyMedium.copy( | ||||
|                     color = MaterialTheme.colorScheme.onPrimary | ||||
|                 ) | ||||
|             ) | ||||
|         } | ||||
|     ) | ||||
| } | ||||
| @@ -2,9 +2,7 @@ package eu.kanade.presentation.source | ||||
|  | ||||
| import androidx.compose.foundation.Image | ||||
| import androidx.compose.foundation.clickable | ||||
| import androidx.compose.foundation.combinedClickable | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.Row | ||||
| import androidx.compose.foundation.layout.WindowInsets | ||||
| import androidx.compose.foundation.layout.asPaddingValues | ||||
| import androidx.compose.foundation.layout.aspectRatio | ||||
| @@ -18,7 +16,6 @@ import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.filled.PushPin | ||||
| import androidx.compose.material.icons.outlined.PushPin | ||||
| import androidx.compose.material3.AlertDialog | ||||
| import androidx.compose.material3.CircularProgressIndicator | ||||
| import androidx.compose.material3.Icon | ||||
| import androidx.compose.material3.IconButton | ||||
| import androidx.compose.material3.LocalTextStyle | ||||
| @@ -30,18 +27,18 @@ 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.LocalContext | ||||
| import androidx.compose.ui.res.painterResource | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import androidx.compose.ui.text.style.TextOverflow | ||||
| import androidx.compose.ui.unit.dp | ||||
| import eu.kanade.domain.source.model.Pin | ||||
| import eu.kanade.domain.source.model.Source | ||||
| import eu.kanade.presentation.components.EmptyScreen | ||||
| import eu.kanade.presentation.components.LoadingScreen | ||||
| import eu.kanade.presentation.source.components.BaseSourceItem | ||||
| import eu.kanade.presentation.theme.header | ||||
| import eu.kanade.presentation.util.horizontalPadding | ||||
| import eu.kanade.tachiyomi.R | ||||
| @@ -62,7 +59,7 @@ fun SourceScreen( | ||||
|     val state by presenter.state.collectAsState() | ||||
|  | ||||
|     when { | ||||
|         state.isLoading -> CircularProgressIndicator() | ||||
|         state.isLoading -> LoadingScreen() | ||||
|         state.hasError -> Text(text = state.error!!.message!!) | ||||
|         state.isEmpty -> EmptyScreen(message = "") | ||||
|         else -> SourceList( | ||||
| @@ -115,7 +112,7 @@ fun SourceList( | ||||
|                 } | ||||
|                 is UiModel.Item -> SourceItem( | ||||
|                     modifier = Modifier.animateItemPlacement(), | ||||
|                     item = model.source, | ||||
|                     source = model.source, | ||||
|                     onClickItem = onClickItem, | ||||
|                     onLongClickItem = { | ||||
|                         setSourceState(it) | ||||
| @@ -160,55 +157,34 @@ fun SourceHeader( | ||||
| @Composable | ||||
| fun SourceItem( | ||||
|     modifier: Modifier = Modifier, | ||||
|     item: Source, | ||||
|     source: Source, | ||||
|     onClickItem: (Source) -> Unit, | ||||
|     onLongClickItem: (Source) -> Unit, | ||||
|     onClickLatest: (Source) -> Unit, | ||||
|     onClickPin: (Source) -> Unit | ||||
| ) { | ||||
|     Row( | ||||
|         modifier = modifier | ||||
|             .combinedClickable( | ||||
|                 onClick = { onClickItem(item) }, | ||||
|                 onLongClick = { onLongClickItem(item) } | ||||
|             ) | ||||
|             .padding(horizontal = horizontalPadding, vertical = 8.dp), | ||||
|         verticalAlignment = Alignment.CenterVertically, | ||||
|     ) { | ||||
|         SourceIcon(source = item) | ||||
|         Column( | ||||
|             modifier = Modifier | ||||
|                 .padding(horizontal = horizontalPadding) | ||||
|                 .weight(1f) | ||||
|         ) { | ||||
|             Text( | ||||
|                 text = item.name, | ||||
|                 maxLines = 1, | ||||
|                 overflow = TextOverflow.Ellipsis, | ||||
|                 style = MaterialTheme.typography.bodyMedium | ||||
|             ) | ||||
|             Text( | ||||
|                 text = LocaleHelper.getDisplayName(item.lang), | ||||
|                 maxLines = 1, | ||||
|                 overflow = TextOverflow.Ellipsis, | ||||
|                 style = MaterialTheme.typography.bodySmall | ||||
|             ) | ||||
|         } | ||||
|         if (item.supportsLatest) { | ||||
|             TextButton(onClick = { onClickLatest(item) }) { | ||||
|                 Text( | ||||
|                     text = stringResource(id = R.string.latest), | ||||
|                     style = LocalTextStyle.current.copy( | ||||
|                         color = MaterialTheme.colorScheme.primary | ||||
|                     ), | ||||
|                 ) | ||||
|     BaseSourceItem( | ||||
|         modifier = modifier, | ||||
|         source = source, | ||||
|         onClickItem = { onClickItem(source) }, | ||||
|         onLongClickItem = { onLongClickItem(source) }, | ||||
|         action = { source -> | ||||
|             if (source.supportsLatest) { | ||||
|                 TextButton(onClick = { onClickLatest(source) }) { | ||||
|                     Text( | ||||
|                         text = stringResource(id = R.string.latest), | ||||
|                         style = LocalTextStyle.current.copy( | ||||
|                             color = MaterialTheme.colorScheme.primary | ||||
|                         ) | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         SourcePinButton( | ||||
|             isPinned = Pin.Pinned in item.pin, | ||||
|             onClick = { onClickPin(item) } | ||||
|         ) | ||||
|     } | ||||
|             SourcePinButton( | ||||
|                 isPinned = Pin.Pinned in source.pin, | ||||
|                 onClick = { onClickPin(source) } | ||||
|             ) | ||||
|         }, | ||||
|     ) | ||||
| } | ||||
|  | ||||
| @Composable | ||||
|   | ||||
| @@ -0,0 +1,68 @@ | ||||
| package eu.kanade.presentation.source.components | ||||
|  | ||||
| import androidx.compose.foundation.combinedClickable | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.Row | ||||
| import androidx.compose.foundation.layout.RowScope | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.text.style.TextOverflow | ||||
| import androidx.compose.ui.unit.dp | ||||
| import eu.kanade.domain.source.model.Source | ||||
| import eu.kanade.presentation.source.SourceIcon | ||||
| import eu.kanade.presentation.util.horizontalPadding | ||||
| import eu.kanade.tachiyomi.util.system.LocaleHelper | ||||
|  | ||||
| @Composable | ||||
| fun BaseSourceItem( | ||||
|     modifier: Modifier = Modifier, | ||||
|     source: Source, | ||||
|     onClickItem: () -> Unit = {}, | ||||
|     onLongClickItem: () -> Unit = {}, | ||||
|     icon: @Composable RowScope.(Source) -> Unit = defaultIcon, | ||||
|     action: @Composable RowScope.(Source) -> Unit = {}, | ||||
|     content: @Composable RowScope.(Source) -> Unit = defaultContent, | ||||
| ) { | ||||
|     Row( | ||||
|         modifier = modifier | ||||
|             .combinedClickable( | ||||
|                 onClick = onClickItem, | ||||
|                 onLongClick = onLongClickItem | ||||
|             ) | ||||
|             .padding(horizontal = horizontalPadding, vertical = 8.dp), | ||||
|         verticalAlignment = Alignment.CenterVertically | ||||
|     ) { | ||||
|         icon.invoke(this, source) | ||||
|         content.invoke(this, source) | ||||
|         action.invoke(this, source) | ||||
|     } | ||||
| } | ||||
|  | ||||
| private val defaultIcon: @Composable RowScope.(Source) -> Unit = { source -> | ||||
|     SourceIcon(source = source) | ||||
| } | ||||
|  | ||||
| private val defaultContent: @Composable RowScope.(Source) -> Unit = { source -> | ||||
|     Column( | ||||
|         modifier = Modifier | ||||
|             .padding(horizontal = horizontalPadding) | ||||
|             .weight(1f) | ||||
|     ) { | ||||
|         Text( | ||||
|             text = source.name, | ||||
|             maxLines = 1, | ||||
|             overflow = TextOverflow.Ellipsis, | ||||
|             style = MaterialTheme.typography.bodyMedium | ||||
|         ) | ||||
|         Text( | ||||
|             text = LocaleHelper.getDisplayName(source.lang), | ||||
|             maxLines = 1, | ||||
|             overflow = TextOverflow.Ellipsis, | ||||
|             style = MaterialTheme.typography.bodySmall | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -7,11 +7,11 @@ import androidx.core.content.edit | ||||
| import androidx.core.net.toUri | ||||
| import androidx.preference.PreferenceManager | ||||
| import com.fredporciuncula.flow.preferences.FlowSharedPreferences | ||||
| import eu.kanade.domain.source.interactor.SetMigrateSorting | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.data.track.TrackService | ||||
| import eu.kanade.tachiyomi.data.track.anilist.Anilist | ||||
| import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesController | ||||
| import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting | ||||
| import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting | ||||
| import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting | ||||
| @@ -254,8 +254,8 @@ class PreferencesHelper(val context: Context) { | ||||
|     fun librarySortingMode() = flowPrefs.getEnum(Keys.librarySortingMode, SortModeSetting.ALPHABETICAL) | ||||
|     fun librarySortingAscending() = flowPrefs.getEnum(Keys.librarySortingDirection, SortDirectionSetting.ASCENDING) | ||||
|  | ||||
|     fun migrationSortingMode() = flowPrefs.getEnum(Keys.migrationSortingMode, MigrationSourcesController.SortSetting.ALPHABETICAL) | ||||
|     fun migrationSortingDirection() = flowPrefs.getEnum(Keys.migrationSortingDirection, MigrationSourcesController.DirectionSetting.ASCENDING) | ||||
|     fun migrationSortingMode() = flowPrefs.getEnum(Keys.migrationSortingMode, SetMigrateSorting.Mode.ALPHABETICAL) | ||||
|     fun migrationSortingDirection() = flowPrefs.getEnum(Keys.migrationSortingDirection, SetMigrateSorting.Direction.ASCENDING) | ||||
|  | ||||
|     fun automaticExtUpdates() = flowPrefs.getBoolean("automatic_ext_updates", true) | ||||
|  | ||||
|   | ||||
| @@ -1,124 +1,68 @@ | ||||
| package eu.kanade.tachiyomi.ui.browse.migration.sources | ||||
|  | ||||
| import android.view.LayoutInflater | ||||
| import android.view.Menu | ||||
| import android.view.MenuInflater | ||||
| import android.view.MenuItem | ||||
| import android.view.View | ||||
| import androidx.recyclerview.widget.LinearLayoutManager | ||||
| import dev.chrisbanes.insetter.applyInsetter | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.input.nestedscroll.NestedScrollConnection | ||||
| import eu.kanade.presentation.source.MigrateSourceScreen | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.databinding.MigrationSourcesControllerBinding | ||||
| import eu.kanade.tachiyomi.ui.base.controller.NucleusController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.ComposeController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.pushController | ||||
| import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrationMangaController | ||||
| import eu.kanade.tachiyomi.util.system.copyToClipboard | ||||
| import eu.kanade.tachiyomi.util.system.openInBrowser | ||||
| import uy.kohesive.injekt.injectLazy | ||||
|  | ||||
| class MigrationSourcesController : | ||||
|     NucleusController<MigrationSourcesControllerBinding, MigrationSourcesPresenter>(), | ||||
|     FlexibleAdapter.OnItemClickListener, | ||||
|     FlexibleAdapter.OnItemLongClickListener { | ||||
|  | ||||
|     private val preferences: PreferencesHelper by injectLazy() | ||||
|  | ||||
|     private var adapter: SourceAdapter? = null | ||||
| class MigrationSourcesController : ComposeController<MigrationSourcesPresenter>() { | ||||
|  | ||||
|     init { | ||||
|         setHasOptionsMenu(true) | ||||
|     } | ||||
|  | ||||
|     override fun createPresenter(): MigrationSourcesPresenter { | ||||
|         return MigrationSourcesPresenter() | ||||
|     } | ||||
|     override fun createPresenter(): MigrationSourcesPresenter = | ||||
|         MigrationSourcesPresenter() | ||||
|  | ||||
|     override fun createBinding(inflater: LayoutInflater) = MigrationSourcesControllerBinding.inflate(inflater) | ||||
|  | ||||
|     override fun onViewCreated(view: View) { | ||||
|         super.onViewCreated(view) | ||||
|  | ||||
|         binding.recycler.applyInsetter { | ||||
|             type(navigationBars = true) { | ||||
|                 padding() | ||||
|     @Composable | ||||
|     override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) { | ||||
|         MigrateSourceScreen( | ||||
|             nestedScrollInterop = nestedScrollInterop, | ||||
|             presenter = presenter, | ||||
|             onClickItem = { source -> | ||||
|                 parentController!!.router.pushController( | ||||
|                     MigrationMangaController( | ||||
|                         source.id, | ||||
|                         source.name | ||||
|                     ) | ||||
|                 ) | ||||
|             }, | ||||
|             onLongClickItem = { source -> | ||||
|                 val sourceId = source.id.toString() | ||||
|                 activity?.copyToClipboard(sourceId, sourceId) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         adapter = SourceAdapter(this) | ||||
|         binding.recycler.layoutManager = LinearLayoutManager(view.context) | ||||
|         binding.recycler.adapter = adapter | ||||
|         adapter?.fastScroller = binding.fastScroller | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     override fun onDestroyView(view: View) { | ||||
|         adapter = null | ||||
|         super.onDestroyView(view) | ||||
|     } | ||||
|  | ||||
|     override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { | ||||
|     override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) = | ||||
|         inflater.inflate(R.menu.browse_migrate, menu) | ||||
|     } | ||||
|  | ||||
|     override fun onOptionsItemSelected(item: MenuItem): Boolean { | ||||
|         when (val itemId = item.itemId) { | ||||
|             R.id.action_source_migration_help -> activity?.openInBrowser(HELP_URL) | ||||
|             R.id.asc_alphabetical, R.id.desc_alphabetical -> { | ||||
|                 setSortingDirection(SortSetting.ALPHABETICAL, itemId == R.id.asc_alphabetical) | ||||
|         return when (val itemId = item.itemId) { | ||||
|             R.id.action_source_migration_help -> { | ||||
|                 activity?.openInBrowser(HELP_URL) | ||||
|                 true | ||||
|             } | ||||
|             R.id.asc_count, R.id.desc_count -> { | ||||
|                 setSortingDirection(SortSetting.TOTAL, itemId == R.id.asc_count) | ||||
|             R.id.asc_alphabetical, | ||||
|             R.id.desc_alphabetical -> { | ||||
|                 presenter.setAlphabeticalSorting(itemId == R.id.asc_alphabetical) | ||||
|                 true | ||||
|             } | ||||
|             R.id.asc_count, | ||||
|             R.id.desc_count -> { | ||||
|                 presenter.setTotalSorting(itemId == R.id.asc_count) | ||||
|                 true | ||||
|             } | ||||
|             else -> super.onOptionsItemSelected(item) | ||||
|         } | ||||
|         return super.onOptionsItemSelected(item) | ||||
|     } | ||||
|  | ||||
|     private fun setSortingDirection(sortSetting: SortSetting, isAscending: Boolean) { | ||||
|         val direction = if (isAscending) { | ||||
|             DirectionSetting.ASCENDING | ||||
|         } else { | ||||
|             DirectionSetting.DESCENDING | ||||
|         } | ||||
|  | ||||
|         preferences.migrationSortingDirection().set(direction) | ||||
|         preferences.migrationSortingMode().set(sortSetting) | ||||
|  | ||||
|         presenter.requestSortUpdate() | ||||
|     } | ||||
|  | ||||
|     fun setSources(sourcesWithManga: List<SourceItem>) { | ||||
|         // Show empty view if needed | ||||
|         if (sourcesWithManga.isNotEmpty()) { | ||||
|             binding.emptyView.hide() | ||||
|         } else { | ||||
|             binding.emptyView.show(R.string.information_empty_library) | ||||
|         } | ||||
|  | ||||
|         adapter?.updateDataSet(sourcesWithManga) | ||||
|     } | ||||
|  | ||||
|     override fun onItemClick(view: View, position: Int): Boolean { | ||||
|         val item = adapter?.getItem(position) as? SourceItem ?: return false | ||||
|         val controller = MigrationMangaController(item.source.id, item.source.name) | ||||
|         parentController!!.router.pushController(controller) | ||||
|         return false | ||||
|     } | ||||
|  | ||||
|     override fun onItemLongClick(position: Int) { | ||||
|         val item = adapter?.getItem(position) as? SourceItem ?: return | ||||
|         val sourceId = item.source.id.toString() | ||||
|         activity?.copyToClipboard(sourceId, sourceId) | ||||
|     } | ||||
|  | ||||
|     enum class DirectionSetting { | ||||
|         ASCENDING, | ||||
|         DESCENDING; | ||||
|     } | ||||
|  | ||||
|     enum class SortSetting { | ||||
|         ALPHABETICAL, | ||||
|         TOTAL; | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,82 +1,60 @@ | ||||
| package eu.kanade.tachiyomi.ui.browse.migration.sources | ||||
|  | ||||
| import android.os.Bundle | ||||
| import com.jakewharton.rxrelay.BehaviorRelay | ||||
| import eu.kanade.tachiyomi.data.database.DatabaseHelper | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.source.LocalSource | ||||
| import eu.kanade.tachiyomi.source.SourceManager | ||||
| import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount | ||||
| import eu.kanade.domain.source.interactor.SetMigrateSorting | ||||
| import eu.kanade.domain.source.model.Source | ||||
| import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter | ||||
| import eu.kanade.tachiyomi.util.lang.combineLatest | ||||
| import rx.android.schedulers.AndroidSchedulers | ||||
| import rx.schedulers.Schedulers | ||||
| import eu.kanade.tachiyomi.util.lang.launchIO | ||||
| import kotlinx.coroutines.flow.MutableStateFlow | ||||
| import kotlinx.coroutines.flow.StateFlow | ||||
| import kotlinx.coroutines.flow.asStateFlow | ||||
| import kotlinx.coroutines.flow.collectLatest | ||||
| import kotlinx.coroutines.flow.update | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.text.Collator | ||||
| import java.util.Collections | ||||
| import java.util.Locale | ||||
|  | ||||
| class MigrationSourcesPresenter( | ||||
|     private val sourceManager: SourceManager = Injekt.get(), | ||||
|     private val db: DatabaseHelper = Injekt.get(), | ||||
|     private val getSourcesWithFavoriteCount: GetSourcesWithFavoriteCount = Injekt.get(), | ||||
|     private val setMigrateSorting: SetMigrateSorting = Injekt.get() | ||||
| ) : BasePresenter<MigrationSourcesController>() { | ||||
|  | ||||
|     private val preferences: PreferencesHelper by injectLazy() | ||||
|  | ||||
|     private val sortRelay = BehaviorRelay.create(Unit) | ||||
|     private val _state: MutableStateFlow<MigrateSourceState> = MutableStateFlow(MigrateSourceState.EMPTY) | ||||
|     val state: StateFlow<MigrateSourceState> = _state.asStateFlow() | ||||
|  | ||||
|     override fun onCreate(savedState: Bundle?) { | ||||
|         super.onCreate(savedState) | ||||
|  | ||||
|         db.getFavoriteMangas() | ||||
|             .asRxObservable() | ||||
|             .combineLatest(sortRelay.observeOn(Schedulers.io())) { sources, _ -> sources } | ||||
|             .observeOn(AndroidSchedulers.mainThread()) | ||||
|             .map { findSourcesWithManga(it) } | ||||
|             .subscribeLatestCache(MigrationSourcesController::setSources) | ||||
|         presenterScope.launchIO { | ||||
|             getSourcesWithFavoriteCount.subscribe() | ||||
|                 .collectLatest { sources -> | ||||
|                     _state.update { state -> | ||||
|                         state.copy(sources = sources) | ||||
|                     } | ||||
|                 } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun requestSortUpdate() { | ||||
|         sortRelay.call(Unit) | ||||
|     fun setAlphabeticalSorting(isAscending: Boolean) { | ||||
|         setMigrateSorting.await(SetMigrateSorting.Mode.ALPHABETICAL, isAscending) | ||||
|     } | ||||
|  | ||||
|     private fun findSourcesWithManga(library: List<Manga>): List<SourceItem> { | ||||
|         val header = SelectionHeader() | ||||
|         return library | ||||
|             .groupBy { it.source } | ||||
|             .filterKeys { it != LocalSource.ID } | ||||
|             .map { | ||||
|                 val source = sourceManager.getOrStub(it.key) | ||||
|                 SourceItem(source, it.value.size, header) | ||||
|             } | ||||
|             .sortedWith(sortFn()) | ||||
|             .toList() | ||||
|     } | ||||
|  | ||||
|     private fun sortFn(): java.util.Comparator<SourceItem> { | ||||
|         val sort by lazy { | ||||
|             preferences.migrationSortingMode().get() | ||||
|         } | ||||
|         val direction by lazy { | ||||
|             preferences.migrationSortingDirection().get() | ||||
|         } | ||||
|  | ||||
|         val locale = Locale.getDefault() | ||||
|         val collator = Collator.getInstance(locale).apply { | ||||
|             strength = Collator.PRIMARY | ||||
|         } | ||||
|         val sortFn: (SourceItem, SourceItem) -> Int = { a, b -> | ||||
|             when (sort) { | ||||
|                 MigrationSourcesController.SortSetting.ALPHABETICAL -> collator.compare(a.source.name.lowercase(locale), b.source.name.lowercase(locale)) | ||||
|                 MigrationSourcesController.SortSetting.TOTAL -> a.mangaCount.compareTo(b.mangaCount) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return when (direction) { | ||||
|             MigrationSourcesController.DirectionSetting.ASCENDING -> Comparator(sortFn) | ||||
|             MigrationSourcesController.DirectionSetting.DESCENDING -> Collections.reverseOrder(sortFn) | ||||
|         } | ||||
|     fun setTotalSorting(isAscending: Boolean) { | ||||
|         setMigrateSorting.await(SetMigrateSorting.Mode.TOTAL, isAscending) | ||||
|     } | ||||
| } | ||||
|  | ||||
| data class MigrateSourceState( | ||||
|     val sources: List<Pair<Source, Long>>? | ||||
| ) { | ||||
|  | ||||
|     val isLoading: Boolean | ||||
|         get() = sources == null | ||||
|  | ||||
|     val isEmpty: Boolean | ||||
|         get() = sources.isNullOrEmpty() | ||||
|  | ||||
|     companion object { | ||||
|         val EMPTY = MigrateSourceState(null) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,62 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.ui.browse.migration.sources | ||||
|  | ||||
| import android.view.View | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.davidea.flexibleadapter.items.AbstractHeaderItem | ||||
| import eu.davidea.flexibleadapter.items.IFlexible | ||||
| import eu.davidea.viewholders.FlexibleViewHolder | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.databinding.SectionHeaderItemBinding | ||||
|  | ||||
| /** | ||||
|  * Item that contains the selection header. | ||||
|  */ | ||||
| class SelectionHeader : AbstractHeaderItem<SelectionHeader.Holder>() { | ||||
|  | ||||
|     /** | ||||
|      * Returns the layout resource of this item. | ||||
|      */ | ||||
|     override fun getLayoutRes(): Int { | ||||
|         return R.layout.section_header_item | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Creates a new view holder for this item. | ||||
|      */ | ||||
|     override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder { | ||||
|         return Holder( | ||||
|             view, | ||||
|             adapter, | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Binds this item to the given view holder. | ||||
|      */ | ||||
|     override fun bindViewHolder( | ||||
|         adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, | ||||
|         holder: Holder, | ||||
|         position: Int, | ||||
|         payloads: List<Any?>?, | ||||
|     ) { | ||||
|         // Intentionally empty | ||||
|     } | ||||
|  | ||||
|     class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) { | ||||
|  | ||||
|         private val binding = SectionHeaderItemBinding.bind(view) | ||||
|  | ||||
|         init { | ||||
|             binding.title.text = view.context.getString(R.string.migration_selection_prompt) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun equals(other: Any?): Boolean { | ||||
|         return other is SelectionHeader | ||||
|     } | ||||
|  | ||||
|     override fun hashCode(): Int { | ||||
|         return 0 | ||||
|     } | ||||
| } | ||||
| @@ -1,18 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.ui.browse.migration.sources | ||||
|  | ||||
| import com.bluelinelabs.conductor.Controller | ||||
| import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.davidea.flexibleadapter.items.IFlexible | ||||
|  | ||||
| /** | ||||
|  * Adapter that holds the catalogue cards. | ||||
|  * | ||||
|  * @param controller instance of [MigrationController]. | ||||
|  */ | ||||
| class SourceAdapter(controller: Controller) : | ||||
|     FlexibleAdapter<IFlexible<*>>(null, controller, true) { | ||||
|  | ||||
|     init { | ||||
|         setDisplayHeadersAtStartUp(true) | ||||
|     } | ||||
| } | ||||
| @@ -1,27 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.ui.browse.migration.sources | ||||
|  | ||||
| import android.view.View | ||||
| import androidx.core.view.isVisible | ||||
| import coil.load | ||||
| import eu.davidea.viewholders.FlexibleViewHolder | ||||
| import eu.kanade.tachiyomi.databinding.SourceMainControllerItemBinding | ||||
| import eu.kanade.tachiyomi.source.icon | ||||
| import eu.kanade.tachiyomi.util.system.LocaleHelper | ||||
|  | ||||
| class SourceHolder(view: View, val adapter: SourceAdapter) : | ||||
|     FlexibleViewHolder(view, adapter) { | ||||
|  | ||||
|     private val binding = SourceMainControllerItemBinding.bind(view) | ||||
|  | ||||
|     fun bind(item: SourceItem) { | ||||
|         val source = item.source | ||||
|  | ||||
|         binding.title.text = "${source.name} (${item.mangaCount})" | ||||
|         binding.subtitle.isVisible = source.lang != "" | ||||
|         binding.subtitle.text = LocaleHelper.getDisplayName(source.lang) | ||||
|  | ||||
|         itemView.post { | ||||
|             binding.image.load(source.icon()) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,48 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.ui.browse.migration.sources | ||||
|  | ||||
| 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.source.Source | ||||
|  | ||||
| /** | ||||
|  * Item that contains source information. | ||||
|  * | ||||
|  * @param source Instance of [Source] containing source information. | ||||
|  * @param header The header for this item. | ||||
|  */ | ||||
| data class SourceItem(val source: Source, val mangaCount: Int, val header: SelectionHeader) : | ||||
|     AbstractSectionableItem<SourceHolder, SelectionHeader>(header) { | ||||
|  | ||||
|     /** | ||||
|      * Returns the layout resource of this item. | ||||
|      */ | ||||
|     override fun getLayoutRes(): Int { | ||||
|         return R.layout.source_main_controller_item | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Creates a new view holder for this item. | ||||
|      */ | ||||
|     override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): SourceHolder { | ||||
|         return SourceHolder( | ||||
|             view, | ||||
|             adapter as SourceAdapter, | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Binds this item to the given view holder. | ||||
|      */ | ||||
|     override fun bindViewHolder( | ||||
|         adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, | ||||
|         holder: SourceHolder, | ||||
|         position: Int, | ||||
|         payloads: List<Any?>?, | ||||
|     ) { | ||||
|         holder.bind(this) | ||||
|     } | ||||
| } | ||||
| @@ -1,31 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="wrap_content"> | ||||
|  | ||||
|     <androidx.recyclerview.widget.RecyclerView | ||||
|         android:id="@+id/recycler" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="match_parent" | ||||
|         android:clipToPadding="false" | ||||
|         android:paddingTop="8dp" | ||||
|         android:paddingBottom="@dimen/action_toolbar_list_padding" /> | ||||
|  | ||||
|     <eu.kanade.tachiyomi.widget.MaterialFastScroll | ||||
|         android:id="@+id/fast_scroller" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="match_parent" | ||||
|         android:layout_gravity="end" | ||||
|         app:fastScrollerBubbleEnabled="false" | ||||
|         tools:visibility="visible" /> | ||||
|  | ||||
|     <eu.kanade.tachiyomi.widget.EmptyView | ||||
|         android:id="@+id/empty_view" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_gravity="center" | ||||
|         android:visibility="gone" /> | ||||
|  | ||||
| </FrameLayout> | ||||
| @@ -1,54 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="64dp" | ||||
|     android:background="@drawable/list_item_selector_background"> | ||||
|  | ||||
|     <ImageView | ||||
|         android:id="@+id/image" | ||||
|         android:layout_width="0dp" | ||||
|         android:layout_height="0dp" | ||||
|         android:paddingStart="16dp" | ||||
|         android:paddingEnd="8dp" | ||||
|         app:layout_constraintBottom_toBottomOf="parent" | ||||
|         app:layout_constraintDimensionRatio="1:1" | ||||
|         app:layout_constraintStart_toStartOf="parent" | ||||
|         app:layout_constraintTop_toTopOf="parent" | ||||
|         tools:ignore="ContentDescription" | ||||
|         tools:src="@mipmap/ic_launcher_round" /> | ||||
|  | ||||
|     <TextView | ||||
|         android:id="@+id/title" | ||||
|         android:layout_width="0dp" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_marginEnd="8dp" | ||||
|         android:ellipsize="end" | ||||
|         android:maxLines="1" | ||||
|         android:paddingStart="0dp" | ||||
|         android:paddingEnd="8dp" | ||||
|         android:textAppearance="?attr/textAppearanceBodyMedium" | ||||
|         app:layout_constraintBottom_toTopOf="@id/subtitle" | ||||
|         app:layout_constraintEnd_toEndOf="parent" | ||||
|         app:layout_constraintStart_toEndOf="@+id/image" | ||||
|         app:layout_constraintTop_toTopOf="parent" | ||||
|         app:layout_constraintVertical_chainStyle="packed" | ||||
|         tools:text="Source title" /> | ||||
|  | ||||
|     <TextView | ||||
|         android:id="@+id/subtitle" | ||||
|         android:layout_width="0dp" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_marginEnd="8dp" | ||||
|         android:maxLines="1" | ||||
|         android:textAppearance="?attr/textAppearanceBodySmall" | ||||
|         android:visibility="gone" | ||||
|         app:layout_constraintBottom_toBottomOf="parent" | ||||
|         app:layout_constraintEnd_toEndOf="parent" | ||||
|         app:layout_constraintStart_toEndOf="@id/image" | ||||
|         app:layout_constraintTop_toBottomOf="@+id/title" | ||||
|         tools:text="English" | ||||
|         tools:visibility="visible" /> | ||||
|  | ||||
| </androidx.constraintlayout.widget.ConstraintLayout> | ||||
| @@ -28,4 +28,12 @@ CREATE INDEX mangas_url_index ON mangas(url); | ||||
| getMangaById: | ||||
| SELECT * | ||||
| FROM mangas | ||||
| WHERE _id = :id; | ||||
| WHERE _id = :id; | ||||
|  | ||||
| getSourceIdWithFavoriteCount: | ||||
| SELECT | ||||
| source, | ||||
| count(*) | ||||
| FROM mangas | ||||
| WHERE favorite = 1 | ||||
| GROUP BY source; | ||||
		Reference in New Issue
	
	Block a user