Fix tracker for multiple branches

This commit is contained in:
Koitharu
2022-08-04 11:32:50 +03:00
parent 14f5d5daa4
commit ffbe05b2ae
10 changed files with 113 additions and 82 deletions

View File

@@ -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<Manga>.ids() = mapToSet { it.id }
fun Collection<Manga>.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
}

View File

@@ -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<ViewGroup.MarginLayoutParams> {
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)
}

View File

@@ -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<ChapterListItem>?) {
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,
)
}
},
)
}

View File

@@ -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<MangaChapter>?): 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
}
}

View File

@@ -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()) }
}

View File

@@ -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)
}
}
}

View File

@@ -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<FragmentFeedBinding>(),
@@ -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()
}

View File

@@ -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<List<TrackingLogItem>?>(null)
@@ -32,7 +33,7 @@ class FeedViewModel(
val onFeedCleared = SingleLiveEvent<Unit>()
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 {

View File

@@ -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" />