diff --git a/.editorconfig b/.editorconfig index e99fe5d60..e39c63dcd 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,5 +15,6 @@ disabled_rules=no-wildcard-imports,no-unused-imports ij_continuation_indent_size = 4 [{*.kt,*.kts}] +ij_kotlin_allow_trailing_comma_on_call_site = true ij_kotlin_allow_trailing_comma = true ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/Manga.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/Manga.kt index ed5594c42..3097f0ac3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/model/Manga.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/model/Manga.kt @@ -1,6 +1,34 @@ package org.koitharu.kotatsu.core.model +import androidx.core.os.LocaleListCompat import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.util.mapToSet +import org.koitharu.kotatsu.parsers.util.toTitleCase +import org.koitharu.kotatsu.utils.ext.iterator -fun Collection.ids() = mapToSet { it.id } \ No newline at end of file +fun Collection.ids() = mapToSet { it.id } + +fun Manga.getPreferredBranch(history: MangaHistory?): String? { + val ch = chapters + if (ch.isNullOrEmpty()) { + return null + } + if (history != null) { + val currentChapter = ch.find { it.id == history.chapterId } + if (currentChapter != null) { + return currentChapter.branch + } + } + val groups = ch.groupBy { it.branch } + for (locale in LocaleListCompat.getAdjustedDefault()) { + var language = locale.getDisplayLanguage(locale).toTitleCase(locale) + if (groups.containsKey(language)) { + return language + } + language = locale.getDisplayName(locale).toTitleCase(locale) + if (groups.containsKey(language)) { + return language + } + } + return groups.maxByOrNull { it.value.size }?.key +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt index b82206eba..ee07a41d0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt @@ -105,7 +105,7 @@ class DetailsActivity : Toast.makeText( this, getString(R.string._s_deleted_from_local_storage, manga.title), - Toast.LENGTH_SHORT + Toast.LENGTH_SHORT, ).show() finishAfterTransition() } @@ -131,7 +131,7 @@ class DetailsActivity : onActionClick = { e.report("DetailsActivity::onError") dismiss() - } + }, ) } else -> { @@ -142,14 +142,14 @@ class DetailsActivity : override fun onWindowInsetsChanged(insets: Insets) { binding.snackbar.updatePadding( - bottom = insets.bottom + bottom = insets.bottom, ) binding.toolbar.updateLayoutParams { topMargin = insets.top } binding.root.updatePadding( left = insets.left, - right = insets.right + right = insets.right, ) } @@ -159,6 +159,7 @@ class DetailsActivity : tab.removeBadge() } else { val badge = tab.orCreateBadge + badge.maxCharacterCount = 3 badge.number = newChapters badge.isVisible = true } @@ -275,8 +276,8 @@ class DetailsActivity : ReaderActivity.newIntent( context = this@DetailsActivity, manga = remoteManga, - state = ReaderState(chapterId, 0, 0) - ) + state = ReaderState(chapterId, 0, 0), + ), ) } setNeutralButton(R.string.download) { _, _ -> @@ -350,8 +351,8 @@ class DetailsActivity : dialogBuilder.setMessage( getString( R.string.large_manga_save_confirm, - resources.getQuantityString(R.plurals.chapters, chaptersCount, chaptersCount) - ) + resources.getQuantityString(R.plurals.chapters, chaptersCount, chaptersCount), + ), ).setPositiveButton(R.string.save) { _, _ -> DownloadService.start(this, manga) } diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt index 6b2b1afac..21c6541ea 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt @@ -2,7 +2,6 @@ package org.koitharu.kotatsu.details.ui import android.app.ActivityOptions import android.os.Bundle -import android.text.Spanned import android.text.method.LinkMovementMethod import android.view.* import androidx.appcompat.widget.PopupMenu @@ -10,18 +9,15 @@ import androidx.core.content.ContextCompat import androidx.core.graphics.Insets import androidx.core.net.toFile import androidx.core.net.toUri -import androidx.core.text.parseAsHtml import androidx.core.view.MenuProvider import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.view.updatePadding import coil.ImageLoader import coil.request.ImageRequest -import coil.size.Scale import coil.util.CoilUtils import com.google.android.material.chip.Chip import kotlinx.coroutines.launch -import org.koin.android.ext.android.get import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.sharedViewModel import org.koitharu.kotatsu.R @@ -33,6 +29,7 @@ import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.ui.BookmarksAdapter import org.koitharu.kotatsu.core.model.MangaHistory import org.koitharu.kotatsu.databinding.FragmentDetailsBinding +import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.details.ui.scrobbling.ScrobblingInfoBottomSheet import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet import org.koitharu.kotatsu.history.domain.PROGRESS_NONE @@ -82,6 +79,7 @@ class DetailsFragment : viewModel.bookmarks.observe(viewLifecycleOwner, ::onBookmarksChanged) viewModel.scrobblingInfo.observe(viewLifecycleOwner, ::onScrobblingInfoChanged) viewModel.description.observe(viewLifecycleOwner, ::onDescriptionChanged) + viewModel.chapters.observe(viewLifecycleOwner, ::onChaptersChanged) addMenuProvider(DetailsMenuProvider()) } @@ -126,18 +124,6 @@ class DetailsFragment : else -> textViewState.isVisible = false } - // Info containers - val chapters = manga.chapters - if (chapters.isNullOrEmpty()) { - infoLayout.textViewChapters.isVisible = false - } else { - infoLayout.textViewChapters.isVisible = true - infoLayout.textViewChapters.text = resources.getQuantityString( - R.plurals.chapters, - chapters.size, - chapters.size, - ) - } if (manga.hasRating) { infoLayout.textViewRating.text = String.format("%.1f", manga.rating * 5) infoLayout.ratingContainer.isVisible = true @@ -164,14 +150,27 @@ class DetailsFragment : infoLayout.textViewNsfw.isVisible = manga.isNsfw - // Buttons - buttonRead.isEnabled = !manga.chapters.isNullOrEmpty() - // Chips bindTags(manga) } } + private fun onChaptersChanged(chapters: List?) { + val infoLayout = binding.infoLayout + if (chapters.isNullOrEmpty()) { + infoLayout.textViewChapters.isVisible = false + } else { + infoLayout.textViewChapters.isVisible = true + infoLayout.textViewChapters.text = resources.getQuantityString( + R.plurals.chapters, + chapters.size, + chapters.size, + ) + } + // Buttons + binding.buttonRead.isEnabled = !chapters.isNullOrEmpty() + } + private fun onDescriptionChanged(description: CharSequence?) { if (description.isNullOrBlank()) { binding.textViewDescription.setText(R.string.no_description) @@ -266,7 +265,7 @@ class DetailsFragment : context = context ?: return, manga = manga, branch = viewModel.selectedBranchValue, - ) + ), ) } } @@ -276,14 +275,14 @@ class DetailsFragment : context = v.context, source = manga.source, query = manga.author ?: return, - ) + ), ) } R.id.imageView_cover -> { val options = ActivityOptions.makeScaleUpAnimation(v, 0, 0, v.width, v.height) startActivity( ImageActivity.newIntent(v.context, manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl }), - options.toBundle() + options.toBundle(), ) } } @@ -309,8 +308,8 @@ class DetailsFragment : c.chapter.branch == branch }?.let { c -> ReaderState(c.chapter.id, 0, 0) - } - ) + }, + ), ) true } @@ -343,7 +342,7 @@ class DetailsFragment : icon = 0, data = tag, ) - } + }, ) } diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt index 3a4eca7ca..9ad1accfa 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt @@ -1,11 +1,11 @@ package org.koitharu.kotatsu.details.ui -import androidx.core.os.LocaleListCompat import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.base.domain.MangaIntent import org.koitharu.kotatsu.core.model.MangaHistory +import org.koitharu.kotatsu.core.model.getPreferredBranch import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.details.ui.model.ChapterListItem @@ -17,8 +17,6 @@ import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.mapToSet -import org.koitharu.kotatsu.parsers.util.toTitleCase -import org.koitharu.kotatsu.utils.ext.iterator import org.koitharu.kotatsu.utils.ext.printStackTraceDebug class MangaDetailsDelegate( @@ -45,12 +43,7 @@ class MangaDetailsDelegate( manga = MangaRepository(manga.source).getDetails(manga) // find default branch val hist = historyRepository.getOne(manga) - selectedBranch.value = if (hist != null) { - val currentChapter = manga.chapters?.find { it.id == hist.chapterId } - if (currentChapter != null) currentChapter.branch else predictBranch(manga.chapters) - } else { - predictBranch(manga.chapters) - } + selectedBranch.value = manga.getPreferredBranch(hist) mangaData.value = manga relatedManga.value = runCatching { if (manga.source == MangaSource.LOCAL) { @@ -163,22 +156,4 @@ class MangaDetailsDelegate( } return result } - - private fun predictBranch(chapters: List?): String? { - if (chapters.isNullOrEmpty()) { - return null - } - val groups = chapters.groupBy { it.branch } - for (locale in LocaleListCompat.getAdjustedDefault()) { - var language = locale.getDisplayLanguage(locale).toTitleCase(locale) - if (groups.containsKey(language)) { - return language - } - language = locale.getDisplayName(locale).toTitleCase(locale) - if (groups.containsKey(language)) { - return language - } - } - return groups.maxByOrNull { it.value.size }?.key - } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/TrackerModule.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/TrackerModule.kt index e15791927..fd6ab2ff6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/TrackerModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/TrackerModule.kt @@ -14,7 +14,7 @@ val trackerModule factory { TrackingRepository(get()) } factory { TrackerNotificationChannels(androidContext(), get()) } - factory { Tracker(get(), get(), get()) } + factory { Tracker(get(), get(), get(), get()) } viewModel { FeedViewModel(get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/domain/Tracker.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/domain/Tracker.kt index e93902485..a585264d7 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/domain/Tracker.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/domain/Tracker.kt @@ -1,8 +1,10 @@ package org.koitharu.kotatsu.tracker.domain import androidx.annotation.VisibleForTesting +import org.koitharu.kotatsu.core.model.getPreferredBranch import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.tracker.domain.model.MangaTracking import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates @@ -12,6 +14,7 @@ import org.koitharu.kotatsu.tracker.work.TrackingItem class Tracker( private val settings: AppSettings, private val repository: TrackingRepository, + private val historyRepository: HistoryRepository, private val channels: TrackerNotificationChannels, ) { @@ -68,7 +71,7 @@ class Tracker( suspend fun fetchUpdates(track: MangaTracking, commit: Boolean): MangaUpdates { val manga = MangaRepository(track.manga.source).getDetails(track.manga) - val updates = compare(track, manga) + val updates = compare(track, manga, getBranch(manga)) if (commit) { repository.saveUpdates(updates) } @@ -78,7 +81,7 @@ class Tracker( @VisibleForTesting suspend fun checkUpdates(manga: Manga, commit: Boolean): MangaUpdates { val track = repository.getTrack(manga) - val updates = compare(track, manga) + val updates = compare(track, manga, getBranch(manga)) if (commit) { repository.saveUpdates(updates) } @@ -90,25 +93,30 @@ class Tracker( repository.deleteTrack(mangaId) } + private suspend fun getBranch(manga: Manga): String? { + val history = historyRepository.getOne(manga) + return manga.getPreferredBranch(history) + } + /** * The main functionality of tracker: check new chapters in [manga] comparing to the [track] */ - private fun compare(track: MangaTracking, manga: Manga): MangaUpdates { + private fun compare(track: MangaTracking, manga: Manga, branch: String?): MangaUpdates { if (track.isEmpty()) { // first check or manga was empty on last check return MangaUpdates(manga, emptyList(), isValid = false) } - val chapters = requireNotNull(manga.chapters) + val chapters = requireNotNull(manga.getChapters(branch)) val newChapters = chapters.takeLastWhile { x -> x.id != track.lastChapterId } return when { newChapters.isEmpty() -> { - return MangaUpdates(manga, emptyList(), isValid = chapters.lastOrNull()?.id == track.lastChapterId) + MangaUpdates(manga, emptyList(), isValid = chapters.lastOrNull()?.id == track.lastChapterId) } newChapters.size == chapters.size -> { - return MangaUpdates(manga, emptyList(), isValid = false) + MangaUpdates(manga, emptyList(), isValid = false) } else -> { - return MangaUpdates(manga, newChapters, isValid = true) + MangaUpdates(manga, newChapters, isValid = true) } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt index 3bcc46ea7..6bf0c7052 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt @@ -18,6 +18,7 @@ import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.list.ui.adapter.MangaListListener import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.main.ui.AppBarOwner +import org.koitharu.kotatsu.main.ui.MainActivity import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.tracker.ui.adapter.FeedAdapter @@ -25,6 +26,7 @@ import org.koitharu.kotatsu.tracker.work.TrackWorker import org.koitharu.kotatsu.utils.ext.addMenuProvider import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.measureHeight +import org.koitharu.kotatsu.utils.ext.resolveDp class FeedFragment : BaseFragment(), @@ -39,7 +41,7 @@ class FeedFragment : override fun onInflateView( inflater: LayoutInflater, - container: ViewGroup? + container: ViewGroup?, ) = FragmentFeedBinding.inflate(inflater, container, false) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -54,7 +56,7 @@ class FeedFragment : paddingVertical = resources.getDimensionPixelOffset(R.dimen.grid_spacing_outer) val decoration = TypedSpacingItemDecoration( FeedAdapter.ITEM_TYPE_FEED to 0, - fallbackSpacing = spacing + fallbackSpacing = spacing, ) addItemDecoration(decoration) } @@ -77,12 +79,25 @@ class FeedFragment : override fun onWindowInsetsChanged(insets: Insets) { val headerHeight = (activity as? AppBarOwner)?.appBar?.measureHeight() ?: insets.top - binding.recyclerView.updatePadding( - top = headerHeight + paddingVertical, - left = insets.left + paddingHorizontal, - right = insets.right + paddingHorizontal, - bottom = insets.bottom + paddingVertical, + binding.root.updatePadding( + left = insets.left, + right = insets.right, ) + if (activity is MainActivity) { + binding.recyclerView.updatePadding( + top = headerHeight, + bottom = insets.bottom, + ) + binding.swipeRefreshLayout.setProgressViewOffset( + true, + headerHeight + resources.resolveDp(-72), + headerHeight + resources.resolveDp(10), + ) + } else { + binding.recyclerView.updatePadding( + bottom = insets.bottom, + ) + } } override fun onRetryClick(error: Throwable) = Unit @@ -101,7 +116,7 @@ class FeedFragment : Snackbar.make( binding.recyclerView, R.string.updates_feed_cleared, - Snackbar.LENGTH_LONG + Snackbar.LENGTH_LONG, ).show() } @@ -109,7 +124,7 @@ class FeedFragment : Snackbar.make( binding.recyclerView, e.getDisplayMessage(resources), - Snackbar.LENGTH_SHORT + Snackbar.LENGTH_SHORT, ).show() } diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedViewModel.kt index 11ae7f6fa..e1d51c294 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedViewModel.kt @@ -16,12 +16,13 @@ import org.koitharu.kotatsu.list.ui.model.* import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem import org.koitharu.kotatsu.tracker.ui.model.toFeedItem +import org.koitharu.kotatsu.tracker.work.TrackWorker import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.daysDiff class FeedViewModel( - private val repository: TrackingRepository + private val repository: TrackingRepository, ) : BaseViewModel() { private val logList = MutableStateFlow?>(null) @@ -32,7 +33,7 @@ class FeedViewModel( val onFeedCleared = SingleLiveEvent() val content = combine( logList.filterNotNull(), - hasNextPage + hasNextPage, ) { list, isHasNextPage -> buildList(list.size + 2) { if (list.isEmpty()) { @@ -43,7 +44,7 @@ class FeedViewModel( textPrimary = R.string.text_empty_holder_primary, textSecondary = R.string.text_feed_holder, actionStringRes = 0, - ) + ), ) } else { list.mapListTo(this) @@ -54,7 +55,7 @@ class FeedViewModel( } }.asLiveDataDistinct( viewModelScope.coroutineContext + Dispatchers.Default, - listOf(header, LoadingState) + listOf(header, LoadingState), ) init { diff --git a/app/src/main/res/layout/activity_details.xml b/app/src/main/res/layout/activity_details.xml index 8190e9c82..078fe60cf 100644 --- a/app/src/main/res/layout/activity_details.xml +++ b/app/src/main/res/layout/activity_details.xml @@ -29,6 +29,9 @@ android:layout_height="match_parent" android:layout_gravity="center_horizontal" android:background="@null" + android:clipChildren="false" + android:clipToPadding="false" + android:paddingHorizontal="@dimen/margin_normal" app:tabGravity="center" app:tabMode="scrollable" />