Compare commits
98 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d62ecdc177 | ||
|
|
77cd7dda5f | ||
|
|
bd7099e97c | ||
|
|
b9457a35b9 | ||
|
|
53918ddddd | ||
|
|
84f0da0871 | ||
|
|
11c2e2e3bc | ||
|
|
622c8d1c18 | ||
|
|
10ffae7d4e | ||
|
|
15b48fd902 | ||
|
|
e2d7f2890d | ||
|
|
af510beb7b | ||
|
|
8cf0203b42 | ||
|
|
ea4a81c6ec | ||
|
|
63b53d2244 | ||
|
|
aba6b64074 | ||
|
|
324bfc733b | ||
|
|
fcfb3c9808 | ||
|
|
4ab77064ee | ||
|
|
ca2182d588 | ||
|
|
5ba410acd5 | ||
|
|
06382649c4 | ||
|
|
4f50e905af | ||
|
|
822cdab6ee | ||
|
|
8fad307c9a | ||
|
|
daa545f3db | ||
|
|
56892aea3c | ||
|
|
73e768def0 | ||
|
|
19da2267d6 | ||
|
|
3affec0f88 | ||
|
|
448c688629 | ||
|
|
fc2ab3f795 | ||
|
|
e520e695f9 | ||
|
|
b34f438430 | ||
|
|
72bfe15728 | ||
|
|
60198bc878 | ||
|
|
631c09badb | ||
|
|
2bf6eb6f0e | ||
|
|
1ee8b65ff7 | ||
|
|
d367750331 | ||
|
|
6d1bc5b1fd | ||
|
|
45771adef0 | ||
|
|
d91f613c28 | ||
|
|
988dd767d8 | ||
|
|
d715c175b8 | ||
|
|
a114605be1 | ||
|
|
f7a9e2ef89 | ||
|
|
2aa3133c52 | ||
|
|
2f15ea213d | ||
|
|
19a3f14190 | ||
|
|
fb716d300e | ||
|
|
1fe5095654 | ||
|
|
820d3f2413 | ||
|
|
34903fc951 | ||
|
|
7ec2e0c5cc | ||
|
|
846c346a86 | ||
|
|
f685ed6932 | ||
|
|
98b8ec5c89 | ||
|
|
0e20bf4afe | ||
|
|
fe588c08e2 | ||
|
|
3ee6ac605d | ||
|
|
535feb424c | ||
|
|
2cca696808 | ||
|
|
b5ea0ec7fa | ||
|
|
1e3d2595cf | ||
|
|
960b960726 | ||
|
|
cd29760836 | ||
|
|
27a2883f0a | ||
|
|
326bca2273 | ||
|
|
b32487fcb8 | ||
|
|
105bdff9ab | ||
|
|
6b767523a9 | ||
|
|
396050c051 | ||
|
|
c32d1877ff | ||
|
|
df78d9bf4c | ||
|
|
cc3bea3b2c | ||
|
|
87aa38b4e8 | ||
|
|
ee0215511a | ||
|
|
bd0056394e | ||
|
|
76ea7ab046 | ||
|
|
3d7ea1637f | ||
|
|
4b30905f9c | ||
|
|
bddb8431c5 | ||
|
|
61e02dd827 | ||
|
|
ff4eac8269 | ||
|
|
32eba77639 | ||
|
|
09eb82ca2e | ||
|
|
4d7ff5f6cc | ||
|
|
59dd53c025 | ||
|
|
c98d7561b8 | ||
|
|
5c8157b81f | ||
|
|
7f5ff1ab14 | ||
|
|
018c84b6af | ||
|
|
b95174727a | ||
|
|
0aec2359cf | ||
|
|
62bd5008fd | ||
|
|
89dd7beafe | ||
|
|
cecf3617af |
@@ -16,8 +16,8 @@ android {
|
||||
applicationId 'org.koitharu.kotatsu'
|
||||
minSdk = 21
|
||||
targetSdk = 34
|
||||
versionCode = 633
|
||||
versionName = '6.8.3'
|
||||
versionCode = 636
|
||||
versionName = '7.0-b2'
|
||||
generatedDensities = []
|
||||
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
|
||||
ksp {
|
||||
@@ -82,7 +82,7 @@ afterEvaluate {
|
||||
}
|
||||
dependencies {
|
||||
//noinspection GradleDependency
|
||||
implementation('com.github.KotatsuApp:kotatsu-parsers:44ea9fe709') {
|
||||
implementation('com.github.KotatsuApp:kotatsu-parsers:a245574dee') {
|
||||
exclude group: 'org.json', module: 'json'
|
||||
}
|
||||
|
||||
@@ -91,8 +91,8 @@ dependencies {
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0'
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.activity:activity-ktx:1.8.2'
|
||||
implementation 'androidx.core:core-ktx:1.13.0'
|
||||
implementation 'androidx.activity:activity-ktx:1.9.0'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.6.2'
|
||||
implementation 'androidx.collection:collection-ktx:1.4.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
|
||||
@@ -104,7 +104,7 @@ dependencies {
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
|
||||
implementation 'com.google.android.material:material:1.12.0-beta01'
|
||||
implementation 'com.google.android.material:material:1.12.0-rc01'
|
||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.7.0'
|
||||
implementation 'androidx.webkit:webkit:1.10.0'
|
||||
|
||||
@@ -145,7 +145,7 @@ dependencies {
|
||||
|
||||
implementation 'org.conscrypt:conscrypt-android:2.5.2'
|
||||
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.13'
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.14'
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.json:json:20240303'
|
||||
|
||||
@@ -174,12 +174,12 @@ class TrackerTest {
|
||||
assertEquals(3, repository.getNewChaptersCount(mangaFirst.id))
|
||||
|
||||
var chapter = requireNotNull(mangaFull.chapters).run { get(lastIndex - 1) }
|
||||
repository.syncWithHistory(mangaFull, chapter.id)
|
||||
tracker.syncWithHistory(mangaFull, chapter.id)
|
||||
|
||||
assertEquals(1, repository.getNewChaptersCount(mangaFirst.id))
|
||||
|
||||
chapter = requireNotNull(mangaFull.chapters).run { get(lastIndex) }
|
||||
repository.syncWithHistory(mangaFull, chapter.id)
|
||||
tracker.syncWithHistory(mangaFull, chapter.id)
|
||||
|
||||
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
|
||||
|
||||
|
||||
12
app/src/debug/AndroidManifest.xml
Normal file
12
app/src/debug/AndroidManifest.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
|
||||
<activity
|
||||
android:name=".tracker.ui.debug.TrackerDebugActivity"
|
||||
android:label="@string/check_for_new_chapters" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,72 @@
|
||||
package org.koitharu.kotatsu.tracker.ui.debug
|
||||
|
||||
import android.graphics.Color
|
||||
import android.text.format.DateUtils
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.text.bold
|
||||
import androidx.core.text.buildSpannedString
|
||||
import androidx.core.text.color
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import coil.ImageLoader
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
|
||||
import org.koitharu.kotatsu.core.util.ext.drawableStart
|
||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||
import org.koitharu.kotatsu.core.util.ext.source
|
||||
import org.koitharu.kotatsu.databinding.ItemTrackDebugBinding
|
||||
import org.koitharu.kotatsu.tracker.data.TrackEntity
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
fun trackDebugAD(
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
coil: ImageLoader,
|
||||
clickListener: OnListItemClickListener<TrackDebugItem>,
|
||||
) = adapterDelegateViewBinding<TrackDebugItem, TrackDebugItem, ItemTrackDebugBinding>(
|
||||
{ layoutInflater, parent -> ItemTrackDebugBinding.inflate(layoutInflater, parent, false) },
|
||||
) {
|
||||
val indicatorNew = ContextCompat.getDrawable(context, R.drawable.ic_new)
|
||||
|
||||
itemView.setOnClickListener { v ->
|
||||
clickListener.onItemClick(item, v)
|
||||
}
|
||||
|
||||
bind {
|
||||
binding.imageViewCover.newImageRequest(lifecycleOwner, item.manga.coverUrl)?.run {
|
||||
defaultPlaceholders(context)
|
||||
allowRgb565(true)
|
||||
source(item.manga.source)
|
||||
enqueueWith(coil)
|
||||
}
|
||||
binding.textViewTitle.text = item.manga.title
|
||||
binding.textViewSummary.text = buildSpannedString {
|
||||
item.lastCheckTime?.let {
|
||||
append(
|
||||
DateUtils.getRelativeDateTimeString(
|
||||
context,
|
||||
it.toEpochMilli(),
|
||||
DateUtils.MINUTE_IN_MILLIS,
|
||||
DateUtils.WEEK_IN_MILLIS,
|
||||
0,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (item.lastResult == TrackEntity.RESULT_FAILED) {
|
||||
append(" - ")
|
||||
bold {
|
||||
color(context.getThemeColor(materialR.attr.colorError, Color.RED)) {
|
||||
append(getString(R.string.error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
binding.textViewTitle.drawableStart = if (item.newChapters > 0) {
|
||||
indicatorNew
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.koitharu.kotatsu.tracker.ui.debug
|
||||
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import java.time.Instant
|
||||
|
||||
data class TrackDebugItem(
|
||||
val manga: Manga,
|
||||
val lastChapterId: Long,
|
||||
val newChapters: Int,
|
||||
val lastCheckTime: Instant?,
|
||||
val lastChapterDate: Instant?,
|
||||
val lastResult: Int,
|
||||
) : ListModel {
|
||||
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
return other is TrackDebugItem && other.manga.id == manga.id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package org.koitharu.kotatsu.tracker.ui.debug
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.updatePadding
|
||||
import coil.ImageLoader
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.databinding.ActivityTrackerDebugBinding
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class TrackerDebugActivity : BaseActivity<ActivityTrackerDebugBinding>(), OnListItemClickListener<TrackDebugItem> {
|
||||
|
||||
@Inject
|
||||
lateinit var coil: ImageLoader
|
||||
|
||||
private val viewModel by viewModels<TrackerDebugViewModel>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(ActivityTrackerDebugBinding.inflate(layoutInflater))
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
val tracksAdapter = BaseListAdapter<TrackDebugItem>()
|
||||
.addDelegate(ListItemType.FEED, trackDebugAD(this, coil, this))
|
||||
with(viewBinding.recyclerView) {
|
||||
adapter = tracksAdapter
|
||||
addItemDecoration(TypedListSpacingDecoration(context, false))
|
||||
}
|
||||
viewModel.content.observe(this, tracksAdapter)
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
val rv = viewBinding.recyclerView
|
||||
rv.updatePadding(
|
||||
left = insets.left + rv.paddingTop,
|
||||
right = insets.right + rv.paddingTop,
|
||||
bottom = insets.bottom,
|
||||
)
|
||||
viewBinding.toolbar.updatePadding(
|
||||
left = insets.left,
|
||||
right = insets.right,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onItemClick(item: TrackDebugItem, view: View) {
|
||||
startActivity(DetailsActivity.newIntent(this, item.manga))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.koitharu.kotatsu.tracker.ui.debug
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.db.entity.toManga
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.util.ext.toInstantOrNull
|
||||
import org.koitharu.kotatsu.tracker.data.TrackWithManga
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class TrackerDebugViewModel @Inject constructor(
|
||||
private val db: MangaDatabase
|
||||
) : BaseViewModel() {
|
||||
|
||||
val content = db.getTracksDao().observeAll()
|
||||
.map { it.toUiList() }
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
private fun List<TrackWithManga>.toUiList(): List<TrackDebugItem> = map {
|
||||
TrackDebugItem(
|
||||
manga = it.manga.toManga(emptySet()),
|
||||
lastChapterId = it.track.lastChapterId,
|
||||
newChapters = it.track.newChapters,
|
||||
lastCheckTime = it.track.lastCheckTime.toInstantOrNull(),
|
||||
lastChapterDate = it.track.lastChapterDate.toInstantOrNull(),
|
||||
lastResult = it.track.lastResult,
|
||||
)
|
||||
}
|
||||
}
|
||||
44
app/src/debug/res/layout/activity_tracker_debug.xml
Normal file
44
app/src/debug/res/layout/activity_tracker_debug.xml
Normal file
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
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="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
android:id="@+id/collapsingToolbarLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
|
||||
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
|
||||
app:toolbarId="@id/toolbar">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:layout_collapseMode="pin" />
|
||||
|
||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/list_spacing_normal"
|
||||
android:scrollbars="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
|
||||
tools:listitem="@layout/item_track_debug" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -4,7 +4,7 @@
|
||||
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"
|
||||
android:layout_height="72dp"
|
||||
android:background="@drawable/list_selector"
|
||||
android:clipChildren="false">
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
|
||||
@@ -26,42 +28,29 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:drawablePadding="8dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||
app:layout_constraintBottom_toTopOf="@+id/textView_subtitle"
|
||||
app:layout_constraintEnd_toStartOf="@id/button_more"
|
||||
android:textAppearance="?attr/textAppearanceTitleSmall"
|
||||
app:layout_constraintBottom_toTopOf="@+id/textView_summary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/imageView_cover"
|
||||
app:layout_constraintTop_toTopOf="@+id/imageView_cover"
|
||||
tools:text="@tools:sample/lorem" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_subtitle"
|
||||
android:id="@+id/textView_summary"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/imageView_cover"
|
||||
app:layout_constraintEnd_toStartOf="@id/button_more"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/imageView_cover"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView_title"
|
||||
tools:text="@tools:sample/lorem/random" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_more"
|
||||
style="@style/Widget.Kotatsu.ExploreButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:gravity="center"
|
||||
android:minWidth="120dp"
|
||||
android:text="@string/more"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -8,4 +8,9 @@
|
||||
android:title="@string/leak_canary_display_activity_label"
|
||||
app:showAsAction="never" />
|
||||
|
||||
</menu>
|
||||
<item
|
||||
android:id="@id/action_tracker"
|
||||
android:title="@string/check_for_new_chapters"
|
||||
app:showAsAction="never" />
|
||||
|
||||
</menu>
|
||||
|
||||
@@ -94,34 +94,6 @@
|
||||
<data android:host="kotatsu.app" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.details.ui.DetailsActivity2"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="${applicationId}.action.VIEW_MANGA" />
|
||||
</intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="kotatsu.app" />
|
||||
<data android:path="/manga" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="kotatsu" />
|
||||
<data android:host="manga" />
|
||||
<data android:host="kotatsu.app" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.reader.ui.ReaderActivity"
|
||||
android:exported="true">
|
||||
@@ -281,6 +253,9 @@
|
||||
<service
|
||||
android:name="org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
<service
|
||||
android:name="org.koitharu.kotatsu.local.ui.ImportService"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
<service
|
||||
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetService"
|
||||
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
||||
|
||||
@@ -16,6 +16,7 @@ import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
|
||||
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
|
||||
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
|
||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||
import org.koitharu.kotatsu.core.util.ext.source
|
||||
@@ -79,9 +80,7 @@ fun alternativeAD(
|
||||
}
|
||||
binding.imageViewCover.newImageRequest(lifecycleOwner, item.manga.coverUrl)?.run {
|
||||
size(CoverSizeResolver(binding.imageViewCover))
|
||||
placeholder(R.drawable.ic_placeholder)
|
||||
fallback(R.drawable.ic_placeholder)
|
||||
error(R.drawable.ic_error_placeholder)
|
||||
defaultPlaceholders(context)
|
||||
transformations(TrimTransformation())
|
||||
allowRgb565(true)
|
||||
tag(item.manga)
|
||||
|
||||
@@ -30,7 +30,7 @@ import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.databinding.FragmentListSimpleBinding
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.list.ui.MangaListSpanResolver
|
||||
import org.koitharu.kotatsu.list.ui.GridSpanResolver
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListHeaderClickListener
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
|
||||
@@ -86,7 +86,7 @@ class BookmarksFragment :
|
||||
val spanSizeLookup = SpanSizeLookup()
|
||||
with(binding.recyclerView) {
|
||||
setHasFixedSize(true)
|
||||
val spanResolver = MangaListSpanResolver(resources)
|
||||
val spanResolver = GridSpanResolver(resources)
|
||||
addItemDecoration(TypedListSpacingDecoration(context, false))
|
||||
adapter = bookmarksAdapter
|
||||
addOnLayoutChangeListener(spanResolver)
|
||||
|
||||
@@ -3,12 +3,12 @@ package org.koitharu.kotatsu.bookmarks.ui.adapter
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import coil.ImageLoader
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
|
||||
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.util.ext.decodeRegion
|
||||
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
|
||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||
import org.koitharu.kotatsu.core.util.ext.source
|
||||
@@ -29,9 +29,7 @@ fun bookmarkListAD(
|
||||
bind {
|
||||
binding.imageViewThumb.newImageRequest(lifecycleOwner, item.imageLoadData)?.run {
|
||||
size(CoverSizeResolver(binding.imageViewThumb))
|
||||
placeholder(R.drawable.ic_placeholder)
|
||||
fallback(R.drawable.ic_placeholder)
|
||||
error(R.drawable.ic_error_placeholder)
|
||||
defaultPlaceholders(context)
|
||||
allowRgb565(true)
|
||||
tag(item)
|
||||
decodeRegion(item.scroll)
|
||||
|
||||
@@ -3,12 +3,12 @@ package org.koitharu.kotatsu.bookmarks.ui.sheet
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import coil.ImageLoader
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
|
||||
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.util.ext.decodeRegion
|
||||
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
|
||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||
import org.koitharu.kotatsu.core.util.ext.source
|
||||
@@ -30,9 +30,7 @@ fun bookmarkLargeAD(
|
||||
bind {
|
||||
binding.imageViewThumb.newImageRequest(lifecycleOwner, item.imageLoadData)?.run {
|
||||
size(CoverSizeResolver(binding.imageViewThumb))
|
||||
placeholder(R.drawable.ic_placeholder)
|
||||
fallback(R.drawable.ic_placeholder)
|
||||
error(R.drawable.ic_error_placeholder)
|
||||
defaultPlaceholders(context)
|
||||
allowRgb565(true)
|
||||
tag(item)
|
||||
decodeRegion(item.scroll)
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
package org.koitharu.kotatsu.bookmarks.ui.sheet
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import coil.ImageLoader
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetBehavior
|
||||
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetCallback
|
||||
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
|
||||
import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.core.util.ext.plus
|
||||
import org.koitharu.kotatsu.core.util.ext.showDistinct
|
||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||
import org.koitharu.kotatsu.databinding.SheetPagesBinding
|
||||
import org.koitharu.kotatsu.list.ui.MangaListSpanResolver
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||
import org.koitharu.kotatsu.reader.ui.thumbnails.OnPageSelectListener
|
||||
import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Deprecated("")
|
||||
@AndroidEntryPoint
|
||||
class BookmarksSheet :
|
||||
BaseAdaptiveSheet<SheetPagesBinding>(),
|
||||
AdaptiveSheetCallback,
|
||||
OnListItemClickListener<Bookmark> {
|
||||
|
||||
private val viewModel by viewModels<BookmarksSheetViewModel>()
|
||||
|
||||
@Inject
|
||||
lateinit var coil: ImageLoader
|
||||
|
||||
@Inject
|
||||
lateinit var settings: AppSettings
|
||||
|
||||
private var bookmarksAdapter: BookmarksAdapter? = null
|
||||
private var spanResolver: MangaListSpanResolver? = null
|
||||
|
||||
private val spanSizeLookup = SpanSizeLookup()
|
||||
private val listCommitCallback = Runnable {
|
||||
spanSizeLookup.invalidateCache()
|
||||
}
|
||||
|
||||
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetPagesBinding {
|
||||
return SheetPagesBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewBindingCreated(binding: SheetPagesBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
addSheetCallback(this)
|
||||
spanResolver = MangaListSpanResolver(binding.root.resources)
|
||||
bookmarksAdapter = BookmarksAdapter(
|
||||
coil = coil,
|
||||
lifecycleOwner = viewLifecycleOwner,
|
||||
clickListener = this@BookmarksSheet,
|
||||
headerClickListener = null,
|
||||
)
|
||||
viewBinding?.headerBar?.setTitle(R.string.bookmarks)
|
||||
with(binding.recyclerView) {
|
||||
addItemDecoration(TypedListSpacingDecoration(context, false))
|
||||
adapter = bookmarksAdapter
|
||||
addOnLayoutChangeListener(spanResolver)
|
||||
spanResolver?.setGridSize(settings.gridSize / 100f, this)
|
||||
(layoutManager as GridLayoutManager).spanSizeLookup = spanSizeLookup
|
||||
}
|
||||
viewModel.content.observe(viewLifecycleOwner, ::onThumbnailsChanged)
|
||||
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
spanResolver = null
|
||||
bookmarksAdapter = null
|
||||
spanSizeLookup.invalidateCache()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onItemClick(item: Bookmark, view: View) {
|
||||
val listener = (parentFragment as? OnPageSelectListener) ?: (activity as? OnPageSelectListener)
|
||||
if (listener != null) {
|
||||
listener.onPageSelected(ReaderPage(item.toMangaPage(), item.page, item.chapterId))
|
||||
} else {
|
||||
val intent = IntentBuilder(view.context)
|
||||
.manga(viewModel.manga)
|
||||
.bookmark(item)
|
||||
.incognito(true)
|
||||
.build()
|
||||
startActivity(intent)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
|
||||
override fun onStateChanged(sheet: View, newState: Int) {
|
||||
viewBinding?.recyclerView?.isFastScrollerEnabled = newState == AdaptiveSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
|
||||
private fun onThumbnailsChanged(list: List<ListModel>) {
|
||||
val adapter = bookmarksAdapter ?: return
|
||||
if (adapter.itemCount == 0) {
|
||||
var position = list.indexOfFirst { it is PageThumbnail && it.isCurrent }
|
||||
if (position > 0) {
|
||||
val spanCount = spanResolver?.spanCount ?: 0
|
||||
val offset = if (position > spanCount + 1) {
|
||||
(resources.getDimensionPixelSize(R.dimen.manga_list_details_item_height) * 0.6).roundToInt()
|
||||
} else {
|
||||
position = 0
|
||||
0
|
||||
}
|
||||
val scrollCallback = RecyclerViewScrollCallback(requireViewBinding().recyclerView, position, offset)
|
||||
adapter.setItems(list, listCommitCallback + scrollCallback)
|
||||
} else {
|
||||
adapter.setItems(list, listCommitCallback)
|
||||
}
|
||||
} else {
|
||||
adapter.setItems(list, listCommitCallback)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class SpanSizeLookup : GridLayoutManager.SpanSizeLookup() {
|
||||
|
||||
init {
|
||||
isSpanIndexCacheEnabled = true
|
||||
isSpanGroupIndexCacheEnabled = true
|
||||
}
|
||||
|
||||
override fun getSpanSize(position: Int): Int {
|
||||
val total = (viewBinding?.recyclerView?.layoutManager as? GridLayoutManager)?.spanCount ?: return 1
|
||||
return when (bookmarksAdapter?.getItemViewType(position)) {
|
||||
ListItemType.PAGE_THUMB.ordinal -> 1
|
||||
else -> total
|
||||
}
|
||||
}
|
||||
|
||||
fun invalidateCache() {
|
||||
invalidateSpanGroupIndexCache()
|
||||
invalidateSpanIndexCache()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val ARG_MANGA = "manga"
|
||||
|
||||
private const val TAG = "BookmarksSheet"
|
||||
|
||||
fun show(fm: FragmentManager, manga: Manga) {
|
||||
BookmarksSheet().withArgs(1) {
|
||||
putParcelable(ARG_MANGA, ParcelableManga(manga))
|
||||
}.showDistinct(fm, TAG)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
package org.koitharu.kotatsu.bookmarks.ui.sheet
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.util.ext.require
|
||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
||||
import org.koitharu.kotatsu.parsers.util.SuspendLazy
|
||||
import javax.inject.Inject
|
||||
|
||||
@Deprecated("")
|
||||
@HiltViewModel
|
||||
class BookmarksSheetViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
mangaRepositoryFactory: MangaRepository.Factory,
|
||||
bookmarksRepository: BookmarksRepository,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val manga = savedStateHandle.require<ParcelableManga>(BookmarksSheet.ARG_MANGA).manga
|
||||
private val chaptersLazy = SuspendLazy {
|
||||
requireNotNull(manga.chapters ?: mangaRepositoryFactory.create(manga.source).getDetails(manga).chapters)
|
||||
}
|
||||
|
||||
val content: StateFlow<List<ListModel>> = bookmarksRepository.observeBookmarks(manga)
|
||||
.map { mapList(it) }
|
||||
.withErrorHandling()
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, listOf(LoadingFooter()))
|
||||
|
||||
private suspend fun mapList(bookmarks: List<Bookmark>): List<ListModel> {
|
||||
val chapters = chaptersLazy.get()
|
||||
val bookmarksMap = bookmarks.groupBy { it.chapterId }
|
||||
val result = ArrayList<ListModel>(bookmarks.size + bookmarksMap.size)
|
||||
for (chapter in chapters) {
|
||||
val b = bookmarksMap[chapter.id]
|
||||
if (b.isNullOrEmpty()) {
|
||||
continue
|
||||
}
|
||||
result += ListHeader(chapter.name)
|
||||
result.addAll(b)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ class CaptchaNotifier(
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setDefaults(NotificationCompat.DEFAULT_SOUND)
|
||||
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||
.setGroup(GROUP_CAPTCHA)
|
||||
.setAutoCancel(true)
|
||||
.setVisibility(
|
||||
if (exception.source?.contentType == ContentType.HENTAI) {
|
||||
@@ -82,5 +83,6 @@ class CaptchaNotifier(
|
||||
private const val PARAM_IGNORE_CAPTCHA = "ignore_captcha"
|
||||
private const val CHANNEL_ID = "captcha"
|
||||
private const val TAG = CHANNEL_ID
|
||||
private const val GROUP_CAPTCHA = "org.koitharu.kotatsu.CAPTCHA"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,9 +40,9 @@ import org.koitharu.kotatsu.core.parser.favicon.FaviconFetcher
|
||||
import org.koitharu.kotatsu.core.ui.image.CoilImageGetter
|
||||
import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle
|
||||
import org.koitharu.kotatsu.core.util.AcraScreenLogger
|
||||
import org.koitharu.kotatsu.core.util.IncognitoModeIndicator
|
||||
import org.koitharu.kotatsu.core.util.ext.connectivityManager
|
||||
import org.koitharu.kotatsu.core.util.ext.isLowRamDevice
|
||||
import org.koitharu.kotatsu.details.ui.pager.pages.MangaPageFetcher
|
||||
import org.koitharu.kotatsu.local.data.CacheDir
|
||||
import org.koitharu.kotatsu.local.data.CbzFetcher
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||
@@ -50,7 +50,6 @@ import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||
import org.koitharu.kotatsu.main.domain.CoverRestoreInterceptor
|
||||
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.reader.ui.thumbnails.MangaPageFetcher
|
||||
import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
|
||||
import org.koitharu.kotatsu.settings.backup.BackupObserver
|
||||
import org.koitharu.kotatsu.sync.domain.SyncController
|
||||
@@ -147,12 +146,10 @@ interface AppModule {
|
||||
fun provideActivityLifecycleCallbacks(
|
||||
appProtectHelper: AppProtectHelper,
|
||||
activityRecreationHandle: ActivityRecreationHandle,
|
||||
incognitoModeIndicator: IncognitoModeIndicator,
|
||||
acraScreenLogger: AcraScreenLogger,
|
||||
): Set<@JvmSuppressWildcards Application.ActivityLifecycleCallbacks> = arraySetOf(
|
||||
appProtectHelper,
|
||||
activityRecreationHandle,
|
||||
incognitoModeIndicator,
|
||||
acraScreenLogger,
|
||||
)
|
||||
|
||||
|
||||
@@ -28,6 +28,9 @@ abstract class MangaDao {
|
||||
@Query("SELECT * FROM manga WHERE source = :source")
|
||||
abstract suspend fun findAllBySource(source: String): List<MangaWithTags>
|
||||
|
||||
@Query("SELECT author FROM manga WHERE author LIKE :query GROUP BY author ORDER BY COUNT(author) DESC LIMIT :limit")
|
||||
abstract suspend fun findAuthors(query: String, limit: Int): List<String>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM manga WHERE (title LIKE :query OR alt_title LIKE :query) AND manga_id IN (SELECT manga_id FROM favourites UNION SELECT manga_id FROM history) LIMIT :limit")
|
||||
abstract suspend fun searchByTitle(query: String, limit: Int): List<MangaWithTags>
|
||||
|
||||
@@ -16,9 +16,15 @@ interface TrackLogsDao {
|
||||
@Query("SELECT * FROM track_logs ORDER BY created_at DESC LIMIT :limit OFFSET 0")
|
||||
fun observeAll(limit: Int): Flow<List<TrackLogWithManga>>
|
||||
|
||||
@Query("SELECT COUNT(*) FROM track_logs WHERE unread = 1")
|
||||
fun observeUnreadCount(): Flow<Int>
|
||||
|
||||
@Query("DELETE FROM track_logs")
|
||||
suspend fun clear()
|
||||
|
||||
@Query("UPDATE track_logs SET unread = 0 WHERE id = :id")
|
||||
suspend fun markAsRead(id: Long)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(entity: TrackLogEntity): Long
|
||||
|
||||
|
||||
@@ -12,5 +12,7 @@ class Migration19To20 : Migration(19, 20) {
|
||||
db.execSQL("CREATE TABLE tracks (`manga_id` INTEGER NOT NULL, `last_chapter_id` INTEGER NOT NULL, `chapters_new` INTEGER NOT NULL, `last_check_time` INTEGER NOT NULL, `last_chapter_date` INTEGER NOT NULL, `last_result` INTEGER NOT NULL, PRIMARY KEY(`manga_id`), FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
|
||||
db.execSQL("INSERT INTO tracks SELECT manga_id, last_chapter_id, chapters_new, last_check AS last_check_time, 0 AS last_chapter_date, 0 AS last_result FROM tracks_bk")
|
||||
db.execSQL("DROP TABLE tracks_bk")
|
||||
|
||||
db.execSQL("ALTER TABLE track_logs ADD COLUMN `unread` INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.koitharu.kotatsu.core.exceptions
|
||||
|
||||
import okhttp3.Headers
|
||||
import okio.IOException
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
|
||||
class CloudFlareBlockedException(
|
||||
val url: String,
|
||||
val source: MangaSource?,
|
||||
) : IOException("Blocked by CloudFlare")
|
||||
@@ -4,6 +4,7 @@ import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import okhttp3.internal.closeQuietly
|
||||
import org.jsoup.Jsoup
|
||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareBlockedException
|
||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import java.net.HttpURLConnection.HTTP_FORBIDDEN
|
||||
@@ -17,14 +18,23 @@ class CloudFlareInterceptor : Interceptor {
|
||||
val content = response.body?.let { response.peekBody(Long.MAX_VALUE) }?.byteStream()?.use {
|
||||
Jsoup.parse(it, Charsets.UTF_8.name(), response.request.url.toString())
|
||||
} ?: return response
|
||||
if (content.getElementById("challenge-error-title") != null) {
|
||||
val hasCaptcha = content.getElementById("challenge-error-title") != null
|
||||
val isBlocked = content.selectFirst("h2[data-translate=\"blocked_why_headline\"]") != null
|
||||
if (hasCaptcha || isBlocked) {
|
||||
val request = response.request
|
||||
response.closeQuietly()
|
||||
throw CloudFlareProtectedException(
|
||||
url = request.url.toString(),
|
||||
source = request.tag(MangaSource::class.java),
|
||||
headers = request.headers,
|
||||
)
|
||||
if (isBlocked) {
|
||||
throw CloudFlareBlockedException(
|
||||
url = request.url.toString(),
|
||||
source = request.tag(MangaSource::class.java),
|
||||
)
|
||||
} else {
|
||||
throw CloudFlareProtectedException(
|
||||
url = request.url.toString(),
|
||||
source = request.tag(MangaSource::class.java),
|
||||
headers = request.headers,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return response
|
||||
|
||||
@@ -32,6 +32,7 @@ import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
|
||||
import java.io.File
|
||||
import java.net.Proxy
|
||||
import java.util.EnumSet
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
@@ -74,6 +75,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
val isNavLabelsVisible: Boolean
|
||||
get() = prefs.getBoolean(KEY_NAV_LABELS, true)
|
||||
|
||||
val isNavBarPinned: Boolean
|
||||
get() = prefs.getBoolean(KEY_NAV_PINNED, false)
|
||||
|
||||
var gridSize: Int
|
||||
get() = prefs.getInt(KEY_GRID_SIZE, 100)
|
||||
set(value) = prefs.edit { putInt(KEY_GRID_SIZE, value) }
|
||||
@@ -142,6 +146,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
val isTrackerWifiOnly: Boolean
|
||||
get() = prefs.getBoolean(KEY_TRACKER_WIFI_ONLY, false)
|
||||
|
||||
val trackerFrequencyFactor: Float
|
||||
get() = prefs.getString(KEY_TRACKER_FREQUENCY, null)?.toFloatOrNull() ?: 1f
|
||||
|
||||
val isTrackerNotificationsEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_TRACKER_NOTIFICATIONS, true)
|
||||
|
||||
@@ -172,6 +179,14 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
get() = prefs.getBoolean(KEY_HISTORY_GROUPING, true)
|
||||
set(value) = prefs.edit { putBoolean(KEY_HISTORY_GROUPING, value) }
|
||||
|
||||
var isUpdatedGroupingEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_UPDATED_GROUPING, true)
|
||||
set(value) = prefs.edit { putBoolean(KEY_UPDATED_GROUPING, value) }
|
||||
|
||||
var isFeedHeaderVisible: Boolean
|
||||
get() = prefs.getBoolean(KEY_FEED_HEADER, true)
|
||||
set(value) = prefs.edit { putBoolean(KEY_FEED_HEADER, value) }
|
||||
|
||||
val isReadingIndicatorsEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_READING_INDICATORS, true)
|
||||
|
||||
@@ -206,6 +221,13 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
get() = prefs.getBoolean(KEY_APP_PASSWORD_NUMERIC, false)
|
||||
set(value) = prefs.edit { putBoolean(KEY_APP_PASSWORD_NUMERIC, value) }
|
||||
|
||||
val searchSuggestionTypes: Set<SearchSuggestionType>
|
||||
get() = prefs.getStringSet(KEY_SEARCH_SUGGESTION_TYPES, null)?.let { stringSet ->
|
||||
stringSet.mapNotNullTo(EnumSet.noneOf(SearchSuggestionType::class.java)) { x ->
|
||||
enumValueOf<SearchSuggestionType>(x)
|
||||
}
|
||||
} ?: EnumSet.allOf(SearchSuggestionType::class.java)
|
||||
|
||||
val isLoggingEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_LOGGING_ENABLED, false)
|
||||
|
||||
@@ -230,11 +252,20 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
|
||||
val defaultDetailsTab: Int
|
||||
get() = if (isPagesTabEnabled) {
|
||||
prefs.getString(KEY_DETAILS_TAB, null)?.toIntOrNull()?.coerceIn(0, 1) ?: 0
|
||||
val raw = prefs.getString(KEY_DETAILS_TAB, null)?.toIntOrNull() ?: 0
|
||||
if (raw == -1) {
|
||||
lastDetailsTab
|
||||
} else {
|
||||
raw
|
||||
}.coerceIn(0, 2)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
var lastDetailsTab: Int
|
||||
get() = prefs.getInt(KEY_DETAILS_LAST_TAB, 0)
|
||||
set(value) = prefs.edit { putInt(KEY_DETAILS_LAST_TAB, value) }
|
||||
|
||||
val isContentPrefetchEnabled: Boolean
|
||||
get() {
|
||||
if (isBackgroundNetworkRestricted()) {
|
||||
@@ -388,9 +419,13 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
val isRelatedMangaEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_RELATED_MANGA, true)
|
||||
|
||||
val isWebtoonZoomEnable: Boolean
|
||||
val isWebtoonZoomEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_WEBTOON_ZOOM, true)
|
||||
|
||||
var isWebtoonGapsEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_WEBTOON_GAPS, false)
|
||||
set(value) = prefs.edit { putBoolean(KEY_WEBTOON_GAPS, value) }
|
||||
|
||||
@get:FloatRange(from = 0.0, to = 0.5)
|
||||
val defaultWebtoonZoomOut: Float
|
||||
get() = prefs.getInt(KEY_WEBTOON_ZOOM_OUT, 0).coerceIn(0, 50) / 100f
|
||||
@@ -541,6 +576,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_READER_VOLUME_BUTTONS = "reader_volume_buttons"
|
||||
const val KEY_TRACKER_ENABLED = "tracker_enabled"
|
||||
const val KEY_TRACKER_WIFI_ONLY = "tracker_wifi"
|
||||
const val KEY_TRACKER_FREQUENCY = "tracker_freq"
|
||||
const val KEY_TRACK_SOURCES = "track_sources"
|
||||
const val KEY_TRACK_CATEGORIES = "track_categories"
|
||||
const val KEY_TRACK_WARNING = "track_warning"
|
||||
@@ -566,6 +602,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_BACKUP_PERIODICAL_OUTPUT = "backup_periodic_output"
|
||||
const val KEY_BACKUP_PERIODICAL_LAST = "backup_periodic_last"
|
||||
const val KEY_HISTORY_GROUPING = "history_grouping"
|
||||
const val KEY_UPDATED_GROUPING = "updated_grouping"
|
||||
const val KEY_READING_INDICATORS = "reading_indicators"
|
||||
const val KEY_REVERSE_CHAPTERS = "reverse_chapters"
|
||||
const val KEY_GRID_VIEW_CHAPTERS = "grid_view_chapters"
|
||||
@@ -600,6 +637,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_LOCAL_LIST_ORDER = "local_order"
|
||||
const val KEY_HISTORY_ORDER = "history_order"
|
||||
const val KEY_FAVORITES_ORDER = "fav_order"
|
||||
const val KEY_WEBTOON_GAPS = "webtoon_gaps"
|
||||
const val KEY_WEBTOON_ZOOM = "webtoon_zoom"
|
||||
const val KEY_WEBTOON_ZOOM_OUT = "webtoon_zoom_out"
|
||||
const val KEY_PREFETCH_CONTENT = "prefetch_content"
|
||||
@@ -626,6 +664,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_RELATED_MANGA = "related_manga"
|
||||
const val KEY_NAV_MAIN = "nav_main"
|
||||
const val KEY_NAV_LABELS = "nav_labels"
|
||||
const val KEY_NAV_PINNED = "nav_pinned"
|
||||
const val KEY_32BIT_COLOR = "enhanced_colors"
|
||||
const val KEY_SOURCES_ORDER = "sources_sort_order"
|
||||
const val KEY_SOURCES_CATALOG = "sources_catalog"
|
||||
@@ -636,11 +675,14 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_IGNORE_DOZE = "ignore_dose"
|
||||
const val KEY_PAGES_TAB = "pages_tab"
|
||||
const val KEY_DETAILS_TAB = "details_tab"
|
||||
const val KEY_DETAILS_LAST_TAB = "details_last_tab"
|
||||
const val KEY_READING_TIME = "reading_time"
|
||||
const val KEY_PAGES_SAVE_DIR = "pages_dir"
|
||||
const val KEY_PAGES_SAVE_ASK = "pages_dir_ask"
|
||||
const val KEY_STATS_ENABLED = "stats_on"
|
||||
const val KEY_APP_UPDATE = "app_update"
|
||||
const val KEY_APP_TRANSLATION = "about_app_translation"
|
||||
const val KEY_FEED_HEADER = "feed_header"
|
||||
const val KEY_SEARCH_SUGGESTION_TYPES = "search_suggest_types"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,12 +17,13 @@ enum class NavItem(
|
||||
EXPLORE(R.id.nav_explore, R.string.explore, R.drawable.ic_explore_selector),
|
||||
SUGGESTIONS(R.id.nav_suggestions, R.string.suggestions, R.drawable.ic_suggestion_selector),
|
||||
FEED(R.id.nav_feed, R.string.feed, R.drawable.ic_feed_selector),
|
||||
UPDATED(R.id.nav_updated, R.string.updated, R.drawable.ic_updated_selector),
|
||||
BOOKMARKS(R.id.nav_bookmarks, R.string.bookmarks, R.drawable.ic_bookmark_selector),
|
||||
;
|
||||
|
||||
fun isAvailable(settings: AppSettings): Boolean = when (this) {
|
||||
SUGGESTIONS -> settings.isSuggestionsEnabled
|
||||
FEED -> settings.isTrackerEnabled
|
||||
UPDATED, FEED -> settings.isTrackerEnabled
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.koitharu.kotatsu.core.prefs
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import org.koitharu.kotatsu.R
|
||||
|
||||
enum class SearchSuggestionType(
|
||||
@StringRes val titleResId: Int,
|
||||
) {
|
||||
|
||||
GENRES(R.string.genres),
|
||||
QUERIES_RECENT(R.string.recent_queries),
|
||||
QUERIES_SUGGEST(R.string.suggested_queries),
|
||||
MANGA(R.string.content_type_manga),
|
||||
SOURCES(R.string.remote_sources),
|
||||
AUTHORS(R.string.authors),
|
||||
}
|
||||
@@ -72,7 +72,7 @@ abstract class BaseActivity<B : ViewBinding> :
|
||||
onBackPressedDispatcher.addCallback(actionModeDelegate)
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
putDataToExtras(intent)
|
||||
super.onNewIntent(intent)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,12 @@ import com.hannesdorfmann.adapterdelegates4.AdapterDelegate
|
||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.asExecutor
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import org.koitharu.kotatsu.core.util.ContinuationResumeRunnable
|
||||
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||
@@ -48,4 +53,14 @@ open class BaseListAdapter<T : ListModel> : AsyncListDifferDelegationAdapter<T>(
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun observeItems(): Flow<List<T>> = callbackFlow {
|
||||
val listListener = ListListener<T> { _, list ->
|
||||
trySendBlocking(list)
|
||||
}
|
||||
addListListener(listListener)
|
||||
awaitClose { removeListListener(listListener) }
|
||||
}.onStart {
|
||||
emit(items)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package org.koitharu.kotatsu.core.ui
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.annotation.AnyThread
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
@@ -39,8 +41,10 @@ abstract class CoroutineIntentService : BaseService() {
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
protected abstract suspend fun processIntent(startId: Int, intent: Intent)
|
||||
|
||||
@AnyThread
|
||||
protected abstract fun onError(startId: Int, error: Throwable)
|
||||
|
||||
private fun errorHandler(startId: Int) = CoroutineExceptionHandler { _, throwable ->
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package org.koitharu.kotatsu.core.ui.image
|
||||
|
||||
import android.animation.TimeAnimator
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.ColorFilter
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||
import com.google.android.material.animation.ArgbEvaluatorCompat
|
||||
import org.koitharu.kotatsu.core.util.ext.animatorDurationScale
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||
import kotlin.math.abs
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
class AnimatedPlaceholderDrawable(context: Context) : Drawable(), Animatable, TimeAnimator.TimeListener {
|
||||
|
||||
private val colorLow = context.getThemeColor(materialR.attr.colorBackgroundFloating)
|
||||
private val colorHigh = context.getThemeColor(materialR.attr.colorSurfaceContainer)
|
||||
private var currentColor: Int = colorLow
|
||||
private var alpha: Int = 255
|
||||
private val interpolator = FastOutSlowInInterpolator()
|
||||
private val period = 2000 * context.animatorDurationScale
|
||||
private val timeAnimator = TimeAnimator()
|
||||
|
||||
init {
|
||||
timeAnimator.setTimeListener(this)
|
||||
updateColor()
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
if (!isRunning) {
|
||||
updateColor()
|
||||
start()
|
||||
}
|
||||
canvas.drawColor(currentColor)
|
||||
}
|
||||
|
||||
override fun setAlpha(alpha: Int) {
|
||||
this.alpha = alpha
|
||||
}
|
||||
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) {
|
||||
throw UnsupportedOperationException("ColorFilter is not supported by PlaceholderDrawable")
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun getOpacity(): Int = PixelFormat.OPAQUE
|
||||
|
||||
override fun getAlpha(): Int = alpha
|
||||
|
||||
override fun onTimeUpdate(animation: TimeAnimator?, totalTime: Long, deltaTime: Long) {
|
||||
if (callback != null) {
|
||||
updateColor()
|
||||
invalidateSelf()
|
||||
}
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
timeAnimator.start()
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
timeAnimator.cancel()
|
||||
}
|
||||
|
||||
override fun isRunning(): Boolean = timeAnimator.isStarted
|
||||
|
||||
private fun updateColor() {
|
||||
val ph = period / 2
|
||||
val fraction = abs((System.currentTimeMillis() % period) - ph) / ph.toFloat()
|
||||
var color = ArgbEvaluatorCompat.getInstance()
|
||||
.evaluate(interpolator.getInterpolation(fraction), colorLow, colorHigh)
|
||||
if (alpha != 255) {
|
||||
color = ColorUtils.setAlphaComponent(color, alpha)
|
||||
}
|
||||
currentColor = color
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package org.koitharu.kotatsu.core.ui.image
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.ColorFilter
|
||||
import android.graphics.Outline
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Path
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.LayerDrawable
|
||||
import android.os.Build
|
||||
import androidx.annotation.ReturnThis
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColorStateList
|
||||
import org.koitharu.kotatsu.core.util.ext.resolveDp
|
||||
import org.koitharu.kotatsu.parsers.util.toIntUp
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
class CardDrawable(
|
||||
context: Context,
|
||||
private var corners: Int,
|
||||
) : Drawable() {
|
||||
|
||||
private val cornerSize = context.resources.resolveDp(12f)
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val cornersF = FloatArray(8)
|
||||
private val boundsF = RectF()
|
||||
private val color: ColorStateList
|
||||
private val path = Path()
|
||||
private var alpha = 255
|
||||
private var state: IntArray? = null
|
||||
private var horizontalInset: Int = 0
|
||||
|
||||
init {
|
||||
paint.style = Paint.Style.FILL
|
||||
color = context.getThemeColorStateList(materialR.attr.colorSurfaceContainerHighest)
|
||||
?: ColorStateList.valueOf(Color.TRANSPARENT)
|
||||
setCorners(corners)
|
||||
updateColor()
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
canvas.drawPath(path, paint)
|
||||
}
|
||||
|
||||
override fun setAlpha(alpha: Int) {
|
||||
this.alpha = alpha
|
||||
updateColor()
|
||||
}
|
||||
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) {
|
||||
paint.colorFilter = colorFilter
|
||||
}
|
||||
|
||||
override fun getColorFilter(): ColorFilter? = paint.colorFilter
|
||||
|
||||
override fun getOutline(outline: Outline) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
outline.setPath(path)
|
||||
} else if (path.isConvex) {
|
||||
outline.setConvexPath(path)
|
||||
}
|
||||
outline.alpha = 1f
|
||||
}
|
||||
|
||||
override fun getPadding(padding: Rect): Boolean {
|
||||
padding.set(
|
||||
horizontalInset,
|
||||
0,
|
||||
horizontalInset,
|
||||
0,
|
||||
)
|
||||
if (corners or TOP != 0) {
|
||||
padding.top += cornerSize.toIntUp()
|
||||
}
|
||||
if (corners or BOTTOM != 0) {
|
||||
padding.bottom += cornerSize.toIntUp()
|
||||
}
|
||||
return horizontalInset != 0
|
||||
}
|
||||
|
||||
override fun onStateChange(state: IntArray): Boolean {
|
||||
this.state = state
|
||||
if (color.isStateful) {
|
||||
updateColor()
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun getOpacity(): Int = PixelFormat.TRANSPARENT
|
||||
|
||||
override fun onBoundsChange(bounds: Rect) {
|
||||
super.onBoundsChange(bounds)
|
||||
boundsF.set(bounds)
|
||||
boundsF.inset(horizontalInset.toFloat(), 0f)
|
||||
path.reset()
|
||||
path.addRoundRect(boundsF, cornersF, Path.Direction.CW)
|
||||
path.close()
|
||||
}
|
||||
|
||||
@ReturnThis
|
||||
fun setCorners(corners: Int): CardDrawable {
|
||||
this.corners = corners
|
||||
val topLeft = if (corners and TOP_LEFT == TOP_LEFT) cornerSize else 0f
|
||||
val topRight = if (corners and TOP_RIGHT == TOP_RIGHT) cornerSize else 0f
|
||||
val bottomRight = if (corners and BOTTOM_RIGHT == BOTTOM_RIGHT) cornerSize else 0f
|
||||
val bottomLeft = if (corners and BOTTOM_LEFT == BOTTOM_LEFT) cornerSize else 0f
|
||||
cornersF[0] = topLeft
|
||||
cornersF[1] = topLeft
|
||||
cornersF[2] = topRight
|
||||
cornersF[3] = topRight
|
||||
cornersF[4] = bottomRight
|
||||
cornersF[5] = bottomRight
|
||||
cornersF[6] = bottomLeft
|
||||
cornersF[7] = bottomLeft
|
||||
invalidateSelf()
|
||||
return this
|
||||
}
|
||||
|
||||
fun setHorizontalInset(inset: Int) {
|
||||
horizontalInset = inset
|
||||
invalidateSelf()
|
||||
}
|
||||
|
||||
private fun updateColor() {
|
||||
paint.color = color.getColorForState(state, color.defaultColor)
|
||||
paint.alpha = alpha
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val TOP_LEFT = 1
|
||||
const val TOP_RIGHT = 2
|
||||
const val BOTTOM_LEFT = 4
|
||||
const val BOTTOM_RIGHT = 8
|
||||
|
||||
const val LEFT = TOP_LEFT or BOTTOM_LEFT
|
||||
const val TOP = TOP_LEFT or TOP_RIGHT
|
||||
const val RIGHT = TOP_RIGHT or BOTTOM_RIGHT
|
||||
const val BOTTOM = BOTTOM_LEFT or BOTTOM_RIGHT
|
||||
|
||||
const val NONE = 0
|
||||
const val ALL = TOP_LEFT or TOP_RIGHT or BOTTOM_RIGHT or BOTTOM_LEFT
|
||||
|
||||
fun from(d: Drawable?): CardDrawable? = when (d) {
|
||||
null -> null
|
||||
is CardDrawable -> d
|
||||
is LayerDrawable -> (0 until d.numberOfLayers).firstNotNullOfOrNull { i ->
|
||||
from(d.getDrawable(i))
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ class FitHeightGridLayoutManager : GridLayoutManager {
|
||||
if (orientation == RecyclerView.VERTICAL && child.layoutParams.height == LayoutParams.MATCH_PARENT) {
|
||||
val parentBottom = height - paddingBottom
|
||||
val offset = parentBottom - bottom
|
||||
super.layoutDecoratedWithMargins(child, left, top + offset, right, bottom + offset)
|
||||
super.layoutDecoratedWithMargins(child, left, top, right, bottom + offset)
|
||||
} else {
|
||||
super.layoutDecoratedWithMargins(child, left, top, right, bottom)
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ class FitHeightLinearLayoutManager : LinearLayoutManager {
|
||||
if (orientation == RecyclerView.VERTICAL && child.layoutParams.height == LayoutParams.MATCH_PARENT) {
|
||||
val parentBottom = height - paddingBottom
|
||||
val offset = parentBottom - bottom
|
||||
super.layoutDecoratedWithMargins(child, left, top + offset, right, bottom + offset)
|
||||
super.layoutDecoratedWithMargins(child, left, top, right, bottom + offset)
|
||||
} else {
|
||||
super.layoutDecoratedWithMargins(child, left, top, right, bottom)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.koitharu.kotatsu.core.ui.list
|
||||
|
||||
import android.app.Notification.Action
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
@@ -80,8 +81,7 @@ class ListSelectionController(
|
||||
}
|
||||
|
||||
fun onItemLongClick(id: Long): Boolean {
|
||||
startActionMode()
|
||||
return actionMode?.also {
|
||||
return startActionMode()?.also {
|
||||
decoration.setItemIsChecked(id, true)
|
||||
notifySelectionChanged()
|
||||
} != null
|
||||
@@ -105,9 +105,9 @@ class ListSelectionController(
|
||||
actionMode = null
|
||||
}
|
||||
|
||||
private fun startActionMode() {
|
||||
if (actionMode == null) {
|
||||
actionMode = appCompatDelegate.startSupportActionMode(this)
|
||||
private fun startActionMode(): ActionMode? {
|
||||
return actionMode ?: appCompatDelegate.startSupportActionMode(this).also {
|
||||
actionMode = it
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@ package org.koitharu.kotatsu.core.ui.sheet
|
||||
|
||||
import android.app.Dialog
|
||||
import android.view.View
|
||||
import android.view.ViewParent
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.view.ancestors
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
@@ -109,7 +112,16 @@ sealed class AdaptiveSheetBehavior {
|
||||
const val STATE_DRAGGING = SideSheetBehavior.STATE_DRAGGING
|
||||
const val STATE_HIDDEN = SideSheetBehavior.STATE_HIDDEN
|
||||
|
||||
fun from(dialog: Dialog?): AdaptiveSheetBehavior? = when (dialog) {
|
||||
fun from(fragment: DialogFragment): AdaptiveSheetBehavior? {
|
||||
from(fragment.dialog)?.let { return it }
|
||||
val rootView = fragment.view ?: return null
|
||||
for (parent in rootView.ancestors) {
|
||||
from(parent)?.let { return it }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun from(dialog: Dialog?): AdaptiveSheetBehavior? = when (dialog) {
|
||||
is BottomSheetDialog -> Bottom(dialog.behavior)
|
||||
is SideSheetDialog -> Side(dialog.behavior)
|
||||
else -> null
|
||||
@@ -121,5 +133,10 @@ sealed class AdaptiveSheetBehavior {
|
||||
is SideSheetBehavior<*> -> Side(behavior)
|
||||
else -> null
|
||||
}
|
||||
|
||||
private fun from(parent: ViewParent): AdaptiveSheetBehavior? {
|
||||
val lp = ((parent as? View)?.layoutParams as? CoordinatorLayout.LayoutParams) ?: return null
|
||||
return from(lp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,10 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.LayoutParams
|
||||
import androidx.activity.ComponentDialog
|
||||
import androidx.activity.OnBackPressedDispatcher
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDialog
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import androidx.appcompat.view.ActionMode
|
||||
@@ -20,11 +22,16 @@ import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.sidesheet.SideSheetDialog
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||
import com.google.android.material.R as materialR
|
||||
@@ -43,16 +50,16 @@ abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
|
||||
get() = requireViewBinding()
|
||||
|
||||
protected val behavior: AdaptiveSheetBehavior?
|
||||
get() = AdaptiveSheetBehavior.from(dialog)
|
||||
get() = AdaptiveSheetBehavior.from(this)
|
||||
|
||||
@JvmField
|
||||
val actionModeDelegate = ActionModeDelegate()
|
||||
var actionModeDelegate: ActionModeDelegate? = null
|
||||
private set
|
||||
|
||||
val isExpanded: Boolean
|
||||
get() = behavior?.state == AdaptiveSheetBehavior.STATE_EXPANDED
|
||||
|
||||
val onBackPressedDispatcher: OnBackPressedDispatcher
|
||||
get() = requireComponentDialog().onBackPressedDispatcher
|
||||
get() = (dialog as? ComponentDialog)?.onBackPressedDispatcher ?: requireActivity().onBackPressedDispatcher
|
||||
|
||||
var isLocked = false
|
||||
private set
|
||||
@@ -71,11 +78,15 @@ abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
|
||||
final override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val binding = requireViewBinding()
|
||||
if (actionModeDelegate == null) {
|
||||
actionModeDelegate = (activity as? BaseActivity<*>)?.actionModeDelegate
|
||||
}
|
||||
onViewBindingCreated(binding, savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
viewBinding = null
|
||||
actionModeDelegate = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
@@ -86,13 +97,15 @@ abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
|
||||
} else {
|
||||
BottomSheetDialogImpl(context, theme)
|
||||
}
|
||||
dialog.onBackPressedDispatcher.addCallback(actionModeDelegate)
|
||||
actionModeDelegate = ActionModeDelegate().also {
|
||||
dialog.onBackPressedDispatcher.addCallback(it)
|
||||
}
|
||||
return dialog
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
protected open fun dispatchSupportActionModeStarted(mode: ActionMode) {
|
||||
actionModeDelegate.onSupportActionModeStarted(mode)
|
||||
actionModeDelegate?.onSupportActionModeStarted(mode)
|
||||
val ctx = requireContext()
|
||||
val actionModeColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
ColorUtils.compositeColors(
|
||||
@@ -118,18 +131,21 @@ abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
|
||||
|
||||
@CallSuper
|
||||
protected open fun dispatchSupportActionModeFinished(mode: ActionMode) {
|
||||
actionModeDelegate.onSupportActionModeFinished(mode)
|
||||
actionModeDelegate?.onSupportActionModeFinished(mode)
|
||||
dialog?.window?.statusBarColor = defaultStatusBarColor
|
||||
}
|
||||
|
||||
fun addSheetCallback(callback: AdaptiveSheetCallback) {
|
||||
val b = behavior ?: return
|
||||
fun addSheetCallback(callback: AdaptiveSheetCallback, lifecycleOwner: LifecycleOwner): Boolean {
|
||||
val b = behavior ?: return false
|
||||
b.addCallback(callback)
|
||||
val rootView = dialog?.findViewById<View>(materialR.id.design_bottom_sheet)
|
||||
?: dialog?.findViewById(materialR.id.coordinator)
|
||||
?: view
|
||||
if (rootView != null) {
|
||||
callback.onStateChanged(rootView, b.state)
|
||||
}
|
||||
lifecycleOwner.lifecycle.addObserver(CallbackRemoveObserver(b, callback))
|
||||
return true
|
||||
}
|
||||
|
||||
protected abstract fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): B
|
||||
@@ -137,8 +153,9 @@ abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
|
||||
protected open fun onViewBindingCreated(binding: B, savedInstanceState: Bundle?) = Unit
|
||||
|
||||
fun startSupportActionMode(callback: ActionMode.Callback): ActionMode? {
|
||||
val appCompatDialog = dialog as? AppCompatDialog ?: return null
|
||||
return appCompatDialog.delegate.startSupportActionMode(callback)
|
||||
val delegate =
|
||||
(dialog as? AppCompatDialog)?.delegate ?: (activity as? AppCompatActivity)?.delegate ?: return null
|
||||
return delegate.startSupportActionMode(callback)
|
||||
}
|
||||
|
||||
protected fun setExpanded(isExpanded: Boolean, isLocked: Boolean) {
|
||||
@@ -283,4 +300,16 @@ abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class CallbackRemoveObserver(
|
||||
private val behavior: AdaptiveSheetBehavior,
|
||||
private val callback: AdaptiveSheetCallback,
|
||||
) : DefaultLifecycleObserver {
|
||||
|
||||
override fun onDestroy(owner: LifecycleOwner) {
|
||||
super.onDestroy(owner)
|
||||
owner.lifecycle.removeObserver(this)
|
||||
behavior.removeCallback(callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.koitharu.kotatsu.core.ui.util
|
||||
|
||||
import android.view.View
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HALF_EXPANDED
|
||||
import org.koitharu.kotatsu.core.util.ext.doOnExpansionsChanged
|
||||
|
||||
class BottomSheetClollapseCallback(
|
||||
private val behavior: BottomSheetBehavior<*>,
|
||||
) : OnBackPressedCallback(behavior.state == STATE_EXPANDED) {
|
||||
|
||||
init {
|
||||
behavior.addBottomSheetCallback(
|
||||
object : BottomSheetBehavior.BottomSheetCallback() {
|
||||
|
||||
override fun onStateChanged(view: View, state: Int) {
|
||||
isEnabled = state == STATE_EXPANDED || state == STATE_HALF_EXPANDED
|
||||
}
|
||||
|
||||
override fun onSlide(p0: View, p1: Float) = Unit
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun handleOnBackPressed() {
|
||||
behavior.state = STATE_COLLAPSED
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package org.koitharu.kotatsu.core.ui.widgets
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.core.content.withStyledAttributes
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||
import org.koitharu.kotatsu.core.util.ext.measureDimension
|
||||
import org.koitharu.kotatsu.core.util.ext.resolveDp
|
||||
import org.koitharu.kotatsu.parsers.util.toIntUp
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
class DotsIndicator @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = R.attr.dotIndicatorStyle,
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private var indicatorSize = context.resources.resolveDp(12f)
|
||||
private var dotSpacing = 0f
|
||||
private var smallDotScale = 0.33f
|
||||
private var smallDotAlpha = 0.6f
|
||||
private var positionOffset: Float = 0f
|
||||
private var position: Int = 0
|
||||
private val inset = context.resources.resolveDp(1f)
|
||||
|
||||
var max: Int = 6
|
||||
set(value) {
|
||||
if (field != value) {
|
||||
field = value
|
||||
requestLayout()
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
var progress: Int
|
||||
get() = position
|
||||
set(value) {
|
||||
if (position != value) {
|
||||
position = value
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
paint.style = Paint.Style.FILL
|
||||
context.withStyledAttributes(attrs, R.styleable.DotsIndicator, defStyleAttr) {
|
||||
paint.color = getColor(
|
||||
R.styleable.DotsIndicator_dotColor,
|
||||
context.getThemeColor(materialR.attr.colorOnBackground, Color.DKGRAY),
|
||||
)
|
||||
indicatorSize = getDimension(R.styleable.DotsIndicator_dotSize, indicatorSize)
|
||||
dotSpacing = getDimension(R.styleable.DotsIndicator_dotSpacing, dotSpacing)
|
||||
smallDotScale = getFloat(R.styleable.DotsIndicator_dotScale, smallDotScale).coerceIn(0f, 1f)
|
||||
smallDotAlpha = getFloat(R.styleable.DotsIndicator_dotAlpha, smallDotAlpha).coerceIn(0f, 1f)
|
||||
max = getInt(R.styleable.DotsIndicator_android_max, max)
|
||||
position = getInt(R.styleable.DotsIndicator_android_progress, position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
val dotSize = getDotSize()
|
||||
val y = paddingTop + (height - paddingTop - paddingBottom) / 2f
|
||||
var x = paddingLeft + dotSize / 2f
|
||||
val radius = dotSize / 2f - inset
|
||||
val spacing = (width - paddingLeft - paddingRight) / max.toFloat() - dotSize
|
||||
x += spacing / 2f
|
||||
for (i in 0 until max) {
|
||||
val scale = when (i) {
|
||||
position -> (1f - smallDotScale) * (1f - positionOffset) + smallDotScale
|
||||
position + 1 -> (1f - smallDotScale) * positionOffset + smallDotScale
|
||||
else -> smallDotScale
|
||||
}
|
||||
paint.alpha = (255 * when (i) {
|
||||
position -> (1f - smallDotAlpha) * (1f - positionOffset) + smallDotAlpha
|
||||
position + 1 -> (1f - smallDotAlpha) * positionOffset + smallDotAlpha
|
||||
else -> smallDotAlpha
|
||||
}).toInt()
|
||||
canvas.drawCircle(x, y, radius * scale, paint)
|
||||
x += spacing + dotSize
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val dotSize = getDotSize()
|
||||
val desiredHeight = (dotSize + paddingTop + paddingBottom).toIntUp()
|
||||
val desiredWidth = ((dotSize + dotSpacing) * max).toIntUp() + paddingLeft + paddingRight
|
||||
setMeasuredDimension(
|
||||
measureDimension(desiredWidth, widthMeasureSpec),
|
||||
measureDimension(desiredHeight, heightMeasureSpec),
|
||||
)
|
||||
}
|
||||
|
||||
fun bindToViewPager(pager: ViewPager2) {
|
||||
pager.registerOnPageChangeCallback(ViewPagerCallback())
|
||||
pager.adapter?.let {
|
||||
it.registerAdapterDataObserver(AdapterObserver(it))
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDotSize() = if (indicatorSize <= 0) {
|
||||
(height - paddingTop - paddingBottom).toFloat()
|
||||
} else {
|
||||
indicatorSize
|
||||
}
|
||||
|
||||
private inner class ViewPagerCallback : ViewPager2.OnPageChangeCallback() {
|
||||
|
||||
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
|
||||
super.onPageScrolled(position, positionOffset, positionOffsetPixels)
|
||||
this@DotsIndicator.position = position
|
||||
this@DotsIndicator.positionOffset = positionOffset
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
private inner class AdapterObserver(
|
||||
private val adapter: RecyclerView.Adapter<*>,
|
||||
) : AdapterDataObserver() {
|
||||
|
||||
override fun onChanged() {
|
||||
super.onChanged()
|
||||
max = adapter.itemCount
|
||||
}
|
||||
|
||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||
super.onItemRangeInserted(positionStart, itemCount)
|
||||
max = adapter.itemCount
|
||||
}
|
||||
|
||||
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
|
||||
super.onItemRangeRemoved(positionStart, itemCount)
|
||||
max = adapter.itemCount
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,15 @@ class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor(
|
||||
|
||||
private var dyRatio = 1F
|
||||
|
||||
var isPinned: Boolean = false
|
||||
set(value) {
|
||||
field = value
|
||||
if (value) {
|
||||
offsetAnimator?.cancel()
|
||||
offsetAnimator = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun layoutDependsOn(parent: CoordinatorLayout, child: BottomNavigationView, dependency: View): Boolean {
|
||||
return dependency is AppBarLayout
|
||||
}
|
||||
@@ -51,7 +60,7 @@ class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor(
|
||||
axes: Int,
|
||||
type: Int,
|
||||
): Boolean {
|
||||
if (axes != ViewCompat.SCROLL_AXIS_VERTICAL) {
|
||||
if (isPinned || axes != ViewCompat.SCROLL_AXIS_VERTICAL) {
|
||||
return false
|
||||
}
|
||||
lastStartedType = type
|
||||
@@ -69,7 +78,9 @@ class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor(
|
||||
type: Int,
|
||||
) {
|
||||
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type)
|
||||
child.translationY = (child.translationY + (dy * dyRatio)).coerceIn(0F, child.height.toFloat())
|
||||
if (!isPinned) {
|
||||
child.translationY = (child.translationY + (dy * dyRatio)).coerceIn(0F, child.height.toFloat())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStopNestedScroll(
|
||||
@@ -78,7 +89,7 @@ class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor(
|
||||
target: View,
|
||||
type: Int,
|
||||
) {
|
||||
if (lastStartedType == ViewCompat.TYPE_TOUCH || type == ViewCompat.TYPE_NON_TOUCH) {
|
||||
if (!isPinned && (lastStartedType == ViewCompat.TYPE_TOUCH || type == ViewCompat.TYPE_NON_TOUCH)) {
|
||||
animateBottomNavigationVisibility(child, child.translationY < child.height / 2)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ package org.koitharu.kotatsu.core.ui.widgets
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Outline
|
||||
import android.graphics.Paint
|
||||
import android.util.AttributeSet
|
||||
@@ -18,7 +20,7 @@ import androidx.core.view.children
|
||||
import androidx.core.widget.TextViewCompat
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColorStateList
|
||||
import org.koitharu.kotatsu.core.util.ext.resolveDp
|
||||
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
|
||||
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
||||
@@ -35,9 +37,8 @@ class ProgressButton @JvmOverloads constructor(
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
|
||||
private var progress = 0f
|
||||
private var colorBase = context.getThemeColor(materialR.attr.colorPrimaryContainer)
|
||||
private var colorProgress = context.getThemeColor(materialR.attr.colorPrimary)
|
||||
private var colorText = context.getThemeColor(materialR.attr.colorOnPrimaryContainer)
|
||||
private var colorBase: ColorStateList = ColorStateList.valueOf(Color.TRANSPARENT)
|
||||
private var colorProgress: ColorStateList = ColorStateList.valueOf(Color.TRANSPARENT)
|
||||
private var progressAnimator: ValueAnimator? = null
|
||||
|
||||
var title: CharSequence?
|
||||
@@ -69,11 +70,14 @@ class ProgressButton @JvmOverloads constructor(
|
||||
)
|
||||
textViewTitle.text = getText(R.styleable.ProgressButton_title)
|
||||
textViewSubtitle.text = getText(R.styleable.ProgressButton_subtitle)
|
||||
colorBase = getColor(R.styleable.ProgressButton_baseColor, colorBase)
|
||||
colorProgress = getColor(R.styleable.ProgressButton_progressColor, colorProgress)
|
||||
colorText = getColor(R.styleable.ProgressButton_textColor, colorText)
|
||||
textViewTitle.setTextColor(colorText)
|
||||
textViewSubtitle.setTextColor(colorText)
|
||||
colorBase = getColorStateList(R.styleable.ProgressButton_baseColor)
|
||||
?: context.getThemeColorStateList(materialR.attr.colorPrimaryContainer) ?: colorBase
|
||||
colorProgress = getColorStateList(R.styleable.ProgressButton_progressColor)
|
||||
?: context.getThemeColorStateList(materialR.attr.colorPrimary) ?: colorProgress
|
||||
getColorStateList(R.styleable.ProgressButton_android_textColor)?.let { colorText ->
|
||||
textViewTitle.setTextColor(colorText)
|
||||
textViewSubtitle.setTextColor(colorText)
|
||||
}
|
||||
progress = getInt(R.styleable.ProgressButton_android_progress, 0).toFloat() /
|
||||
getInt(R.styleable.ProgressButton_android_max, 100).toFloat()
|
||||
}
|
||||
@@ -87,15 +91,15 @@ class ProgressButton @JvmOverloads constructor(
|
||||
)
|
||||
|
||||
paint.style = Paint.Style.FILL
|
||||
paint.color = colorProgress
|
||||
paint.alpha = 84 // 255 * 0.33F
|
||||
applyGravity()
|
||||
setWillNotDraw(false)
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
canvas.drawColor(colorBase)
|
||||
canvas.drawColor(colorBase.getColorForState(drawableState, colorBase.defaultColor))
|
||||
paint.color = colorProgress.getColorForState(drawableState, colorProgress.defaultColor)
|
||||
paint.alpha = 84 // 255 * 0.33F
|
||||
canvas.drawRect(0f, 0f, width * progress, height.toFloat(), paint)
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,15 @@ class SlidingBottomNavigationView @JvmOverloads constructor(
|
||||
private var currentState = STATE_UP
|
||||
private var behavior = HideBottomNavigationOnScrollBehavior()
|
||||
|
||||
var isPinned: Boolean
|
||||
get() = behavior.isPinned
|
||||
set(value) {
|
||||
behavior.isPinned = value
|
||||
if (value) {
|
||||
translationX = 0f
|
||||
}
|
||||
}
|
||||
|
||||
override fun getBehavior(): CoordinatorLayout.Behavior<*> {
|
||||
return behavior
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.withStyledAttributes
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.setPadding
|
||||
import com.google.android.material.shape.MaterialShapeDrawable
|
||||
import com.google.android.material.shape.ShapeAppearanceModel
|
||||
@@ -103,16 +104,22 @@ class TipView @JvmOverloads constructor(
|
||||
|
||||
fun setPrimaryButtonText(@StringRes resId: Int) {
|
||||
binding.buttonPrimary.setTextAndVisible(resId)
|
||||
updateButtonsLayout()
|
||||
}
|
||||
|
||||
fun setSecondaryButtonText(@StringRes resId: Int) {
|
||||
binding.buttonSecondary.setTextAndVisible(resId)
|
||||
updateButtonsLayout()
|
||||
}
|
||||
|
||||
fun setIcon(@DrawableRes resId: Int) {
|
||||
icon = ContextCompat.getDrawable(context, resId)
|
||||
}
|
||||
|
||||
private fun updateButtonsLayout() {
|
||||
binding.layoutButtons.isVisible = binding.buttonPrimary.isVisible || binding.buttonSecondary.isVisible
|
||||
}
|
||||
|
||||
interface OnButtonClickListener {
|
||||
|
||||
fun onPrimaryButtonClick(tipView: TipView)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.koitharu.kotatsu.core.util
|
||||
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.report
|
||||
import kotlin.coroutines.AbstractCoroutineContextElement
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class AcraCoroutineErrorHandler : AbstractCoroutineContextElement(CoroutineExceptionHandler),
|
||||
CoroutineExceptionHandler {
|
||||
|
||||
override fun handleException(context: CoroutineContext, exception: Throwable) {
|
||||
exception.printStackTraceDebug()
|
||||
exception.report()
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.util
|
||||
|
||||
import androidx.collection.ArrayMap
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
@Deprecated("", replaceWith = ReplaceWith("CompositeMutex2"))
|
||||
class CompositeMutex<T : Any> : Set<T> {
|
||||
|
||||
private val state = ArrayMap<T, MutableStateFlow<Boolean>>()
|
||||
private val mutex = Mutex()
|
||||
|
||||
override val size: Int
|
||||
get() = state.size
|
||||
|
||||
override fun contains(element: T): Boolean {
|
||||
return state.containsKey(element)
|
||||
}
|
||||
|
||||
override fun containsAll(elements: Collection<T>): Boolean {
|
||||
return elements.all { x -> state.containsKey(x) }
|
||||
}
|
||||
|
||||
override fun isEmpty(): Boolean {
|
||||
return state.isEmpty()
|
||||
}
|
||||
|
||||
override fun iterator(): Iterator<T> {
|
||||
return state.keys.iterator()
|
||||
}
|
||||
|
||||
suspend fun lock(element: T) {
|
||||
while (coroutineContext.isActive) {
|
||||
waitForRemoval(element)
|
||||
mutex.withLock {
|
||||
if (state[element] == null) {
|
||||
state[element] = MutableStateFlow(false)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun unlock(element: T) {
|
||||
checkNotNull(state.remove(element)) {
|
||||
"CompositeMutex is not locked for $element"
|
||||
}.value = true
|
||||
}
|
||||
|
||||
private suspend fun waitForRemoval(element: T) {
|
||||
val flow = state[element] ?: return
|
||||
flow.first { it }
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.util
|
||||
|
||||
class CompositeRunnable(
|
||||
private val children: List<Runnable>,
|
||||
) : Runnable, Collection<Runnable> by children {
|
||||
|
||||
override fun run() {
|
||||
for (child in children) {
|
||||
child.run()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,8 @@ class Event<T>(
|
||||
|
||||
suspend fun consume(collector: FlowCollector<T>) {
|
||||
if (!isConsumed) {
|
||||
collector.emit(data)
|
||||
isConsumed = true
|
||||
collector.emit(data)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.util
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.flowWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||
import org.koitharu.kotatsu.core.ui.DefaultActivityLifecycleCallbacks
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class IncognitoModeIndicator @Inject constructor(
|
||||
private val settings: AppSettings,
|
||||
) : DefaultActivityLifecycleCallbacks {
|
||||
|
||||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||
if (activity !is AppCompatActivity) {
|
||||
return
|
||||
}
|
||||
settings.observeAsFlow(
|
||||
key = AppSettings.KEY_INCOGNITO_MODE,
|
||||
valueProducer = { isIncognitoModeEnabled },
|
||||
).flowOn(Dispatchers.IO)
|
||||
.flowWithLifecycle(activity.lifecycle)
|
||||
.onEach { updateStatusBar(activity, it) }
|
||||
.launchIn(activity.lifecycleScope)
|
||||
}
|
||||
|
||||
private fun updateStatusBar(activity: AppCompatActivity, isIncognitoModeEnabled: Boolean) {
|
||||
activity.window.statusBarColor = if (isIncognitoModeEnabled) {
|
||||
ContextCompat.getColor(activity, R.color.status_bar_incognito)
|
||||
} else {
|
||||
activity.getThemeColor(android.R.attr.statusBarColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.core.util
|
||||
import androidx.collection.ArrayMap
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
|
||||
class CompositeMutex2<T : Any> : Set<T> {
|
||||
class MultiMutex<T : Any> : Set<T> {
|
||||
|
||||
private val delegates = ArrayMap<T, Mutex>()
|
||||
|
||||
@@ -65,6 +65,7 @@ import org.xmlpull.v1.XmlPullParser
|
||||
import org.xmlpull.v1.XmlPullParserException
|
||||
import java.io.File
|
||||
import kotlin.math.roundToLong
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
val Context.activityManager: ActivityManager?
|
||||
get() = getSystemService(ACTIVITY_SERVICE) as? ActivityManager
|
||||
@@ -141,7 +142,7 @@ fun Window.setNavigationBarTransparentCompat(context: Context, elevation: Float,
|
||||
} else {
|
||||
// Set navbar scrim 70% of navigationBarColor
|
||||
ElevationOverlayProvider(context).compositeOverlayIfNeeded(
|
||||
context.getThemeColor(com.google.android.material.R.attr.colorSurfaceContainer, alphaFactor),
|
||||
context.getThemeColor(materialR.attr.colorSurfaceContainer, alphaFactor),
|
||||
elevation,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package org.koitharu.kotatsu.core.util.ext
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.widget.ImageView
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import coil.ImageLoader
|
||||
@@ -12,9 +14,11 @@ import coil.request.SuccessResult
|
||||
import coil.util.CoilUtils
|
||||
import com.google.android.material.progressindicator.BaseProgressIndicator
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.image.AnimatedPlaceholderDrawable
|
||||
import org.koitharu.kotatsu.core.ui.image.RegionBitmapDecoder
|
||||
import org.koitharu.kotatsu.core.util.progress.ImageRequestIndicatorListener
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
fun ImageView.newImageRequest(lifecycleOwner: LifecycleOwner, data: Any?): ImageRequest.Builder? {
|
||||
val current = CoilUtils.result(this)
|
||||
@@ -85,6 +89,17 @@ fun ImageRequest.Builder.source(source: MangaSource?): ImageRequest.Builder {
|
||||
return tag(MangaSource::class.java, source)
|
||||
}
|
||||
|
||||
fun ImageRequest.Builder.defaultPlaceholders(context: Context): ImageRequest.Builder {
|
||||
val errorColor = ColorUtils.blendARGB(
|
||||
context.getThemeColor(materialR.attr.colorErrorContainer),
|
||||
context.getThemeColor(materialR.attr.colorBackgroundFloating),
|
||||
0.25f,
|
||||
)
|
||||
return placeholder(AnimatedPlaceholderDrawable(context))
|
||||
.fallback(ColorDrawable(context.getThemeColor(materialR.attr.colorSurfaceContainer)))
|
||||
.error(ColorDrawable(errorColor))
|
||||
}
|
||||
|
||||
fun ImageRequest.Builder.addListener(listener: ImageRequest.Listener): ImageRequest.Builder {
|
||||
val existing = build().listener
|
||||
return listener(
|
||||
|
||||
@@ -68,3 +68,5 @@ fun <T> Iterable<T>.sortedWithSafe(comparator: Comparator<in T>): List<T> = try
|
||||
toList()
|
||||
}
|
||||
}
|
||||
|
||||
fun Collection<*>?.sizeOrZero() = if (this == null) 0 else size
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.koitharu.kotatsu.core.util.ext
|
||||
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.lifecycle.LifecycleDestroyedException
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
@@ -10,17 +9,20 @@ import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import dagger.hilt.android.lifecycle.RetainedLifecycle
|
||||
import kotlinx.coroutines.CancellableContinuation
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.plus
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import org.koitharu.kotatsu.core.util.AcraCoroutineErrorHandler
|
||||
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
val processLifecycleScope: LifecycleCoroutineScope
|
||||
inline get() = ProcessLifecycleOwner.get().lifecycleScope
|
||||
val processLifecycleScope: CoroutineScope
|
||||
get() = ProcessLifecycleOwner.get().lifecycleScope + AcraCoroutineErrorHandler()
|
||||
|
||||
val RetainedLifecycle.lifecycleScope: RetainedLifecycleCoroutineScope
|
||||
inline get() = RetainedLifecycleCoroutineScope(this)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.koitharu.kotatsu.core.util.ext
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
@@ -28,7 +29,7 @@ fun <T> Flow<T>.observe(owner: LifecycleOwner, minState: Lifecycle.State, collec
|
||||
|
||||
fun <T> Flow<Event<T>?>.observeEvent(owner: LifecycleOwner, collector: FlowCollector<T>) {
|
||||
owner.lifecycleScope.launch {
|
||||
owner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
owner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
collect {
|
||||
it?.consume(collector)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
package org.koitharu.kotatsu.core.util.ext
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
inline fun <T : Fragment> T.withArgs(size: Int, block: Bundle.() -> Unit): T {
|
||||
val b = Bundle(size)
|
||||
@@ -33,26 +28,6 @@ fun Fragment.addMenuProvider(provider: MenuProvider) {
|
||||
requireActivity().addMenuProvider(provider, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
||||
}
|
||||
|
||||
@MainThread
|
||||
suspend fun Fragment.awaitViewLifecycle(): LifecycleOwner {
|
||||
val liveData = viewLifecycleOwnerLiveData
|
||||
liveData.value?.let { return it }
|
||||
return suspendCancellableCoroutine { cont ->
|
||||
val observer = object : Observer<LifecycleOwner?> {
|
||||
override fun onChanged(value: LifecycleOwner?) {
|
||||
if (value != null) {
|
||||
liveData.removeObserver(this)
|
||||
cont.resume(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
liveData.observeForever(observer)
|
||||
cont.invokeOnCancellation {
|
||||
liveData.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun DialogFragment.showDistinct(fm: FragmentManager, tag: String) {
|
||||
val existing = fm.findFragmentByTag(tag) as? DialogFragment?
|
||||
if (existing != null && existing.isVisible && existing.arguments == this.arguments) {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package org.koitharu.kotatsu.core.util.ext
|
||||
|
||||
import org.koitharu.kotatsu.core.util.CompositeRunnable
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T> Class<T>.castOrNull(obj: Any?): T? {
|
||||
if (obj == null || !isInstance(obj)) {
|
||||
@@ -9,15 +7,3 @@ fun <T> Class<T>.castOrNull(obj: Any?): T? {
|
||||
}
|
||||
return obj as T
|
||||
}
|
||||
|
||||
/* CompositeRunnable */
|
||||
|
||||
operator fun Runnable.plus(other: Runnable): Runnable {
|
||||
val list = ArrayList<Runnable>(this.size + other.size)
|
||||
if (this is CompositeRunnable) list.addAll(this) else list.add(this)
|
||||
if (other is CompositeRunnable) list.addAll(other) else list.add(other)
|
||||
return CompositeRunnable(list)
|
||||
}
|
||||
|
||||
private val Runnable.size: Int
|
||||
get() = if (this is CompositeRunnable) size else 1
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.util.ext
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.text.StaticLayout
|
||||
import androidx.core.graphics.withTranslation
|
||||
|
||||
fun StaticLayout.draw(canvas: Canvas, x: Float, y: Float) {
|
||||
canvas.withTranslation(x, y) {
|
||||
draw(this)
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import org.jsoup.HttpStatusException
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.BadBackupFormatException
|
||||
import org.koitharu.kotatsu.core.exceptions.CaughtException
|
||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareBlockedException
|
||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
||||
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
|
||||
import org.koitharu.kotatsu.core.exceptions.NoDataReceivedException
|
||||
@@ -38,6 +39,7 @@ private const val IMAGE_FORMAT_NOT_SUPPORTED = "Image format not supported"
|
||||
fun Throwable.getDisplayMessage(resources: Resources): String = when (this) {
|
||||
is AuthRequiredException -> resources.getString(R.string.auth_required)
|
||||
is CloudFlareProtectedException -> resources.getString(R.string.captcha_required)
|
||||
is CloudFlareBlockedException -> resources.getString(R.string.blocked_by_server_message)
|
||||
is ActivityNotFoundException,
|
||||
is UnsupportedOperationException,
|
||||
-> resources.getString(R.string.operation_not_supported)
|
||||
@@ -79,6 +81,8 @@ fun Throwable.getDisplayIcon() = when (this) {
|
||||
is SocketTimeoutException,
|
||||
-> R.drawable.ic_plug_large
|
||||
|
||||
is CloudFlareBlockedException -> R.drawable.ic_denied_large
|
||||
|
||||
else -> R.drawable.ic_error_large
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package org.koitharu.kotatsu.core.util.ext
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.createViewModelLazy
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.ViewModelStore
|
||||
import androidx.lifecycle.viewmodel.CreationExtras
|
||||
|
||||
@MainThread
|
||||
@@ -19,7 +17,3 @@ inline fun <reified VM : ViewModel> Fragment.parentFragmentViewModels(
|
||||
extrasProducer = { extrasProducer?.invoke() ?: requireParentFragment().defaultViewModelCreationExtras },
|
||||
factoryProducer = factoryProducer ?: { requireParentFragment().defaultViewModelProviderFactory },
|
||||
)
|
||||
|
||||
val ViewModelStore.values: Collection<ViewModel>
|
||||
@SuppressLint("RestrictedApi")
|
||||
get() = this.keys().mapNotNull { get(it) }
|
||||
|
||||
@@ -83,7 +83,6 @@ suspend fun WorkManager.getWorkSpec(id: UUID): WorkSpec? = suspendCoroutine { co
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
suspend fun WorkManager.getWorkInputData(id: UUID): Data? = getWorkSpec(id)?.input
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import okio.IOException
|
||||
import org.koitharu.kotatsu.core.model.isLocal
|
||||
@@ -17,6 +18,7 @@ import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.util.ext.peek
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.sanitize
|
||||
import org.koitharu.kotatsu.details.data.MangaDetails
|
||||
import org.koitharu.kotatsu.explore.domain.RecoverMangaUseCase
|
||||
@@ -25,7 +27,9 @@ import org.koitharu.kotatsu.parsers.exception.NotFoundException
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.util.recoverNotNull
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.tracker.domain.Tracker
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
class DetailsLoadUseCase @Inject constructor(
|
||||
private val mangaDataRepository: MangaDataRepository,
|
||||
@@ -33,6 +37,7 @@ class DetailsLoadUseCase @Inject constructor(
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
private val recoverUseCase: RecoverMangaUseCase,
|
||||
private val imageGetter: Html.ImageGetter,
|
||||
private val trackerProvider: Provider<Tracker>,
|
||||
) {
|
||||
|
||||
operator fun invoke(intent: MangaIntent): Flow<MangaDetails> = channelFlow {
|
||||
@@ -49,6 +54,7 @@ class DetailsLoadUseCase @Inject constructor(
|
||||
send(MangaDetails(manga, null, null, false))
|
||||
try {
|
||||
val details = getDetails(manga)
|
||||
launch { updateTracker(manga) }
|
||||
send(MangaDetails(details, local?.peek(), details.description?.parseAsHtml(withImages = false), false))
|
||||
send(MangaDetails(details, local?.await(), details.description?.parseAsHtml(withImages = true), true))
|
||||
} catch (e: IOException) {
|
||||
@@ -90,4 +96,10 @@ class DetailsLoadUseCase @Inject constructor(
|
||||
}
|
||||
return spannable
|
||||
}
|
||||
|
||||
private suspend fun updateTracker(details: Manga) = runCatchingCancellable {
|
||||
trackerProvider.get().syncWithDetails(details)
|
||||
}.onFailure { e ->
|
||||
e.printStackTraceDebug()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
package org.koitharu.kotatsu.details.ui
|
||||
|
||||
import android.transition.TransitionManager
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.setMargins
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeDimensionPixelSize
|
||||
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
|
||||
import org.koitharu.kotatsu.databinding.ItemTipBinding
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
class ButtonTip(
|
||||
private val root: ViewGroup,
|
||||
private val insetsDelegate: WindowInsetsDelegate,
|
||||
private val viewModel: DetailsViewModel,
|
||||
) : View.OnClickListener, WindowInsetsDelegate.WindowInsetsListener {
|
||||
|
||||
private var selfBinding = ItemTipBinding.inflate(LayoutInflater.from(root.context), root, false)
|
||||
private val actionBarSize = root.context.getThemeDimensionPixelSize(materialR.attr.actionBarSize)
|
||||
|
||||
init {
|
||||
selfBinding.textView.setText(R.string.details_button_tip)
|
||||
selfBinding.imageViewIcon.setImageResource(R.drawable.ic_tap)
|
||||
selfBinding.root.id = R.id.layout_tip
|
||||
selfBinding.buttonClose.setOnClickListener(this)
|
||||
}
|
||||
|
||||
override fun onClick(v: View?) {
|
||||
remove()
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
if (root is CoordinatorLayout) {
|
||||
selfBinding.root.updateLayoutParams<CoordinatorLayout.LayoutParams> {
|
||||
bottomMargin = topMargin + insets.bottom + insets.top + actionBarSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addToRoot() {
|
||||
val lp: ViewGroup.LayoutParams = when (root) {
|
||||
is CoordinatorLayout -> CoordinatorLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
).apply {
|
||||
// anchorId = R.id.layout_bottom
|
||||
// anchorGravity = Gravity.TOP
|
||||
gravity = Gravity.BOTTOM
|
||||
setMargins(root.resources.getDimensionPixelOffset(R.dimen.margin_normal))
|
||||
bottomMargin += actionBarSize
|
||||
}
|
||||
|
||||
is ConstraintLayout -> ConstraintLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
).apply {
|
||||
width = root.resources.getDimensionPixelSize(R.dimen.m3_side_sheet_width)
|
||||
setMargins(root.resources.getDimensionPixelOffset(R.dimen.margin_normal))
|
||||
}
|
||||
|
||||
else -> ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
root.addView(selfBinding.root, lp)
|
||||
if (root is ConstraintLayout) {
|
||||
val cs = ConstraintSet()
|
||||
cs.clone(root)
|
||||
cs.connect(R.id.layout_tip, ConstraintSet.TOP, R.id.appbar, ConstraintSet.BOTTOM)
|
||||
cs.connect(R.id.layout_tip, ConstraintSet.START, R.id.card_chapters, ConstraintSet.START)
|
||||
cs.connect(R.id.layout_tip, ConstraintSet.END, R.id.card_chapters, ConstraintSet.END)
|
||||
cs.applyTo(root)
|
||||
}
|
||||
insetsDelegate.addInsetsListener(this)
|
||||
}
|
||||
|
||||
fun remove() {
|
||||
if (root.context.isAnimationsEnabled) {
|
||||
TransitionManager.beginDelayedTransition(root)
|
||||
}
|
||||
insetsDelegate.removeInsetsListener(this)
|
||||
root.removeView(selfBinding.root)
|
||||
viewModel.onButtonTipClosed()
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
package org.koitharu.kotatsu.details.ui
|
||||
|
||||
import android.view.InputDevice
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.View.OnLayoutChangeListener
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import org.koitharu.kotatsu.core.ui.util.ActionModeListener
|
||||
import org.koitharu.kotatsu.core.util.ext.doOnExpansionsChanged
|
||||
import org.koitharu.kotatsu.core.util.ext.setTabsEnabled
|
||||
|
||||
class ChaptersBottomSheetMediator(
|
||||
private val behavior: BottomSheetBehavior<*>,
|
||||
private val pager: ViewPager2,
|
||||
private val tabLayout: TabLayout,
|
||||
) : OnBackPressedCallback(false),
|
||||
ActionModeListener,
|
||||
OnLayoutChangeListener, View.OnGenericMotionListener {
|
||||
|
||||
private var lockCounter = 0
|
||||
|
||||
init {
|
||||
behavior.doOnExpansionsChanged { isExpanded ->
|
||||
isEnabled = isExpanded
|
||||
if (!isExpanded) {
|
||||
unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleOnBackPressed() {
|
||||
behavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||
}
|
||||
|
||||
override fun onActionModeStarted(mode: ActionMode) {
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
lock()
|
||||
}
|
||||
|
||||
override fun onActionModeFinished(mode: ActionMode) {
|
||||
unlock()
|
||||
}
|
||||
|
||||
override fun onLayoutChange(
|
||||
v: View?,
|
||||
left: Int,
|
||||
top: Int,
|
||||
right: Int,
|
||||
bottom: Int,
|
||||
oldLeft: Int,
|
||||
oldTop: Int,
|
||||
oldRight: Int,
|
||||
oldBottom: Int,
|
||||
) {
|
||||
val height = bottom - top
|
||||
if (height != behavior.peekHeight) {
|
||||
behavior.peekHeight = height
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGenericMotion(v: View?, event: MotionEvent): Boolean {
|
||||
if (event.source and InputDevice.SOURCE_CLASS_POINTER != 0) {
|
||||
if (event.actionMasked == MotionEvent.ACTION_SCROLL) {
|
||||
if (event.getAxisValue(MotionEvent.AXIS_VSCROLL) < 0f) {
|
||||
behavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||
} else {
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun lock() {
|
||||
lockCounter++
|
||||
updateLock()
|
||||
}
|
||||
|
||||
fun unlock() {
|
||||
lockCounter--
|
||||
if (lockCounter < 0) {
|
||||
lockCounter = 0
|
||||
}
|
||||
updateLock()
|
||||
}
|
||||
|
||||
private fun updateLock() {
|
||||
behavior.isDraggable = lockCounter <= 0
|
||||
pager.isUserInputEnabled = lockCounter <= 0
|
||||
tabLayout.setTabsEnabled(lockCounter <= 0)
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,8 @@ fun MangaDetails.mapChapters(
|
||||
fun List<ChapterListItem>.withVolumeHeaders(context: Context): List<ListModel> {
|
||||
var prevVolume = 0
|
||||
val result = ArrayList<ListModel>((size * 1.4).toInt())
|
||||
for (item in this) {
|
||||
var groupPos: Byte = 0
|
||||
for ((index, item) in this.withIndex()) {
|
||||
val chapter = item.chapter
|
||||
if (chapter.volume != prevVolume) {
|
||||
val text = if (chapter.volume == 0) {
|
||||
@@ -82,8 +83,19 @@ fun List<ChapterListItem>.withVolumeHeaders(context: Context): List<ListModel> {
|
||||
}
|
||||
result.add(ListHeader(text))
|
||||
prevVolume = chapter.volume
|
||||
groupPos = ChapterListItem.GROUP_START
|
||||
} else if (groupPos == ChapterListItem.GROUP_START) {
|
||||
groupPos = ChapterListItem.GROUP_MIDDLE
|
||||
}
|
||||
if (groupPos != 0.toByte()) {
|
||||
val next = this.getOrNull(index + 1)
|
||||
if (next == null || next.chapter.volume != prevVolume) {
|
||||
groupPos = ChapterListItem.GROUP_END
|
||||
}
|
||||
result.add(item.copy(groupPosition = groupPos))
|
||||
} else {
|
||||
result.add(item)
|
||||
}
|
||||
result.add(item)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
package org.koitharu.kotatsu.details.ui
|
||||
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.view.MenuProvider
|
||||
import org.koitharu.kotatsu.R
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
class ChaptersMenuProvider(
|
||||
private val viewModel: DetailsViewModel,
|
||||
private val bottomSheetMediator: ChaptersBottomSheetMediator?,
|
||||
) : OnBackPressedCallback(false), MenuProvider, SearchView.OnQueryTextListener, MenuItem.OnActionExpandListener {
|
||||
|
||||
private var searchItemRef: WeakReference<MenuItem>? = null
|
||||
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.opt_chapters, menu)
|
||||
val searchMenuItem = menu.findItem(R.id.action_search)
|
||||
searchMenuItem.setOnActionExpandListener(this)
|
||||
val searchView = searchMenuItem.actionView as SearchView
|
||||
searchView.setOnQueryTextListener(this)
|
||||
searchView.setIconifiedByDefault(false)
|
||||
searchView.queryHint = searchMenuItem.title
|
||||
searchItemRef = WeakReference(searchMenuItem)
|
||||
}
|
||||
|
||||
override fun onPrepareMenu(menu: Menu) {
|
||||
menu.findItem(R.id.action_search)?.isVisible = viewModel.isChaptersEmpty.value == false
|
||||
menu.findItem(R.id.action_reversed)?.isChecked = viewModel.isChaptersReversed.value == true
|
||||
menu.findItem(R.id.action_grid_view)?.isChecked = viewModel.isChaptersInGridView.value == true
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
|
||||
R.id.action_reversed -> {
|
||||
viewModel.setChaptersReversed(!menuItem.isChecked)
|
||||
true
|
||||
}
|
||||
R.id.action_grid_view-> {
|
||||
viewModel.setChaptersInGridView(!menuItem.isChecked)
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
|
||||
override fun handleOnBackPressed() {
|
||||
searchItemRef?.get()?.collapseActionView()
|
||||
}
|
||||
|
||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||
bottomSheetMediator?.lock()
|
||||
isEnabled = true
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||
isEnabled = false
|
||||
(item.actionView as? SearchView)?.setQuery("", false)
|
||||
viewModel.performChapterSearch(null)
|
||||
bottomSheetMediator?.unlock()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onQueryTextSubmit(query: String?): Boolean = false
|
||||
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
viewModel.performChapterSearch(newText)
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -8,66 +8,101 @@ import android.text.style.DynamicDrawableSpan
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.ImageSpan
|
||||
import android.text.style.RelativeSizeSpan
|
||||
import android.transition.AutoTransition
|
||||
import android.transition.Slide
|
||||
import android.transition.TransitionManager
|
||||
import android.view.Gravity
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.MarginLayoutParams
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.view.ViewTreeObserver
|
||||
import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.text.buildSpannedString
|
||||
import androidx.core.text.inSpans
|
||||
import androidx.core.view.MenuHost
|
||||
import androidx.core.text.method.LinkMovementMethodCompat
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
||||
import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import coil.request.SuccessResult
|
||||
import coil.transform.CircleCropTransformation
|
||||
import coil.util.CoilUtils
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filterNot
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
import org.koitharu.kotatsu.core.model.iconResId
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.model.titleResId
|
||||
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
||||
import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
|
||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||
import org.koitharu.kotatsu.core.ui.image.ChipIconTarget
|
||||
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.util.BottomSheetClollapseCallback
|
||||
import org.koitharu.kotatsu.core.ui.util.MenuInvalidator
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
||||
import org.koitharu.kotatsu.core.util.ext.doOnExpansionsChanged
|
||||
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.core.util.FileSize
|
||||
import org.koitharu.kotatsu.core.util.ViewBadge
|
||||
import org.koitharu.kotatsu.core.util.ext.crossfade
|
||||
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
|
||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
|
||||
import org.koitharu.kotatsu.core.util.ext.measureHeight
|
||||
import org.koitharu.kotatsu.core.util.ext.menuView
|
||||
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
|
||||
import org.koitharu.kotatsu.core.util.ext.isTextTruncated
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.core.util.ext.recyclerView
|
||||
import org.koitharu.kotatsu.core.util.ext.parentView
|
||||
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
|
||||
import org.koitharu.kotatsu.core.util.ext.setNavigationBarTransparentCompat
|
||||
import org.koitharu.kotatsu.core.util.ext.setNavigationIconSafe
|
||||
import org.koitharu.kotatsu.core.util.ext.setOnContextClickListenerCompat
|
||||
import org.koitharu.kotatsu.core.util.ext.source
|
||||
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
||||
import org.koitharu.kotatsu.databinding.ActivityDetailsBinding
|
||||
import org.koitharu.kotatsu.details.data.MangaDetails
|
||||
import org.koitharu.kotatsu.details.data.ReadingTime
|
||||
import org.koitharu.kotatsu.details.service.MangaPrefetchService
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||
import org.koitharu.kotatsu.details.ui.model.HistoryInfo
|
||||
import org.koitharu.kotatsu.details.ui.pager.DetailsPagerAdapter
|
||||
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesSheet
|
||||
import org.koitharu.kotatsu.details.ui.related.RelatedMangaActivity
|
||||
import org.koitharu.kotatsu.details.ui.scrobbling.ScrobblingItemDecoration
|
||||
import org.koitharu.kotatsu.details.ui.scrobbling.ScrollingInfoAdapter
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver
|
||||
import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteSheet
|
||||
import org.koitharu.kotatsu.image.ui.ImageActivity
|
||||
import org.koitharu.kotatsu.list.domain.ListExtraProvider
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||
import org.koitharu.kotatsu.list.ui.adapter.mangaGridItemAD
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.MangaItemModel
|
||||
import org.koitharu.kotatsu.list.ui.size.StaticItemSizeResolver
|
||||
import org.koitharu.kotatsu.local.ui.info.LocalInfoDialog
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
|
||||
import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet
|
||||
import java.lang.ref.WeakReference
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.util.ellipsize
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
||||
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet
|
||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||
import org.koitharu.kotatsu.search.ui.SearchActivity
|
||||
import org.koitharu.kotatsu.stats.ui.sheet.MangaStatsSheet
|
||||
import javax.inject.Inject
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
@@ -75,25 +110,22 @@ import com.google.android.material.R as materialR
|
||||
class DetailsActivity :
|
||||
BaseActivity<ActivityDetailsBinding>(),
|
||||
View.OnClickListener,
|
||||
NoModalBottomSheetOwner,
|
||||
View.OnLongClickListener,
|
||||
PopupMenu.OnMenuItemClickListener {
|
||||
View.OnLongClickListener, PopupMenu.OnMenuItemClickListener, View.OnLayoutChangeListener,
|
||||
ViewTreeObserver.OnDrawListener, ChipsView.OnChipClickListener, OnListItemClickListener<Bookmark> {
|
||||
|
||||
@Inject
|
||||
lateinit var appShortcutManager: AppShortcutManager
|
||||
lateinit var shortcutManager: AppShortcutManager
|
||||
|
||||
@Inject
|
||||
lateinit var settings: AppSettings
|
||||
lateinit var coil: ImageLoader
|
||||
|
||||
private var buttonTip: WeakReference<ButtonTip>? = null
|
||||
@Inject
|
||||
lateinit var tagHighlighter: ListExtraProvider
|
||||
|
||||
private val viewModel: DetailsViewModel by viewModels()
|
||||
|
||||
val secondaryMenuHost: MenuHost
|
||||
get() = viewBinding.toolbarChapters ?: this
|
||||
|
||||
var bottomSheetMediator: ChaptersBottomSheetMediator? = null
|
||||
private set
|
||||
private lateinit var chaptersBadge: ViewBadge
|
||||
private lateinit var menuProvider: DetailsMenuProvider
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -105,78 +137,157 @@ class DetailsActivity :
|
||||
viewBinding.buttonRead.setOnClickListener(this)
|
||||
viewBinding.buttonRead.setOnLongClickListener(this)
|
||||
viewBinding.buttonRead.setOnContextClickListenerCompat(this)
|
||||
viewBinding.buttonDropdown.setOnClickListener(this)
|
||||
|
||||
if (viewBinding.layoutBottom != null) {
|
||||
val behavior = BottomSheetBehavior.from(checkNotNull(viewBinding.layoutBottom))
|
||||
val bsMediator = ChaptersBottomSheetMediator(behavior, viewBinding.pager, viewBinding.tabs)
|
||||
actionModeDelegate.addListener(bsMediator)
|
||||
checkNotNull(viewBinding.layoutBsHeader).addOnLayoutChangeListener(bsMediator)
|
||||
onBackPressedDispatcher.addCallback(bsMediator)
|
||||
bottomSheetMediator = bsMediator
|
||||
behavior.doOnExpansionsChanged(::onChaptersSheetStateChanged)
|
||||
viewBinding.toolbarChapters?.setNavigationOnClickListener {
|
||||
behavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||
}
|
||||
viewBinding.toolbarChapters?.setOnGenericMotionListener(bsMediator)
|
||||
viewBinding.buttonDownload?.setOnClickListener(this)
|
||||
viewBinding.infoLayout.chipBranch.setOnClickListener(this)
|
||||
viewBinding.infoLayout.chipSize.setOnClickListener(this)
|
||||
viewBinding.infoLayout.chipSource.setOnClickListener(this)
|
||||
viewBinding.infoLayout.chipFavorite.setOnClickListener(this)
|
||||
viewBinding.infoLayout.chipAuthor.setOnClickListener(this)
|
||||
viewBinding.infoLayout.chipTime.setOnClickListener(this)
|
||||
viewBinding.imageViewCover.setOnClickListener(this)
|
||||
viewBinding.buttonDescriptionMore.setOnClickListener(this)
|
||||
viewBinding.buttonScrobblingMore.setOnClickListener(this)
|
||||
viewBinding.buttonRelatedMore.setOnClickListener(this)
|
||||
viewBinding.infoLayout.chipSource.setOnClickListener(this)
|
||||
viewBinding.infoLayout.chipSize.setOnClickListener(this)
|
||||
viewBinding.textViewDescription.addOnLayoutChangeListener(this)
|
||||
viewBinding.textViewDescription.viewTreeObserver.addOnDrawListener(this)
|
||||
viewBinding.textViewDescription.movementMethod = LinkMovementMethodCompat.getInstance()
|
||||
viewBinding.chipsTags.onChipClickListener = this
|
||||
TitleScrollCoordinator(viewBinding.textViewTitle).attach(viewBinding.scrollView)
|
||||
viewBinding.containerBottomSheet?.let { BottomSheetBehavior.from(it) }?.let { behavior ->
|
||||
onBackPressedDispatcher.addCallback(BottomSheetClollapseCallback(behavior))
|
||||
}
|
||||
initPager()
|
||||
chaptersBadge = ViewBadge(viewBinding.buttonRead, this)
|
||||
|
||||
viewModel.manga.filterNotNull().observe(this, ::onMangaUpdated)
|
||||
viewModel.details.filterNotNull().observe(this, ::onMangaUpdated)
|
||||
viewModel.onMangaRemoved.observeEvent(this, ::onMangaRemoved)
|
||||
viewModel.newChaptersCount.observe(this, ::onNewChaptersChanged)
|
||||
viewModel.onError.observeEvent(this, DetailsErrorObserver(this, viewModel, exceptionResolver))
|
||||
viewModel.onActionDone.observeEvent(
|
||||
this,
|
||||
ReversibleActionObserver(viewBinding.containerDetails, viewBinding.layoutBottom),
|
||||
)
|
||||
viewModel.onShowTip.observeEvent(this) { showTip() }
|
||||
viewModel.historyInfo.observe(this, ::onHistoryChanged)
|
||||
viewModel.selectedBranch.observe(this) {
|
||||
viewBinding.toolbarChapters?.subtitle = it
|
||||
viewBinding.textViewSubtitle?.textAndVisible = it
|
||||
viewModel.onError
|
||||
.filterNot { ChaptersPagesSheet.isShown(supportFragmentManager) }
|
||||
.observeEvent(this, DetailsErrorObserver(this, viewModel, exceptionResolver))
|
||||
viewModel.onActionDone
|
||||
.filterNot { ChaptersPagesSheet.isShown(supportFragmentManager) }
|
||||
.observeEvent(this, ReversibleActionObserver(viewBinding.scrollView, null))
|
||||
combine(viewModel.historyInfo, viewModel.isLoading, ::Pair).observe(this) {
|
||||
onHistoryChanged(it.first, it.second)
|
||||
}
|
||||
val chaptersMenuInvalidator = MenuInvalidator(viewBinding.toolbarChapters ?: this)
|
||||
viewModel.isChaptersReversed.observe(this, chaptersMenuInvalidator)
|
||||
viewModel.isChaptersEmpty.observe(this, chaptersMenuInvalidator)
|
||||
viewModel.isLoading.observe(this, ::onLoadingStateChanged)
|
||||
viewModel.scrobblingInfo.observe(this, ::onScrobblingInfoChanged)
|
||||
viewModel.localSize.observe(this, ::onLocalSizeChanged)
|
||||
viewModel.relatedManga.observe(this, ::onRelatedMangaChanged)
|
||||
viewModel.readingTime.observe(this, ::onReadingTimeChanged)
|
||||
viewModel.selectedBranch.observe(this) {
|
||||
viewBinding.infoLayout.chipBranch.text = it.ifNullOrEmpty { getString(R.string.system_default) }
|
||||
}
|
||||
viewModel.favouriteCategories.observe(this, ::onFavoritesChanged)
|
||||
val menuInvalidator = MenuInvalidator(this)
|
||||
viewModel.favouriteCategories.observe(this, menuInvalidator)
|
||||
viewModel.isStatsAvailable.observe(this, menuInvalidator)
|
||||
viewModel.remoteManga.observe(this, menuInvalidator)
|
||||
viewModel.branches.observe(this) {
|
||||
viewBinding.buttonDropdown.isVisible = it.size > 1
|
||||
viewBinding.infoLayout.chipBranch.isVisible = it.size > 1
|
||||
}
|
||||
viewModel.chapters.observe(this, PrefetchObserver(this))
|
||||
viewModel.onDownloadStarted.observeEvent(
|
||||
this,
|
||||
DownloadStartedObserver(viewBinding.containerDetails),
|
||||
)
|
||||
viewModel.onDownloadStarted
|
||||
.filterNot { ChaptersPagesSheet.isShown(supportFragmentManager) }
|
||||
.observeEvent(this, DownloadStartedObserver(viewBinding.scrollView))
|
||||
|
||||
addMenuProvider(
|
||||
DetailsMenuProvider(
|
||||
activity = this,
|
||||
viewModel = viewModel,
|
||||
snackbarHost = viewBinding.pager,
|
||||
appShortcutManager = appShortcutManager,
|
||||
),
|
||||
menuProvider = DetailsMenuProvider(
|
||||
activity = this,
|
||||
viewModel = viewModel,
|
||||
snackbarHost = viewBinding.scrollView,
|
||||
appShortcutManager = shortcutManager,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getBottomSheetCollapsedHeight(): Int {
|
||||
return viewBinding.layoutBsHeader?.measureHeight() ?: 0
|
||||
addMenuProvider(menuProvider)
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.button_read -> openReader(isIncognitoMode = false)
|
||||
R.id.button_dropdown -> showBranchPopupMenu(v)
|
||||
R.id.chip_branch -> showBranchPopupMenu(v)
|
||||
R.id.button_download -> DownloadDialogHelper(v, viewModel).show(menuProvider)
|
||||
|
||||
R.id.chip_author -> {
|
||||
val manga = viewModel.manga.value ?: return
|
||||
startActivity(
|
||||
SearchActivity.newIntent(
|
||||
context = v.context,
|
||||
source = manga.source,
|
||||
query = manga.author ?: return,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
R.id.chip_source -> {
|
||||
val manga = viewModel.manga.value ?: return
|
||||
startActivity(
|
||||
MangaListActivity.newIntent(
|
||||
context = v.context,
|
||||
source = manga.source,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
R.id.chip_size -> {
|
||||
val manga = viewModel.manga.value ?: return
|
||||
LocalInfoDialog.show(supportFragmentManager, manga)
|
||||
}
|
||||
|
||||
R.id.chip_favorite -> {
|
||||
val manga = viewModel.manga.value ?: return
|
||||
FavoriteSheet.show(supportFragmentManager, manga)
|
||||
}
|
||||
|
||||
R.id.chip_time -> {
|
||||
if (viewModel.isStatsAvailable.value) {
|
||||
val manga = viewModel.manga.value ?: return
|
||||
MangaStatsSheet.show(supportFragmentManager, manga)
|
||||
} else {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
||||
R.id.imageView_cover -> {
|
||||
val manga = viewModel.manga.value ?: return
|
||||
startActivity(
|
||||
ImageActivity.newIntent(
|
||||
v.context,
|
||||
manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl },
|
||||
manga.source,
|
||||
),
|
||||
scaleUpActivityOptionsOf(v),
|
||||
)
|
||||
}
|
||||
|
||||
R.id.button_description_more -> {
|
||||
val tv = viewBinding.textViewDescription
|
||||
TransitionManager.beginDelayedTransition(tv.parentView)
|
||||
if (tv.maxLines in 1 until Integer.MAX_VALUE) {
|
||||
tv.maxLines = Integer.MAX_VALUE
|
||||
} else {
|
||||
tv.maxLines = resources.getInteger(R.integer.details_description_lines)
|
||||
}
|
||||
}
|
||||
|
||||
R.id.button_scrobbling_more -> {
|
||||
val manga = viewModel.manga.value ?: return
|
||||
ScrobblingSelectorSheet.show(supportFragmentManager, manga, null)
|
||||
}
|
||||
|
||||
R.id.button_related_more -> {
|
||||
val manga = viewModel.manga.value ?: return
|
||||
startActivity(RelatedMangaActivity.newIntent(v.context, manga))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onChipClick(chip: Chip, data: Any?) {
|
||||
val tag = data as? MangaTag ?: return
|
||||
startActivity(MangaListActivity.newIntent(this, setOf(tag)))
|
||||
}
|
||||
|
||||
override fun onLongClick(v: View): Boolean = when (v.id) {
|
||||
R.id.button_read -> {
|
||||
buttonTip?.get()?.remove()
|
||||
buttonTip = null
|
||||
val menu = PopupMenu(v.context, v)
|
||||
menu.inflate(R.menu.popup_read)
|
||||
menu.menu.findItem(R.id.action_forget)?.isVisible = viewModel.historyInfo.value.run {
|
||||
@@ -203,46 +314,197 @@ class DetailsActivity :
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_pages_thumbs -> {
|
||||
val history = viewModel.historyInfo.value.history
|
||||
PagesThumbnailsSheet.show(
|
||||
fm = supportFragmentManager,
|
||||
manga = viewModel.manga.value ?: return false,
|
||||
chapterId = history?.chapterId
|
||||
?: viewModel.chapters.value.firstOrNull()?.chapter?.id
|
||||
?: return false,
|
||||
currentPage = history?.page ?: 0,
|
||||
)
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun onChaptersSheetStateChanged(isExpanded: Boolean) {
|
||||
val toolbar = viewBinding.toolbarChapters ?: return
|
||||
if (isAnimationsEnabled) {
|
||||
val transition = AutoTransition()
|
||||
transition.duration = getAnimationDuration(R.integer.config_shorterAnimTime)
|
||||
TransitionManager.beginDelayedTransition(toolbar, transition)
|
||||
}
|
||||
if (isExpanded) {
|
||||
toolbar.setNavigationIconSafe(materialR.drawable.abc_ic_clear_material)
|
||||
} else {
|
||||
toolbar.navigationIcon = null
|
||||
}
|
||||
toolbar.menuView?.isVisible = isExpanded
|
||||
viewBinding.buttonRead.isGone = isExpanded
|
||||
override fun onItemClick(item: Bookmark, view: View) {
|
||||
startActivity(
|
||||
ReaderActivity.IntentBuilder(view.context).bookmark(item).incognito(true).build(),
|
||||
)
|
||||
Toast.makeText(view.context, R.string.incognito_mode, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun onMangaUpdated(manga: Manga) {
|
||||
title = manga.title
|
||||
val hasChapters = !manga.chapters.isNullOrEmpty()
|
||||
viewBinding.buttonRead.isEnabled = hasChapters
|
||||
invalidateOptionsMenu()
|
||||
showBottomSheet(manga.chapters != null)
|
||||
viewBinding.groupHeader?.isVisible = hasChapters
|
||||
override fun onDraw() {
|
||||
viewBinding.run {
|
||||
buttonDescriptionMore.isVisible = textViewDescription.maxLines == Int.MAX_VALUE ||
|
||||
textViewDescription.isTextTruncated
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLayoutChange(
|
||||
v: View?,
|
||||
left: Int,
|
||||
top: Int,
|
||||
right: Int,
|
||||
bottom: Int,
|
||||
oldLeft: Int,
|
||||
oldTop: Int,
|
||||
oldRight: Int,
|
||||
oldBottom: Int
|
||||
) {
|
||||
with(viewBinding) {
|
||||
buttonDescriptionMore.isVisible = textViewDescription.isTextTruncated
|
||||
}
|
||||
}
|
||||
|
||||
private fun onFavoritesChanged(categories: Set<FavouriteCategory>) {
|
||||
val chip = viewBinding.infoLayout.chipFavorite
|
||||
chip.setChipIconResource(if (categories.isEmpty()) R.drawable.ic_heart_outline else R.drawable.ic_heart)
|
||||
chip.text = if (categories.isEmpty()) {
|
||||
getString(R.string.add_to_favourites)
|
||||
} else {
|
||||
if (categories.size == 1) {
|
||||
categories.first().title.ellipsize(FAV_LABEL_LIMIT)
|
||||
}
|
||||
buildString(FAV_LABEL_LIMIT + 6) {
|
||||
for ((i, cat) in categories.withIndex()) {
|
||||
if (i == 0) {
|
||||
append(cat.title.ellipsize(FAV_LABEL_LIMIT - 4))
|
||||
} else if (length + cat.title.length > FAV_LABEL_LIMIT) {
|
||||
append(", ")
|
||||
append(getString(R.string.list_ellipsize_pattern, categories.size - i))
|
||||
break
|
||||
} else {
|
||||
append(", ")
|
||||
append(cat.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onReadingTimeChanged(time: ReadingTime?) {
|
||||
val chip = viewBinding.infoLayout.chipTime
|
||||
chip.textAndVisible = time?.formatShort(chip.resources)
|
||||
}
|
||||
|
||||
private fun onDescriptionChanged(description: CharSequence?) {
|
||||
val tv = viewBinding.textViewDescription
|
||||
if (description.isNullOrBlank()) {
|
||||
tv.setText(R.string.no_description)
|
||||
} else {
|
||||
tv.text = description
|
||||
}
|
||||
}
|
||||
|
||||
private fun onLocalSizeChanged(size: Long) {
|
||||
val chip = viewBinding.infoLayout.chipSize
|
||||
if (size == 0L) {
|
||||
chip.isVisible = false
|
||||
} else {
|
||||
chip.text = FileSize.BYTES.format(chip.context, size)
|
||||
chip.isVisible = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRelatedMangaChanged(related: List<MangaItemModel>) {
|
||||
if (related.isEmpty()) {
|
||||
viewBinding.groupRelated.isVisible = false
|
||||
return
|
||||
}
|
||||
val rv = viewBinding.recyclerViewRelated
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val adapter = (rv.adapter as? BaseListAdapter<ListModel>) ?: BaseListAdapter<ListModel>()
|
||||
.addDelegate(
|
||||
ListItemType.MANGA_GRID,
|
||||
mangaGridItemAD(
|
||||
coil, this,
|
||||
StaticItemSizeResolver(resources.getDimensionPixelSize(R.dimen.smaller_grid_width)),
|
||||
) { item, view ->
|
||||
startActivity(newIntent(view.context, item))
|
||||
},
|
||||
).also { rv.adapter = it }
|
||||
adapter.items = related
|
||||
viewBinding.groupRelated.isVisible = true
|
||||
}
|
||||
|
||||
private fun onLoadingStateChanged(isLoading: Boolean) {
|
||||
val button = viewBinding.buttonDownload ?: return
|
||||
if (isLoading) {
|
||||
button.setImageDrawable(
|
||||
CircularProgressDrawable(this).also {
|
||||
it.setStyle(CircularProgressDrawable.LARGE)
|
||||
it.setColorSchemeColors(getThemeColor(materialR.attr.colorControlNormal))
|
||||
it.start()
|
||||
},
|
||||
)
|
||||
} else {
|
||||
button.setImageResource(R.drawable.ic_download)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onScrobblingInfoChanged(scrobblings: List<ScrobblingInfo>) {
|
||||
var adapter = viewBinding.recyclerViewScrobbling.adapter as? ScrollingInfoAdapter
|
||||
viewBinding.groupScrobbling.isGone = scrobblings.isEmpty()
|
||||
if (adapter != null) {
|
||||
adapter.items = scrobblings
|
||||
} else {
|
||||
adapter = ScrollingInfoAdapter(this, coil, supportFragmentManager)
|
||||
adapter.items = scrobblings
|
||||
viewBinding.recyclerViewScrobbling.adapter = adapter
|
||||
viewBinding.recyclerViewScrobbling.addItemDecoration(ScrobblingItemDecoration())
|
||||
}
|
||||
}
|
||||
|
||||
private fun onMangaUpdated(details: MangaDetails) {
|
||||
with(viewBinding) {
|
||||
val manga = details.toManga()
|
||||
// Main
|
||||
loadCover(manga)
|
||||
textViewTitle.text = manga.title
|
||||
textViewSubtitle.textAndVisible = manga.altTitle
|
||||
infoLayout.chipAuthor.textAndVisible = manga.author
|
||||
if (manga.hasRating) {
|
||||
ratingBar.rating = manga.rating * ratingBar.numStars
|
||||
ratingBar.isVisible = true
|
||||
} else {
|
||||
ratingBar.isVisible = false
|
||||
}
|
||||
|
||||
manga.state?.let { state ->
|
||||
textViewState.textAndVisible = resources.getString(state.titleResId)
|
||||
imageViewState.setImageResource(state.iconResId)
|
||||
imageViewState.isVisible = true
|
||||
} ?: run {
|
||||
textViewState.isVisible = false
|
||||
imageViewState.isVisible = false
|
||||
}
|
||||
|
||||
if (manga.source == MangaSource.LOCAL || manga.source == MangaSource.DUMMY) {
|
||||
infoLayout.chipSource.isVisible = false
|
||||
} else {
|
||||
infoLayout.chipSource.text = manga.source.title
|
||||
infoLayout.chipSource.isVisible = true
|
||||
}
|
||||
|
||||
textViewNsfw.isVisible = manga.isNsfw
|
||||
|
||||
// Chips
|
||||
bindTags(manga)
|
||||
|
||||
textViewDescription.text = details.description.ifNullOrEmpty { getString(R.string.no_description) }
|
||||
|
||||
viewBinding.infoLayout.chipSource.also { chip ->
|
||||
ImageRequest.Builder(this@DetailsActivity)
|
||||
.data(manga.source.faviconUri())
|
||||
.lifecycle(this@DetailsActivity)
|
||||
.crossfade(false)
|
||||
.size(resources.getDimensionPixelSize(materialR.dimen.m3_chip_icon_size))
|
||||
.target(ChipIconTarget(chip))
|
||||
.placeholder(R.drawable.ic_web)
|
||||
.fallback(R.drawable.ic_web)
|
||||
.error(R.drawable.ic_web)
|
||||
.source(manga.source)
|
||||
.transformations(CircleCropTransformation())
|
||||
.allowRgb565(true)
|
||||
.enqueueWith(coil)
|
||||
}
|
||||
|
||||
title = manga.title
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onMangaRemoved(manga: Manga) {
|
||||
@@ -259,60 +521,37 @@ class DetailsActivity :
|
||||
left = insets.left,
|
||||
right = insets.right,
|
||||
)
|
||||
if (insets.bottom > 0) {
|
||||
window.setNavigationBarTransparentCompat(
|
||||
this,
|
||||
viewBinding.layoutBottom?.elevation ?: 0f,
|
||||
0.9f,
|
||||
)
|
||||
}
|
||||
viewBinding.cardChapters?.updateLayoutParams<MarginLayoutParams> {
|
||||
bottomMargin = insets.bottom + marginEnd
|
||||
val baseOffset = leftMargin
|
||||
bottomMargin = insets.bottom + baseOffset
|
||||
topMargin = insets.bottom + baseOffset
|
||||
}
|
||||
viewBinding.dragHandle?.updateLayoutParams<MarginLayoutParams> {
|
||||
bottomMargin = insets.top
|
||||
viewBinding.scrollView.updatePadding(
|
||||
bottom = insets.bottom,
|
||||
)
|
||||
viewBinding.containerBottomSheet?.let { bs ->
|
||||
window.setNavigationBarTransparentCompat(this, bs.elevation, 0.9f)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onHistoryChanged(info: HistoryInfo) {
|
||||
with(viewBinding.buttonRead) {
|
||||
if (info.history != null) {
|
||||
setText(R.string._continue)
|
||||
setIconResource(if (info.isIncognitoMode) R.drawable.ic_incognito else R.drawable.ic_play)
|
||||
} else {
|
||||
setText(R.string.read)
|
||||
setIconResource(if (info.isIncognitoMode) R.drawable.ic_incognito else R.drawable.ic_play)
|
||||
}
|
||||
}
|
||||
val text = when {
|
||||
!info.isValid -> getString(R.string.loading_)
|
||||
info.currentChapter >= 0 -> getString(
|
||||
R.string.chapter_d_of_d,
|
||||
info.currentChapter + 1,
|
||||
info.totalChapters,
|
||||
)
|
||||
|
||||
private fun onHistoryChanged(info: HistoryInfo, isLoading: Boolean) = with(viewBinding) {
|
||||
buttonRead.setTitle(if (info.canContinue) R.string._continue else R.string.read)
|
||||
buttonRead.subtitle = when {
|
||||
isLoading -> getString(R.string.loading_)
|
||||
info.isIncognitoMode -> getString(R.string.incognito_mode)
|
||||
info.isChapterMissing -> getString(R.string.chapter_is_missing)
|
||||
info.currentChapter >= 0 -> getString(R.string.chapter_d_of_d, info.currentChapter + 1, info.totalChapters)
|
||||
info.totalChapters == 0 -> getString(R.string.no_chapters)
|
||||
else -> resources.getQuantityString(
|
||||
R.plurals.chapters,
|
||||
info.totalChapters,
|
||||
info.totalChapters,
|
||||
)
|
||||
info.totalChapters == -1 -> getString(R.string.error_occurred)
|
||||
else -> resources.getQuantityString(R.plurals.chapters, info.totalChapters, info.totalChapters)
|
||||
}
|
||||
viewBinding.toolbarChapters?.title = text
|
||||
viewBinding.textViewTitle?.text = text
|
||||
buttonRead.setProgress(info.history?.percent?.coerceIn(0f, 1f) ?: 0f, true)
|
||||
buttonDownload?.isEnabled = info.isValid && info.canDownload
|
||||
buttonRead.isEnabled = info.isValid
|
||||
}
|
||||
|
||||
private fun onNewChaptersChanged(count: Int) {
|
||||
val tab = viewBinding.tabs.getTabAt(0) ?: return
|
||||
if (count == 0) {
|
||||
tab.removeBadge()
|
||||
} else {
|
||||
val badge = tab.orCreateBadge
|
||||
badge.horizontalOffsetWithText = -resources.getDimensionPixelOffset(R.dimen.margin_small)
|
||||
badge.number = count
|
||||
badge.isVisible = true
|
||||
}
|
||||
chaptersBadge.counter = count
|
||||
}
|
||||
|
||||
private fun showBranchPopupMenu(v: View) {
|
||||
@@ -347,8 +586,11 @@ class DetailsActivity :
|
||||
append(branch.count.toString())
|
||||
}
|
||||
}
|
||||
menu.menu.add(Menu.NONE, Menu.NONE, i, title)
|
||||
val item = menu.menu.add(R.id.group_branches, Menu.NONE, i, title)
|
||||
item.isCheckable = true
|
||||
item.isChecked = branch.isSelected
|
||||
}
|
||||
menu.menu.setGroupCheckable(R.id.group_branches, true, true)
|
||||
menu.setOnMenuItemClickListener {
|
||||
viewModel.setSelectedBranch(branches.getOrNull(it.order)?.name)
|
||||
true
|
||||
@@ -360,11 +602,11 @@ class DetailsActivity :
|
||||
val manga = viewModel.manga.value ?: return
|
||||
val chapterId = viewModel.historyInfo.value.history?.chapterId
|
||||
if (chapterId != null && manga.chapters?.none { x -> x.id == chapterId } == true) {
|
||||
Snackbar.make(viewBinding.containerDetails, R.string.chapter_is_missing, Snackbar.LENGTH_SHORT)
|
||||
Snackbar.make(viewBinding.scrollView, R.string.chapter_is_missing, Snackbar.LENGTH_SHORT)
|
||||
.show()
|
||||
} else {
|
||||
startActivity(
|
||||
IntentBuilder(this)
|
||||
ReaderActivity.IntentBuilder(this)
|
||||
.manga(manga)
|
||||
.branch(viewModel.selectedBranchValue)
|
||||
.incognito(isIncognitoMode)
|
||||
@@ -376,24 +618,45 @@ class DetailsActivity :
|
||||
}
|
||||
}
|
||||
|
||||
private fun initPager() {
|
||||
val adapter = DetailsPagerAdapter(this, settings)
|
||||
viewBinding.pager.recyclerView?.isNestedScrollingEnabled = false
|
||||
viewBinding.pager.offscreenPageLimit = 1
|
||||
viewBinding.pager.adapter = adapter
|
||||
TabLayoutMediator(viewBinding.tabs, viewBinding.pager, adapter).attach()
|
||||
viewBinding.pager.setCurrentItem(settings.defaultDetailsTab, false)
|
||||
viewBinding.tabs.isVisible = adapter.itemCount > 1
|
||||
private fun bindTags(manga: Manga) {
|
||||
viewBinding.chipsTags.isVisible = manga.tags.isNotEmpty()
|
||||
viewBinding.chipsTags.setChips(
|
||||
manga.tags.map { tag ->
|
||||
ChipsView.ChipModel(
|
||||
title = tag.title,
|
||||
tint = tagHighlighter.getTagTint(tag),
|
||||
icon = 0,
|
||||
data = tag,
|
||||
isCheckable = false,
|
||||
isChecked = false,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun showBottomSheet(isVisible: Boolean) {
|
||||
val view = viewBinding.layoutBottom ?: return
|
||||
if (view.isVisible == isVisible) return
|
||||
val transition = Slide(Gravity.BOTTOM)
|
||||
transition.addTarget(view)
|
||||
transition.interpolator = AccelerateDecelerateInterpolator()
|
||||
TransitionManager.beginDelayedTransition(viewBinding.root as ViewGroup, transition)
|
||||
view.isVisible = isVisible
|
||||
private fun loadCover(manga: Manga) {
|
||||
val imageUrl = manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl }
|
||||
val lastResult = CoilUtils.result(viewBinding.imageViewCover)
|
||||
if (lastResult is SuccessResult && lastResult.request.data == imageUrl) {
|
||||
return
|
||||
}
|
||||
val request = ImageRequest.Builder(this)
|
||||
.target(viewBinding.imageViewCover)
|
||||
.size(CoverSizeResolver(viewBinding.imageViewCover))
|
||||
.data(imageUrl)
|
||||
.tag(manga.source)
|
||||
.crossfade(this)
|
||||
.lifecycle(this)
|
||||
.placeholderMemoryCacheKey(manga.coverUrl)
|
||||
val previousDrawable = lastResult?.drawable
|
||||
if (previousDrawable != null) {
|
||||
request.fallback(previousDrawable)
|
||||
.placeholder(previousDrawable)
|
||||
.error(previousDrawable)
|
||||
} else {
|
||||
request.defaultPlaceholders(this)
|
||||
}
|
||||
request.enqueueWith(coil)
|
||||
}
|
||||
|
||||
private class PrefetchObserver(
|
||||
@@ -414,34 +677,18 @@ class DetailsActivity :
|
||||
}
|
||||
}
|
||||
|
||||
private fun showTip() {
|
||||
val tip = ButtonTip(viewBinding.root as ViewGroup, insetsDelegate, viewModel)
|
||||
tip.addToRoot()
|
||||
buttonTip = WeakReference(tip)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val TIP_BUTTON = "btn_read"
|
||||
private const val KEY_NEW_ACTIVITY = "new_details_screen"
|
||||
private const val FAV_LABEL_LIMIT = 10
|
||||
|
||||
fun newIntent(context: Context, manga: Manga): Intent {
|
||||
return getActivityIntent(context)
|
||||
return Intent(context, DetailsActivity::class.java)
|
||||
.putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga))
|
||||
}
|
||||
|
||||
fun newIntent(context: Context, mangaId: Long): Intent {
|
||||
return getActivityIntent(context)
|
||||
return Intent(context, DetailsActivity::class.java)
|
||||
.putExtra(MangaIntent.KEY_ID, mangaId)
|
||||
}
|
||||
|
||||
private fun getActivityIntent(context: Context): Intent {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val useNewActivity = prefs.getBoolean(KEY_NEW_ACTIVITY, false)
|
||||
return Intent(
|
||||
context,
|
||||
if (useNewActivity) DetailsActivity2::class.java else DetailsActivity::class.java,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,703 +0,0 @@
|
||||
package org.koitharu.kotatsu.details.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.text.style.DynamicDrawableSpan
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.ImageSpan
|
||||
import android.text.style.RelativeSizeSpan
|
||||
import android.transition.TransitionManager
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewTreeObserver
|
||||
import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.text.buildSpannedString
|
||||
import androidx.core.text.inSpans
|
||||
import androidx.core.text.method.LinkMovementMethodCompat
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
||||
import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import coil.request.SuccessResult
|
||||
import coil.transform.CircleCropTransformation
|
||||
import coil.util.CoilUtils
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
import org.koitharu.kotatsu.core.model.iconResId
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.model.titleResId
|
||||
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
||||
import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
|
||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||
import org.koitharu.kotatsu.core.ui.image.ChipIconTarget
|
||||
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration
|
||||
import org.koitharu.kotatsu.core.ui.util.MenuInvalidator
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.core.util.FileSize
|
||||
import org.koitharu.kotatsu.core.util.ViewBadge
|
||||
import org.koitharu.kotatsu.core.util.ext.crossfade
|
||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
|
||||
import org.koitharu.kotatsu.core.util.ext.isTextTruncated
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.core.util.ext.parentView
|
||||
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
|
||||
import org.koitharu.kotatsu.core.util.ext.setOnContextClickListenerCompat
|
||||
import org.koitharu.kotatsu.core.util.ext.source
|
||||
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
||||
import org.koitharu.kotatsu.databinding.ActivityDetailsNewBinding
|
||||
import org.koitharu.kotatsu.details.data.MangaDetails
|
||||
import org.koitharu.kotatsu.details.data.ReadingTime
|
||||
import org.koitharu.kotatsu.details.service.MangaPrefetchService
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||
import org.koitharu.kotatsu.details.ui.model.HistoryInfo
|
||||
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesSheet
|
||||
import org.koitharu.kotatsu.details.ui.related.RelatedMangaActivity
|
||||
import org.koitharu.kotatsu.details.ui.scrobbling.ScrobblingItemDecoration
|
||||
import org.koitharu.kotatsu.details.ui.scrobbling.ScrollingInfoAdapter
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteSheet
|
||||
import org.koitharu.kotatsu.image.ui.ImageActivity
|
||||
import org.koitharu.kotatsu.list.domain.ListExtraProvider
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||
import org.koitharu.kotatsu.list.ui.adapter.mangaGridItemAD
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.MangaItemModel
|
||||
import org.koitharu.kotatsu.list.ui.size.StaticItemSizeResolver
|
||||
import org.koitharu.kotatsu.local.ui.info.LocalInfoDialog
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.util.ellipsize
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
||||
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet
|
||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||
import org.koitharu.kotatsu.search.ui.SearchActivity
|
||||
import org.koitharu.kotatsu.stats.ui.sheet.MangaStatsSheet
|
||||
import javax.inject.Inject
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
@AndroidEntryPoint
|
||||
class DetailsActivity2 :
|
||||
BaseActivity<ActivityDetailsNewBinding>(),
|
||||
View.OnClickListener,
|
||||
View.OnLongClickListener, PopupMenu.OnMenuItemClickListener, View.OnLayoutChangeListener,
|
||||
ViewTreeObserver.OnDrawListener, ChipsView.OnChipClickListener, OnListItemClickListener<Bookmark> {
|
||||
|
||||
@Inject
|
||||
lateinit var shortcutManager: AppShortcutManager
|
||||
|
||||
@Inject
|
||||
lateinit var coil: ImageLoader
|
||||
|
||||
@Inject
|
||||
lateinit var tagHighlighter: ListExtraProvider
|
||||
|
||||
private val viewModel: DetailsViewModel by viewModels()
|
||||
|
||||
var bottomSheetMediator: ChaptersBottomSheetMediator? = null
|
||||
private set
|
||||
|
||||
private lateinit var chaptersBadge: ViewBadge
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(ActivityDetailsNewBinding.inflate(layoutInflater))
|
||||
supportActionBar?.run {
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
setDisplayShowTitleEnabled(false)
|
||||
}
|
||||
viewBinding.buttonRead.setOnClickListener(this)
|
||||
viewBinding.buttonRead.setOnLongClickListener(this)
|
||||
viewBinding.buttonRead.setOnContextClickListenerCompat(this)
|
||||
viewBinding.buttonChapters.setOnClickListener(this)
|
||||
viewBinding.infoLayout.chipBranch.setOnClickListener(this)
|
||||
viewBinding.infoLayout.chipSize.setOnClickListener(this)
|
||||
viewBinding.infoLayout.chipSource.setOnClickListener(this)
|
||||
viewBinding.infoLayout.chipFavorite.setOnClickListener(this)
|
||||
viewBinding.infoLayout.chipAuthor.setOnClickListener(this)
|
||||
viewBinding.infoLayout.chipTime.setOnClickListener(this)
|
||||
viewBinding.imageViewCover.setOnClickListener(this)
|
||||
viewBinding.buttonDescriptionMore.setOnClickListener(this)
|
||||
viewBinding.buttonScrobblingMore.setOnClickListener(this)
|
||||
viewBinding.buttonRelatedMore.setOnClickListener(this)
|
||||
viewBinding.infoLayout.chipSource.setOnClickListener(this)
|
||||
viewBinding.infoLayout.chipSize.setOnClickListener(this)
|
||||
viewBinding.textViewDescription.addOnLayoutChangeListener(this)
|
||||
viewBinding.textViewDescription.viewTreeObserver.addOnDrawListener(this)
|
||||
viewBinding.textViewDescription.movementMethod = LinkMovementMethodCompat.getInstance()
|
||||
viewBinding.chipsTags.onChipClickListener = this
|
||||
viewBinding.recyclerViewRelated.addItemDecoration(
|
||||
SpacingItemDecoration(resources.getDimensionPixelOffset(R.dimen.grid_spacing)),
|
||||
)
|
||||
TitleScrollCoordinator(viewBinding.textViewTitle).attach(viewBinding.scrollView)
|
||||
|
||||
chaptersBadge = ViewBadge(viewBinding.buttonChapters, this)
|
||||
|
||||
viewModel.details.filterNotNull().observe(this, ::onMangaUpdated)
|
||||
viewModel.onMangaRemoved.observeEvent(this, ::onMangaRemoved)
|
||||
viewModel.newChaptersCount.observe(this, ::onNewChaptersChanged)
|
||||
viewModel.onError.observeEvent(
|
||||
this,
|
||||
SnackbarErrorObserver(viewBinding.scrollView, null, exceptionResolver) {
|
||||
if (it) viewModel.reload()
|
||||
},
|
||||
)
|
||||
viewModel.onActionDone.observeEvent(this, ReversibleActionObserver(viewBinding.scrollView, null))
|
||||
viewModel.historyInfo.observe(this, ::onHistoryChanged)
|
||||
viewModel.isLoading.observe(this, ::onLoadingStateChanged)
|
||||
viewModel.scrobblingInfo.observe(this, ::onScrobblingInfoChanged)
|
||||
viewModel.localSize.observe(this, ::onLocalSizeChanged)
|
||||
viewModel.relatedManga.observe(this, ::onRelatedMangaChanged)
|
||||
// viewModel.chapters.observe(this, ::onChaptersChanged)
|
||||
viewModel.readingTime.observe(this, ::onReadingTimeChanged)
|
||||
viewModel.selectedBranch.observe(this) {
|
||||
viewBinding.infoLayout.chipBranch.text = it.ifNullOrEmpty { getString(R.string.system_default) }
|
||||
}
|
||||
viewModel.favouriteCategories.observe(this, ::onFavoritesChanged)
|
||||
val menuInvalidator = MenuInvalidator(this)
|
||||
viewModel.isStatsAvailable.observe(this, menuInvalidator)
|
||||
viewModel.remoteManga.observe(this, menuInvalidator)
|
||||
viewModel.branches.observe(this) {
|
||||
viewBinding.infoLayout.chipBranch.isVisible = it.size > 1
|
||||
}
|
||||
viewModel.chapters.observe(this, PrefetchObserver(this))
|
||||
viewModel.onDownloadStarted.observeEvent(
|
||||
this,
|
||||
DownloadStartedObserver(viewBinding.scrollView),
|
||||
)
|
||||
|
||||
addMenuProvider(
|
||||
DetailsMenuProvider(
|
||||
activity = this,
|
||||
viewModel = viewModel,
|
||||
snackbarHost = viewBinding.scrollView,
|
||||
appShortcutManager = shortcutManager,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.button_read -> openReader(isIncognitoMode = false)
|
||||
R.id.chip_branch -> showBranchPopupMenu(v)
|
||||
R.id.button_chapters -> {
|
||||
ChaptersPagesSheet.show(supportFragmentManager)
|
||||
}
|
||||
|
||||
R.id.chip_author -> {
|
||||
val manga = viewModel.manga.value ?: return
|
||||
startActivity(
|
||||
SearchActivity.newIntent(
|
||||
context = v.context,
|
||||
source = manga.source,
|
||||
query = manga.author ?: return,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
R.id.chip_source -> {
|
||||
val manga = viewModel.manga.value ?: return
|
||||
startActivity(
|
||||
MangaListActivity.newIntent(
|
||||
context = v.context,
|
||||
source = manga.source,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
R.id.chip_size -> {
|
||||
val manga = viewModel.manga.value ?: return
|
||||
LocalInfoDialog.show(supportFragmentManager, manga)
|
||||
}
|
||||
|
||||
R.id.chip_favorite -> {
|
||||
val manga = viewModel.manga.value ?: return
|
||||
FavoriteSheet.show(supportFragmentManager, manga)
|
||||
}
|
||||
|
||||
R.id.chip_time -> {
|
||||
if (viewModel.isStatsAvailable.value) {
|
||||
val manga = viewModel.manga.value ?: return
|
||||
MangaStatsSheet.show(supportFragmentManager, manga)
|
||||
} else {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
||||
R.id.imageView_cover -> {
|
||||
val manga = viewModel.manga.value ?: return
|
||||
startActivity(
|
||||
ImageActivity.newIntent(
|
||||
v.context,
|
||||
manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl },
|
||||
manga.source,
|
||||
),
|
||||
scaleUpActivityOptionsOf(v),
|
||||
)
|
||||
}
|
||||
|
||||
R.id.button_description_more -> {
|
||||
val tv = viewBinding.textViewDescription
|
||||
TransitionManager.beginDelayedTransition(tv.parentView)
|
||||
if (tv.maxLines in 1 until Integer.MAX_VALUE) {
|
||||
tv.maxLines = Integer.MAX_VALUE
|
||||
} else {
|
||||
tv.maxLines = resources.getInteger(R.integer.details_description_lines)
|
||||
}
|
||||
}
|
||||
|
||||
R.id.button_scrobbling_more -> {
|
||||
val manga = viewModel.manga.value ?: return
|
||||
ScrobblingSelectorSheet.show(supportFragmentManager, manga, null)
|
||||
}
|
||||
|
||||
R.id.button_related_more -> {
|
||||
val manga = viewModel.manga.value ?: return
|
||||
startActivity(RelatedMangaActivity.newIntent(v.context, manga))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onChipClick(chip: Chip, data: Any?) {
|
||||
val tag = data as? MangaTag ?: return
|
||||
startActivity(MangaListActivity.newIntent(this, setOf(tag)))
|
||||
}
|
||||
|
||||
override fun onLongClick(v: View): Boolean = when (v.id) {
|
||||
R.id.button_read -> {
|
||||
val menu = PopupMenu(v.context, v)
|
||||
menu.inflate(R.menu.popup_read)
|
||||
menu.menu.findItem(R.id.action_forget)?.isVisible = viewModel.historyInfo.value.run {
|
||||
!isIncognitoMode && history != null
|
||||
}
|
||||
menu.setOnMenuItemClickListener(this)
|
||||
menu.setForceShowIcon(true)
|
||||
menu.show()
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.action_incognito -> {
|
||||
openReader(isIncognitoMode = true)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_forget -> {
|
||||
viewModel.removeFromHistory()
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemClick(item: Bookmark, view: View) {
|
||||
startActivity(
|
||||
IntentBuilder(view.context).bookmark(item).incognito(true).build(),
|
||||
)
|
||||
Toast.makeText(view.context, R.string.incognito_mode, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onDraw() {
|
||||
viewBinding.run {
|
||||
buttonDescriptionMore.isVisible = textViewDescription.maxLines == Int.MAX_VALUE ||
|
||||
textViewDescription.isTextTruncated
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLayoutChange(
|
||||
v: View?,
|
||||
left: Int,
|
||||
top: Int,
|
||||
right: Int,
|
||||
bottom: Int,
|
||||
oldLeft: Int,
|
||||
oldTop: Int,
|
||||
oldRight: Int,
|
||||
oldBottom: Int
|
||||
) {
|
||||
with(viewBinding) {
|
||||
buttonDescriptionMore.isVisible = textViewDescription.isTextTruncated
|
||||
}
|
||||
}
|
||||
|
||||
private fun onChaptersChanged(chapters: List<ChapterListItem>?) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
private fun onFavoritesChanged(categories: Set<FavouriteCategory>) {
|
||||
val chip = viewBinding.infoLayout.chipFavorite
|
||||
chip.setChipIconResource(if (categories.isEmpty()) R.drawable.ic_heart_outline else R.drawable.ic_heart)
|
||||
chip.text = if (categories.isEmpty()) {
|
||||
getString(R.string.add_to_favourites)
|
||||
} else {
|
||||
if (categories.size == 1) {
|
||||
categories.first().title.ellipsize(FAV_LABEL_LIMIT)
|
||||
}
|
||||
buildString(FAV_LABEL_LIMIT + 6) {
|
||||
for ((i, cat) in categories.withIndex()) {
|
||||
if (i == 0) {
|
||||
append(cat.title.ellipsize(FAV_LABEL_LIMIT - 4))
|
||||
} else if (length + cat.title.length > FAV_LABEL_LIMIT) {
|
||||
append(", ")
|
||||
append(getString(R.string.list_ellipsize_pattern, categories.size - i))
|
||||
break
|
||||
} else {
|
||||
append(", ")
|
||||
append(cat.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onReadingTimeChanged(time: ReadingTime?) {
|
||||
val chip = viewBinding.infoLayout.chipTime
|
||||
chip.textAndVisible = time?.formatShort(chip.resources)
|
||||
}
|
||||
|
||||
private fun onDescriptionChanged(description: CharSequence?) {
|
||||
val tv = viewBinding.textViewDescription
|
||||
if (description.isNullOrBlank()) {
|
||||
tv.setText(R.string.no_description)
|
||||
} else {
|
||||
tv.text = description
|
||||
}
|
||||
}
|
||||
|
||||
private fun onLocalSizeChanged(size: Long) {
|
||||
val chip = viewBinding.infoLayout.chipSize
|
||||
if (size == 0L) {
|
||||
chip.isVisible = false
|
||||
} else {
|
||||
chip.text = FileSize.BYTES.format(chip.context, size)
|
||||
chip.isVisible = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRelatedMangaChanged(related: List<MangaItemModel>) {
|
||||
if (related.isEmpty()) {
|
||||
viewBinding.groupRelated.isVisible = false
|
||||
return
|
||||
}
|
||||
val rv = viewBinding.recyclerViewRelated
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val adapter = (rv.adapter as? BaseListAdapter<ListModel>) ?: BaseListAdapter<ListModel>()
|
||||
.addDelegate(
|
||||
ListItemType.MANGA_GRID,
|
||||
mangaGridItemAD(
|
||||
coil, this,
|
||||
StaticItemSizeResolver(resources.getDimensionPixelSize(R.dimen.smaller_grid_width)),
|
||||
) { item, view ->
|
||||
startActivity(DetailsActivity.newIntent(view.context, item))
|
||||
},
|
||||
).also { rv.adapter = it }
|
||||
adapter.items = related
|
||||
viewBinding.groupRelated.isVisible = true
|
||||
}
|
||||
|
||||
private fun onLoadingStateChanged(isLoading: Boolean) {
|
||||
val button = viewBinding.buttonChapters
|
||||
if (isLoading) {
|
||||
button.setImageDrawable(
|
||||
CircularProgressDrawable(this).also {
|
||||
it.setStyle(CircularProgressDrawable.LARGE)
|
||||
it.setColorSchemeColors(getThemeColor(materialR.attr.colorControlNormal))
|
||||
it.start()
|
||||
},
|
||||
)
|
||||
} else {
|
||||
button.setImageResource(R.drawable.ic_list_sheet)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onScrobblingInfoChanged(scrobblings: List<ScrobblingInfo>) {
|
||||
var adapter = viewBinding.recyclerViewScrobbling.adapter as? ScrollingInfoAdapter
|
||||
viewBinding.groupScrobbling.isGone = scrobblings.isEmpty()
|
||||
if (adapter != null) {
|
||||
adapter.items = scrobblings
|
||||
} else {
|
||||
adapter = ScrollingInfoAdapter(this, coil, supportFragmentManager)
|
||||
adapter.items = scrobblings
|
||||
viewBinding.recyclerViewScrobbling.adapter = adapter
|
||||
viewBinding.recyclerViewScrobbling.addItemDecoration(ScrobblingItemDecoration())
|
||||
}
|
||||
}
|
||||
|
||||
private fun onMangaUpdated(details: MangaDetails) {
|
||||
with(viewBinding) {
|
||||
val manga = details.toManga()
|
||||
val hasChapters = !manga.chapters.isNullOrEmpty()
|
||||
// Main
|
||||
loadCover(manga)
|
||||
textViewTitle.text = manga.title
|
||||
textViewSubtitle.textAndVisible = manga.altTitle
|
||||
infoLayout.chipAuthor.textAndVisible = manga.author
|
||||
if (manga.hasRating) {
|
||||
ratingBar.rating = manga.rating * ratingBar.numStars
|
||||
ratingBar.isVisible = true
|
||||
} else {
|
||||
ratingBar.isVisible = false
|
||||
}
|
||||
|
||||
manga.state?.let { state ->
|
||||
textViewState.textAndVisible = resources.getString(state.titleResId)
|
||||
imageViewState.setImageResource(state.iconResId)
|
||||
} ?: run {
|
||||
textViewState.isVisible = false
|
||||
imageViewState.isVisible = false
|
||||
}
|
||||
|
||||
if (manga.source == MangaSource.LOCAL || manga.source == MangaSource.DUMMY) {
|
||||
infoLayout.chipSource.isVisible = false
|
||||
} else {
|
||||
infoLayout.chipSource.text = manga.source.title
|
||||
infoLayout.chipSource.isVisible = true
|
||||
}
|
||||
|
||||
textViewNsfw.isVisible = manga.isNsfw
|
||||
|
||||
// Chips
|
||||
bindTags(manga)
|
||||
|
||||
textViewDescription.text = details.description.ifNullOrEmpty { getString(R.string.no_description) }
|
||||
|
||||
viewBinding.infoLayout.chipSource.also { chip ->
|
||||
ImageRequest.Builder(this@DetailsActivity2)
|
||||
.data(manga.source.faviconUri())
|
||||
.lifecycle(this@DetailsActivity2)
|
||||
.crossfade(false)
|
||||
.size(resources.getDimensionPixelSize(materialR.dimen.m3_chip_icon_size))
|
||||
.target(ChipIconTarget(chip))
|
||||
.placeholder(R.drawable.ic_web)
|
||||
.fallback(R.drawable.ic_web)
|
||||
.error(R.drawable.ic_web)
|
||||
.source(manga.source)
|
||||
.transformations(CircleCropTransformation())
|
||||
.allowRgb565(true)
|
||||
.enqueueWith(coil)
|
||||
}
|
||||
|
||||
buttonChapters.isEnabled = hasChapters
|
||||
title = manga.title
|
||||
buttonRead.isEnabled = hasChapters
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onMangaRemoved(manga: Manga) {
|
||||
Toast.makeText(
|
||||
this,
|
||||
getString(R.string._s_deleted_from_local_storage, manga.title),
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
finishAfterTransition()
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
viewBinding.root.updatePadding(
|
||||
left = insets.left,
|
||||
right = insets.right,
|
||||
bottom = insets.bottom
|
||||
)
|
||||
}
|
||||
|
||||
private fun onHistoryChanged(info: HistoryInfo) {
|
||||
with(viewBinding.buttonRead) {
|
||||
if (info.history != null) {
|
||||
setTitle(R.string._continue)
|
||||
} else {
|
||||
setTitle(R.string.read)
|
||||
}
|
||||
}
|
||||
viewBinding.buttonRead.subtitle = when {
|
||||
!info.isValid -> getString(R.string.loading_)
|
||||
info.currentChapter >= 0 -> getString(
|
||||
R.string.chapter_d_of_d,
|
||||
info.currentChapter + 1,
|
||||
info.totalChapters,
|
||||
)
|
||||
|
||||
info.totalChapters == 0 -> getString(R.string.no_chapters)
|
||||
else -> resources.getQuantityString(
|
||||
R.plurals.chapters,
|
||||
info.totalChapters,
|
||||
info.totalChapters,
|
||||
)
|
||||
}
|
||||
viewBinding.buttonRead.setProgress(info.history?.percent?.coerceIn(0f, 1f) ?: 0f, true)
|
||||
}
|
||||
|
||||
private fun onNewChaptersChanged(count: Int) {
|
||||
chaptersBadge.counter = count
|
||||
}
|
||||
|
||||
private fun showBranchPopupMenu(v: View) {
|
||||
val menu = PopupMenu(v.context, v)
|
||||
val branches = viewModel.branches.value
|
||||
for ((i, branch) in branches.withIndex()) {
|
||||
val title = buildSpannedString {
|
||||
if (branch.isCurrent) {
|
||||
inSpans(
|
||||
ImageSpan(
|
||||
this@DetailsActivity2,
|
||||
R.drawable.ic_current_chapter,
|
||||
DynamicDrawableSpan.ALIGN_BASELINE,
|
||||
),
|
||||
) {
|
||||
append(' ')
|
||||
}
|
||||
append(' ')
|
||||
}
|
||||
append(branch.name ?: getString(R.string.system_default))
|
||||
append(' ')
|
||||
append(' ')
|
||||
inSpans(
|
||||
ForegroundColorSpan(
|
||||
v.context.getThemeColor(
|
||||
android.R.attr.textColorSecondary,
|
||||
Color.LTGRAY,
|
||||
),
|
||||
),
|
||||
RelativeSizeSpan(0.74f),
|
||||
) {
|
||||
append(branch.count.toString())
|
||||
}
|
||||
}
|
||||
val item = menu.menu.add(R.id.group_branches, Menu.NONE, i, title)
|
||||
item.isCheckable = true
|
||||
item.isChecked = branch.isSelected
|
||||
}
|
||||
menu.menu.setGroupCheckable(R.id.group_branches, true, true)
|
||||
menu.setOnMenuItemClickListener {
|
||||
viewModel.setSelectedBranch(branches.getOrNull(it.order)?.name)
|
||||
true
|
||||
}
|
||||
menu.show()
|
||||
}
|
||||
|
||||
private fun openReader(isIncognitoMode: Boolean) {
|
||||
val manga = viewModel.manga.value ?: return
|
||||
val chapterId = viewModel.historyInfo.value.history?.chapterId
|
||||
if (chapterId != null && manga.chapters?.none { x -> x.id == chapterId } == true) {
|
||||
Snackbar.make(viewBinding.scrollView, R.string.chapter_is_missing, Snackbar.LENGTH_SHORT)
|
||||
.show()
|
||||
} else {
|
||||
startActivity(
|
||||
IntentBuilder(this)
|
||||
.manga(manga)
|
||||
.branch(viewModel.selectedBranchValue)
|
||||
.incognito(isIncognitoMode)
|
||||
.build(),
|
||||
)
|
||||
if (isIncognitoMode) {
|
||||
Toast.makeText(this, R.string.incognito_mode, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindTags(manga: Manga) {
|
||||
viewBinding.chipsTags.isVisible = manga.tags.isNotEmpty()
|
||||
viewBinding.chipsTags.setChips(
|
||||
manga.tags.map { tag ->
|
||||
ChipsView.ChipModel(
|
||||
title = tag.title,
|
||||
tint = tagHighlighter.getTagTint(tag),
|
||||
icon = 0,
|
||||
data = tag,
|
||||
isCheckable = false,
|
||||
isChecked = false,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun loadCover(manga: Manga) {
|
||||
val imageUrl = manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl }
|
||||
val lastResult = CoilUtils.result(viewBinding.imageViewCover)
|
||||
if (lastResult is SuccessResult && lastResult.request.data == imageUrl) {
|
||||
return
|
||||
}
|
||||
val request = ImageRequest.Builder(this)
|
||||
.target(viewBinding.imageViewCover)
|
||||
.size(CoverSizeResolver(viewBinding.imageViewCover))
|
||||
.data(imageUrl)
|
||||
.tag(manga.source)
|
||||
.crossfade(this)
|
||||
.lifecycle(this)
|
||||
.placeholderMemoryCacheKey(manga.coverUrl)
|
||||
val previousDrawable = lastResult?.drawable
|
||||
if (previousDrawable != null) {
|
||||
request.fallback(previousDrawable)
|
||||
.placeholder(previousDrawable)
|
||||
.error(previousDrawable)
|
||||
} else {
|
||||
request.fallback(R.drawable.ic_placeholder)
|
||||
.placeholder(R.drawable.ic_placeholder)
|
||||
.error(R.drawable.ic_error_placeholder)
|
||||
}
|
||||
request.enqueueWith(coil)
|
||||
}
|
||||
|
||||
private class PrefetchObserver(
|
||||
private val context: Context,
|
||||
) : FlowCollector<List<ChapterListItem>?> {
|
||||
|
||||
private var isCalled = false
|
||||
|
||||
override suspend fun emit(value: List<ChapterListItem>?) {
|
||||
if (value.isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
if (!isCalled) {
|
||||
isCalled = true
|
||||
val item = value.find { it.isCurrent } ?: value.first()
|
||||
MangaPrefetchService.prefetchPages(context, item.chapter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val FAV_LABEL_LIMIT = 10
|
||||
|
||||
fun newIntent(context: Context, manga: Manga): Intent {
|
||||
return Intent(context, DetailsActivity2::class.java)
|
||||
.putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga))
|
||||
}
|
||||
|
||||
fun newIntent(context: Context, mangaId: Long): Intent {
|
||||
return Intent(context, DetailsActivity2::class.java)
|
||||
.putExtra(MangaIntent.KEY_ID, mangaId)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ class DetailsErrorObserver(
|
||||
private val viewModel: DetailsViewModel,
|
||||
resolver: ExceptionResolver?,
|
||||
) : ErrorObserver(
|
||||
activity.viewBinding.containerDetails, null, resolver,
|
||||
activity.viewBinding.scrollView, null, resolver,
|
||||
{ isResolved ->
|
||||
if (isResolved) {
|
||||
viewModel.reload()
|
||||
|
||||
@@ -1,424 +0,0 @@
|
||||
package org.koitharu.kotatsu.details.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.transition.TransitionManager
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewTreeObserver
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.text.method.LinkMovementMethodCompat
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import coil.request.SuccessResult
|
||||
import coil.util.CoilUtils
|
||||
import com.google.android.material.chip.Chip
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksAdapter
|
||||
import org.koitharu.kotatsu.bookmarks.ui.sheet.BookmarksSheet
|
||||
import org.koitharu.kotatsu.core.model.countChaptersByBranch
|
||||
import org.koitharu.kotatsu.core.model.iconResId
|
||||
import org.koitharu.kotatsu.core.model.titleResId
|
||||
import org.koitharu.kotatsu.core.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.core.util.FileSize
|
||||
import org.koitharu.kotatsu.core.util.ext.crossfade
|
||||
import org.koitharu.kotatsu.core.util.ext.drawableTop
|
||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
|
||||
import org.koitharu.kotatsu.core.util.ext.isTextTruncated
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.parentView
|
||||
import org.koitharu.kotatsu.core.util.ext.resolveDp
|
||||
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
|
||||
import org.koitharu.kotatsu.core.util.ext.showOrHide
|
||||
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
||||
import org.koitharu.kotatsu.databinding.FragmentDetailsBinding
|
||||
import org.koitharu.kotatsu.details.data.ReadingTime
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||
import org.koitharu.kotatsu.details.ui.model.HistoryInfo
|
||||
import org.koitharu.kotatsu.details.ui.related.RelatedMangaActivity
|
||||
import org.koitharu.kotatsu.details.ui.scrobbling.ScrobblingItemDecoration
|
||||
import org.koitharu.kotatsu.details.ui.scrobbling.ScrollingInfoAdapter
|
||||
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
|
||||
import org.koitharu.kotatsu.image.ui.ImageActivity
|
||||
import org.koitharu.kotatsu.list.domain.ListExtraProvider
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||
import org.koitharu.kotatsu.list.ui.adapter.mangaGridItemAD
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.MangaItemModel
|
||||
import org.koitharu.kotatsu.list.ui.size.StaticItemSizeResolver
|
||||
import org.koitharu.kotatsu.local.ui.info.LocalInfoDialog
|
||||
import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
||||
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet
|
||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||
import org.koitharu.kotatsu.search.ui.SearchActivity
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class DetailsFragment :
|
||||
BaseFragment<FragmentDetailsBinding>(),
|
||||
View.OnClickListener,
|
||||
ChipsView.OnChipClickListener,
|
||||
OnListItemClickListener<Bookmark>, ViewTreeObserver.OnDrawListener, View.OnLayoutChangeListener {
|
||||
|
||||
@Inject
|
||||
lateinit var coil: ImageLoader
|
||||
|
||||
@Inject
|
||||
lateinit var tagHighlighter: ListExtraProvider
|
||||
|
||||
private val viewModel by activityViewModels<DetailsViewModel>()
|
||||
|
||||
override fun onCreateViewBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
) = FragmentDetailsBinding.inflate(inflater, container, false)
|
||||
|
||||
override fun onViewBindingCreated(binding: FragmentDetailsBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
binding.textViewAuthor.setOnClickListener(this)
|
||||
binding.imageViewCover.setOnClickListener(this)
|
||||
binding.buttonDescriptionMore.setOnClickListener(this)
|
||||
binding.buttonBookmarksMore.setOnClickListener(this)
|
||||
binding.buttonScrobblingMore.setOnClickListener(this)
|
||||
binding.buttonRelatedMore.setOnClickListener(this)
|
||||
binding.infoLayout.textViewSource.setOnClickListener(this)
|
||||
binding.infoLayout.textViewSize.setOnClickListener(this)
|
||||
binding.textViewDescription.addOnLayoutChangeListener(this)
|
||||
binding.textViewDescription.viewTreeObserver.addOnDrawListener(this)
|
||||
binding.textViewDescription.movementMethod = LinkMovementMethodCompat.getInstance()
|
||||
binding.chipsTags.onChipClickListener = this
|
||||
binding.recyclerViewRelated.addItemDecoration(
|
||||
SpacingItemDecoration(resources.getDimensionPixelOffset(R.dimen.grid_spacing)),
|
||||
)
|
||||
TitleScrollCoordinator(binding.textViewTitle).attach(binding.scrollView)
|
||||
viewModel.manga.filterNotNull().observe(viewLifecycleOwner, ::onMangaUpdated)
|
||||
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
|
||||
viewModel.historyInfo.observe(viewLifecycleOwner, ::onHistoryChanged)
|
||||
viewModel.bookmarks.observe(viewLifecycleOwner, ::onBookmarksChanged)
|
||||
viewModel.scrobblingInfo.observe(viewLifecycleOwner, ::onScrobblingInfoChanged)
|
||||
viewModel.description.observe(viewLifecycleOwner, ::onDescriptionChanged)
|
||||
viewModel.localSize.observe(viewLifecycleOwner, ::onLocalSizeChanged)
|
||||
viewModel.relatedManga.observe(viewLifecycleOwner, ::onRelatedMangaChanged)
|
||||
viewModel.chapters.observe(viewLifecycleOwner, ::onChaptersChanged)
|
||||
viewModel.readingTime.observe(viewLifecycleOwner, ::onReadingTimeChanged)
|
||||
}
|
||||
|
||||
override fun onItemClick(item: Bookmark, view: View) {
|
||||
startActivity(
|
||||
ReaderActivity.IntentBuilder(view.context).bookmark(item).incognito(true).build(),
|
||||
)
|
||||
Toast.makeText(view.context, R.string.incognito_mode, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onItemLongClick(item: Bookmark, view: View): Boolean {
|
||||
val menu = PopupMenu(view.context, view)
|
||||
menu.inflate(R.menu.popup_bookmark)
|
||||
menu.setOnMenuItemClickListener { menuItem ->
|
||||
when (menuItem.itemId) {
|
||||
R.id.action_remove -> viewModel.removeBookmark(item)
|
||||
}
|
||||
true
|
||||
}
|
||||
menu.show()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDraw() {
|
||||
viewBinding?.run {
|
||||
buttonDescriptionMore.isVisible = textViewDescription.maxLines == Int.MAX_VALUE ||
|
||||
textViewDescription.isTextTruncated
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLayoutChange(
|
||||
v: View?,
|
||||
left: Int,
|
||||
top: Int,
|
||||
right: Int,
|
||||
bottom: Int,
|
||||
oldLeft: Int,
|
||||
oldTop: Int,
|
||||
oldRight: Int,
|
||||
oldBottom: Int
|
||||
) {
|
||||
with(viewBinding ?: return) {
|
||||
buttonDescriptionMore.isVisible = textViewDescription.isTextTruncated
|
||||
}
|
||||
}
|
||||
|
||||
private fun onMangaUpdated(manga: Manga) {
|
||||
with(requireViewBinding()) {
|
||||
// Main
|
||||
loadCover(manga)
|
||||
textViewTitle.text = manga.title
|
||||
textViewSubtitle.textAndVisible = manga.altTitle
|
||||
textViewAuthor.textAndVisible = manga.author
|
||||
if (manga.hasRating) {
|
||||
ratingBar.rating = manga.rating * ratingBar.numStars
|
||||
ratingBar.isVisible = true
|
||||
} else {
|
||||
ratingBar.isVisible = false
|
||||
}
|
||||
|
||||
infoLayout.textViewState.apply {
|
||||
manga.state?.let { state ->
|
||||
textAndVisible = resources.getString(state.titleResId)
|
||||
drawableTop = ContextCompat.getDrawable(context, state.iconResId)
|
||||
} ?: run {
|
||||
isVisible = false
|
||||
}
|
||||
}
|
||||
if (manga.source == MangaSource.LOCAL || manga.source == MangaSource.DUMMY) {
|
||||
infoLayout.textViewSource.isVisible = false
|
||||
} else {
|
||||
infoLayout.textViewSource.text = manga.source.title
|
||||
infoLayout.textViewSource.isVisible = true
|
||||
}
|
||||
|
||||
infoLayout.textViewNsfw.isVisible = manga.isNsfw
|
||||
|
||||
// Chips
|
||||
bindTags(manga)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onChaptersChanged(chapters: List<ChapterListItem>?) {
|
||||
val infoLayout = requireViewBinding().infoLayout
|
||||
if (chapters.isNullOrEmpty()) {
|
||||
infoLayout.textViewChapters.isVisible = false
|
||||
} else {
|
||||
val count = chapters.countChaptersByBranch()
|
||||
infoLayout.textViewChapters.isVisible = true
|
||||
val chaptersText = resources.getQuantityString(R.plurals.chapters, count, count)
|
||||
infoLayout.textViewChapters.text = chaptersText
|
||||
}
|
||||
}
|
||||
|
||||
private fun onReadingTimeChanged(time: ReadingTime?) {
|
||||
val binding = viewBinding ?: return
|
||||
if (time == null) {
|
||||
binding.approximateReadTimeLayout.isVisible = false
|
||||
return
|
||||
}
|
||||
binding.approximateReadTime.text = time.format(resources)
|
||||
binding.approximateReadTimeTitle.setText(
|
||||
if (time.isContinue) R.string.approximate_remaining_time else R.string.approximate_reading_time,
|
||||
)
|
||||
binding.approximateReadTimeLayout.isVisible = true
|
||||
}
|
||||
|
||||
private fun onDescriptionChanged(description: CharSequence?) {
|
||||
val tv = requireViewBinding().textViewDescription
|
||||
if (description.isNullOrBlank()) {
|
||||
tv.setText(R.string.no_description)
|
||||
} else {
|
||||
tv.text = description
|
||||
}
|
||||
}
|
||||
|
||||
private fun onLocalSizeChanged(size: Long) {
|
||||
val textView = requireViewBinding().infoLayout.textViewSize
|
||||
if (size == 0L) {
|
||||
textView.isVisible = false
|
||||
} else {
|
||||
textView.text = FileSize.BYTES.format(textView.context, size)
|
||||
textView.isVisible = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRelatedMangaChanged(related: List<MangaItemModel>) {
|
||||
if (related.isEmpty()) {
|
||||
requireViewBinding().groupRelated.isVisible = false
|
||||
return
|
||||
}
|
||||
val rv = viewBinding?.recyclerViewRelated ?: return
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val adapter = (rv.adapter as? BaseListAdapter<ListModel>) ?: BaseListAdapter<ListModel>()
|
||||
.addDelegate(
|
||||
ListItemType.MANGA_GRID,
|
||||
mangaGridItemAD(
|
||||
coil, viewLifecycleOwner,
|
||||
StaticItemSizeResolver(resources.getDimensionPixelSize(R.dimen.smaller_grid_width)),
|
||||
) { item, view ->
|
||||
startActivity(DetailsActivity.newIntent(view.context, item))
|
||||
},
|
||||
).also { rv.adapter = it }
|
||||
adapter.items = related
|
||||
requireViewBinding().groupRelated.isVisible = true
|
||||
}
|
||||
|
||||
private fun onHistoryChanged(history: HistoryInfo) {
|
||||
requireViewBinding().progressView.setPercent(history.history?.percent ?: PROGRESS_NONE, animate = true)
|
||||
}
|
||||
|
||||
private fun onLoadingStateChanged(isLoading: Boolean) {
|
||||
requireViewBinding().progressBar.showOrHide(isLoading)
|
||||
}
|
||||
|
||||
private fun onBookmarksChanged(bookmarks: List<Bookmark>) {
|
||||
var adapter = requireViewBinding().recyclerViewBookmarks.adapter as? BookmarksAdapter
|
||||
requireViewBinding().groupBookmarks.isGone = bookmarks.isEmpty()
|
||||
if (adapter != null) {
|
||||
adapter.items = bookmarks
|
||||
} else {
|
||||
adapter = BookmarksAdapter(coil, viewLifecycleOwner, this)
|
||||
adapter.items = bookmarks
|
||||
requireViewBinding().recyclerViewBookmarks.adapter = adapter
|
||||
val spacing = resources.getDimensionPixelOffset(R.dimen.bookmark_list_spacing)
|
||||
requireViewBinding().recyclerViewBookmarks.addItemDecoration(SpacingItemDecoration(spacing))
|
||||
}
|
||||
}
|
||||
|
||||
private fun onScrobblingInfoChanged(scrobblings: List<ScrobblingInfo>) {
|
||||
var adapter = requireViewBinding().recyclerViewScrobbling.adapter as? ScrollingInfoAdapter
|
||||
requireViewBinding().groupScrobbling.isGone = scrobblings.isEmpty()
|
||||
if (adapter != null) {
|
||||
adapter.items = scrobblings
|
||||
} else {
|
||||
adapter = ScrollingInfoAdapter(viewLifecycleOwner, coil, childFragmentManager)
|
||||
adapter.items = scrobblings
|
||||
requireViewBinding().recyclerViewScrobbling.adapter = adapter
|
||||
requireViewBinding().recyclerViewScrobbling.addItemDecoration(ScrobblingItemDecoration())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
val manga = viewModel.manga.value ?: return
|
||||
when (v.id) {
|
||||
R.id.textView_author -> {
|
||||
startActivity(
|
||||
SearchActivity.newIntent(
|
||||
context = v.context,
|
||||
source = manga.source,
|
||||
query = manga.author ?: return,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
R.id.textView_source -> {
|
||||
startActivity(
|
||||
MangaListActivity.newIntent(
|
||||
context = v.context,
|
||||
source = manga.source,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
R.id.textView_size -> {
|
||||
LocalInfoDialog.show(parentFragmentManager, manga)
|
||||
}
|
||||
|
||||
R.id.imageView_cover -> {
|
||||
startActivity(
|
||||
ImageActivity.newIntent(
|
||||
v.context,
|
||||
manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl },
|
||||
manga.source,
|
||||
),
|
||||
scaleUpActivityOptionsOf(v),
|
||||
)
|
||||
}
|
||||
|
||||
R.id.button_description_more -> {
|
||||
val tv = requireViewBinding().textViewDescription
|
||||
TransitionManager.beginDelayedTransition(tv.parentView)
|
||||
if (tv.maxLines in 1 until Integer.MAX_VALUE) {
|
||||
tv.maxLines = Integer.MAX_VALUE
|
||||
} else {
|
||||
tv.maxLines = resources.getInteger(R.integer.details_description_lines)
|
||||
}
|
||||
}
|
||||
|
||||
R.id.button_scrobbling_more -> {
|
||||
ScrobblingSelectorSheet.show(parentFragmentManager, manga, null)
|
||||
}
|
||||
|
||||
R.id.button_bookmarks_more -> {
|
||||
BookmarksSheet.show(parentFragmentManager, manga)
|
||||
}
|
||||
|
||||
R.id.button_related_more -> {
|
||||
startActivity(RelatedMangaActivity.newIntent(v.context, manga))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onChipClick(chip: Chip, data: Any?) {
|
||||
val tag = data as? MangaTag ?: return
|
||||
startActivity(MangaListActivity.newIntent(requireContext(), setOf(tag)))
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
requireViewBinding().root.updatePadding(
|
||||
bottom = (
|
||||
(activity as? NoModalBottomSheetOwner)?.getBottomSheetCollapsedHeight()
|
||||
?.plus(insets.bottom)?.plus(resources.resolveDp(16))
|
||||
)
|
||||
?: insets.bottom,
|
||||
)
|
||||
}
|
||||
|
||||
private fun bindTags(manga: Manga) {
|
||||
requireViewBinding().chipsTags.setChips(
|
||||
manga.tags.map { tag ->
|
||||
ChipsView.ChipModel(
|
||||
title = tag.title,
|
||||
tint = tagHighlighter.getTagTint(tag),
|
||||
icon = 0,
|
||||
data = tag,
|
||||
isCheckable = false,
|
||||
isChecked = false,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun loadCover(manga: Manga) {
|
||||
val imageUrl = manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl }
|
||||
val lastResult = CoilUtils.result(requireViewBinding().imageViewCover)
|
||||
if (lastResult is SuccessResult && lastResult.request.data == imageUrl) {
|
||||
return
|
||||
}
|
||||
val request = ImageRequest.Builder(context ?: return)
|
||||
.target(requireViewBinding().imageViewCover)
|
||||
.size(CoverSizeResolver(requireViewBinding().imageViewCover))
|
||||
.data(imageUrl)
|
||||
.tag(manga.source)
|
||||
.crossfade(requireContext())
|
||||
.lifecycle(viewLifecycleOwner)
|
||||
.placeholderMemoryCacheKey(manga.coverUrl)
|
||||
val previousDrawable = lastResult?.drawable
|
||||
if (previousDrawable != null) {
|
||||
request.fallback(previousDrawable)
|
||||
.placeholder(previousDrawable)
|
||||
.error(previousDrawable)
|
||||
} else {
|
||||
request.fallback(R.drawable.ic_placeholder)
|
||||
.placeholder(R.drawable.ic_placeholder)
|
||||
.error(R.drawable.ic_error_placeholder)
|
||||
}
|
||||
request.enqueueWith(coil)
|
||||
}
|
||||
}
|
||||
@@ -19,8 +19,8 @@ import org.koitharu.kotatsu.browser.BrowserActivity
|
||||
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.util.ShareHelper
|
||||
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesSheet
|
||||
import org.koitharu.kotatsu.download.ui.dialog.DownloadOption
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteSheet
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet
|
||||
import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity
|
||||
@@ -35,7 +35,6 @@ class DetailsMenuProvider(
|
||||
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.opt_details, menu)
|
||||
menu.findItem(R.id.action_favourite).isVisible = activity is DetailsActivity
|
||||
}
|
||||
|
||||
override fun onPrepareMenu(menu: Menu) {
|
||||
@@ -48,9 +47,6 @@ class DetailsMenuProvider(
|
||||
menu.findItem(R.id.action_scrobbling).isVisible = viewModel.isScrobblingAvailable
|
||||
menu.findItem(R.id.action_online).isVisible = viewModel.remoteManga.value != null
|
||||
menu.findItem(R.id.action_stats).isVisible = viewModel.isStatsAvailable.value
|
||||
menu.findItem(R.id.action_favourite).setIcon(
|
||||
if (viewModel.favouriteCategories.value.isNotEmpty()) R.drawable.ic_heart else R.drawable.ic_heart_outline,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
@@ -66,12 +62,6 @@ class DetailsMenuProvider(
|
||||
}
|
||||
}
|
||||
|
||||
R.id.action_favourite -> {
|
||||
viewModel.manga.value?.let {
|
||||
FavoriteSheet.show(activity.supportFragmentManager, it)
|
||||
}
|
||||
}
|
||||
|
||||
R.id.action_delete -> {
|
||||
val title = viewModel.manga.value?.title.orEmpty()
|
||||
MaterialAlertDialogBuilder(activity)
|
||||
@@ -145,6 +135,7 @@ class DetailsMenuProvider(
|
||||
is DownloadOption.WholeManga -> null
|
||||
is DownloadOption.SelectionHint -> {
|
||||
viewModel.startChaptersSelection()
|
||||
ChaptersPagesSheet.show(activity.supportFragmentManager, ChaptersPagesSheet.TAB_CHAPTERS)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,6 @@ import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterNot
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
@@ -86,8 +84,8 @@ class DetailsViewModel @Inject constructor(
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val intent = MangaIntent(savedStateHandle)
|
||||
private val mangaId = intent.mangaId
|
||||
private var loadingJob: Job
|
||||
val mangaId = intent.mangaId
|
||||
|
||||
val onActionDone = MutableEventFlow<ReversibleAction>()
|
||||
val onShowTip = MutableEventFlow<Unit>()
|
||||
@@ -133,7 +131,7 @@ class DetailsViewModel @Inject constructor(
|
||||
)
|
||||
|
||||
val historyInfo: StateFlow<HistoryInfo> = combine(
|
||||
manga,
|
||||
details,
|
||||
selectedBranch,
|
||||
history,
|
||||
interactor.observeIncognitoMode(manga),
|
||||
@@ -247,12 +245,6 @@ class DetailsViewModel @Inject constructor(
|
||||
localStorageChanges
|
||||
.collect { onDownloadComplete(it) }
|
||||
}
|
||||
launchJob(Dispatchers.Default) {
|
||||
if (settings.isTipEnabled(DetailsActivity.TIP_BUTTON)) {
|
||||
manga.filterNot { it?.chapters.isNullOrEmpty() }.first()
|
||||
onShowTip.call(Unit)
|
||||
}
|
||||
}
|
||||
launchJob(Dispatchers.Default) {
|
||||
val manga = details.firstOrNull { !it?.chapters.isNullOrEmpty() } ?: return@launchJob
|
||||
val h = history.firstOrNull()
|
||||
@@ -363,10 +355,6 @@ class DetailsViewModel @Inject constructor(
|
||||
onSelectChapter.call(chapter.chapter.id)
|
||||
}
|
||||
|
||||
fun onButtonTipClosed() {
|
||||
settings.closeTip(DetailsActivity.TIP_BUTTON)
|
||||
}
|
||||
|
||||
fun removeFromHistory() {
|
||||
launchJob(Dispatchers.Default) {
|
||||
val handle = historyRepository.delete(setOf(mangaId))
|
||||
|
||||
@@ -28,7 +28,7 @@ class DownloadDialogHelper(
|
||||
}
|
||||
|
||||
if (history != null) {
|
||||
val unreadChapters = branchChapters.takeLastWhile { it.id != history.chapterId }
|
||||
val unreadChapters = branchChapters.dropWhile { it.id != history.chapterId }
|
||||
if (unreadChapters.isNotEmpty() && unreadChapters.size < branchChapters.size) {
|
||||
add(DownloadOption.AllUnreadChapters(unreadChapters.ids(), branch))
|
||||
if (unreadChapters.size > 5) {
|
||||
|
||||
@@ -13,13 +13,13 @@ import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
||||
import org.koitharu.kotatsu.databinding.ItemChapterBinding
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import com.google.android.material.R as MR
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
fun chapterListItemAD(
|
||||
clickListener: OnListItemClickListener<ChapterListItem>,
|
||||
) = adapterDelegateViewBinding<ChapterListItem, ListModel, ItemChapterBinding>(
|
||||
viewBinding = { inflater, parent -> ItemChapterBinding.inflate(inflater, parent, false) },
|
||||
on = { item, _, _ -> item is ChapterListItem && !item.isGrid }
|
||||
on = { item, _, _ -> item is ChapterListItem && !item.isGrid },
|
||||
) {
|
||||
|
||||
val eventListener = AdapterDelegateClickListenerAdapter(this, clickListener)
|
||||
@@ -27,10 +27,17 @@ fun chapterListItemAD(
|
||||
itemView.setOnLongClickListener(eventListener)
|
||||
|
||||
bind { payloads ->
|
||||
if (payloads.isEmpty()) {
|
||||
binding.textViewTitle.text = item.chapter.name
|
||||
binding.textViewDescription.textAndVisible = item.description
|
||||
}
|
||||
binding.textViewTitle.text = item.chapter.name
|
||||
binding.textViewDescription.textAndVisible = item.description
|
||||
itemView.setBackgroundResource(
|
||||
when {
|
||||
item.isGroupStart && item.isGroupEnd -> R.drawable.bg_card_full
|
||||
item.isGroupStart -> R.drawable.bg_card_top
|
||||
item.isGroupMiddle -> R.drawable.bg_card_none
|
||||
item.isGroupEnd -> R.drawable.bg_card_bottom
|
||||
else -> R.drawable.list_selector
|
||||
},
|
||||
)
|
||||
when {
|
||||
item.isCurrent -> {
|
||||
binding.textViewTitle.drawableStart = ContextCompat.getDrawable(context, R.drawable.ic_current_chapter)
|
||||
@@ -47,7 +54,7 @@ fun chapterListItemAD(
|
||||
null
|
||||
}
|
||||
binding.textViewTitle.setTextColor(context.getThemeColorStateList(android.R.attr.textColorPrimary))
|
||||
binding.textViewDescription.setTextColor(context.getThemeColorStateList(MR.attr.colorOutline))
|
||||
binding.textViewDescription.setTextColor(context.getThemeColorStateList(materialR.attr.colorOutline))
|
||||
binding.textViewTitle.typeface = Typeface.DEFAULT
|
||||
binding.textViewDescription.typeface = Typeface.DEFAULT
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
import android.view.View
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.koitharu.kotatsu.R
|
||||
@@ -20,10 +19,7 @@ import com.google.android.material.R as materialR
|
||||
class ChaptersSelectionDecoration(context: Context) : AbstractSelectionItemDecoration() {
|
||||
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val radius = context.resources.getDimension(materialR.dimen.abc_control_corner_material)
|
||||
private val checkIcon = ContextCompat.getDrawable(context, materialR.drawable.ic_mtrl_checked_circle)
|
||||
private val iconOffset = context.resources.getDimensionPixelOffset(R.dimen.chapter_check_offset)
|
||||
private val iconSize = context.resources.getDimensionPixelOffset(R.dimen.chapter_check_size)
|
||||
private val defaultRadius = context.resources.getDimension(materialR.dimen.abc_control_corner_material)
|
||||
private val strokeColor = context.getThemeColor(materialR.attr.colorPrimary, Color.RED)
|
||||
private val fillColor = ColorUtils.setAlphaComponent(
|
||||
ColorUtils.blendARGB(strokeColor, context.getThemeColor(materialR.attr.colorSurface), 0.8f),
|
||||
@@ -36,12 +32,11 @@ class ChaptersSelectionDecoration(context: Context) : AbstractSelectionItemDecor
|
||||
98,
|
||||
)
|
||||
paint.style = Paint.Style.FILL
|
||||
hasBackground = true
|
||||
hasBackground = false
|
||||
hasForeground = true
|
||||
isIncludeDecorAndMargins = false
|
||||
|
||||
paint.strokeWidth = context.resources.getDimension(R.dimen.selection_stroke_width)
|
||||
checkIcon?.setTint(strokeColor)
|
||||
}
|
||||
|
||||
override fun getItemId(parent: RecyclerView, child: View): Long {
|
||||
@@ -50,19 +45,6 @@ class ChaptersSelectionDecoration(context: Context) : AbstractSelectionItemDecor
|
||||
return item.chapter.id
|
||||
}
|
||||
|
||||
override fun onDrawBackground(
|
||||
canvas: Canvas,
|
||||
parent: RecyclerView,
|
||||
child: View,
|
||||
bounds: RectF,
|
||||
state: RecyclerView.State,
|
||||
) {
|
||||
if (child is CardView) {
|
||||
return
|
||||
}
|
||||
canvas.drawRoundRect(bounds, radius, radius, paint)
|
||||
}
|
||||
|
||||
override fun onDrawForeground(
|
||||
canvas: Canvas,
|
||||
parent: RecyclerView,
|
||||
@@ -70,24 +52,16 @@ class ChaptersSelectionDecoration(context: Context) : AbstractSelectionItemDecor
|
||||
bounds: RectF,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
if (child !is CardView) {
|
||||
return
|
||||
val radius = if (child is CardView) {
|
||||
child.radius
|
||||
} else {
|
||||
defaultRadius
|
||||
}
|
||||
val radius = child.radius
|
||||
paint.color = fillColor
|
||||
paint.style = Paint.Style.FILL
|
||||
canvas.drawRoundRect(bounds, radius, radius, paint)
|
||||
paint.color = strokeColor
|
||||
paint.style = Paint.Style.STROKE
|
||||
canvas.drawRoundRect(bounds, radius, radius, paint)
|
||||
checkIcon?.run {
|
||||
setBounds(
|
||||
(bounds.right - iconSize - iconOffset).toInt(),
|
||||
(bounds.top + iconOffset).toInt(),
|
||||
(bounds.right - iconOffset).toInt(),
|
||||
(bounds.top + iconOffset + iconSize).toInt(),
|
||||
)
|
||||
draw(canvas)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,13 @@ import org.jsoup.internal.StringUtil.StringJoiner
|
||||
import org.koitharu.kotatsu.core.model.formatNumber
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import kotlin.experimental.and
|
||||
|
||||
data class ChapterListItem(
|
||||
val chapter: MangaChapter,
|
||||
val flags: Int,
|
||||
val flags: Byte,
|
||||
private val uploadDateMs: Long,
|
||||
private val groupPosition: Byte,
|
||||
) : ListModel {
|
||||
|
||||
var description: String? = null
|
||||
@@ -51,6 +53,15 @@ data class ChapterListItem(
|
||||
val isGrid: Boolean
|
||||
get() = hasFlag(FLAG_GRID)
|
||||
|
||||
val isGroupStart: Boolean
|
||||
get() = (groupPosition and GROUP_START) == GROUP_START
|
||||
|
||||
val isGroupMiddle: Boolean
|
||||
get() = (groupPosition and GROUP_MIDDLE) == GROUP_MIDDLE
|
||||
|
||||
val isGroupEnd: Boolean
|
||||
get() = (groupPosition and GROUP_END) == GROUP_END
|
||||
|
||||
private fun buildDescription(): String {
|
||||
val joiner = StringJoiner(" • ")
|
||||
chapter.formatNumber()?.let {
|
||||
@@ -67,7 +78,7 @@ data class ChapterListItem(
|
||||
return joiner.complete()
|
||||
}
|
||||
|
||||
private fun hasFlag(flag: Int): Boolean {
|
||||
private fun hasFlag(flag: Byte): Boolean {
|
||||
return (flags and flag) == flag
|
||||
}
|
||||
|
||||
@@ -88,11 +99,15 @@ data class ChapterListItem(
|
||||
|
||||
companion object {
|
||||
|
||||
const val FLAG_UNREAD = 2
|
||||
const val FLAG_CURRENT = 4
|
||||
const val FLAG_NEW = 8
|
||||
const val FLAG_BOOKMARKED = 16
|
||||
const val FLAG_DOWNLOADED = 32
|
||||
const val FLAG_GRID = 64
|
||||
const val FLAG_UNREAD: Byte = 2
|
||||
const val FLAG_CURRENT: Byte = 4
|
||||
const val FLAG_NEW: Byte = 8
|
||||
const val FLAG_BOOKMARKED: Byte = 16
|
||||
const val FLAG_DOWNLOADED: Byte = 32
|
||||
const val FLAG_GRID: Byte = 64
|
||||
|
||||
const val GROUP_START: Byte = 2
|
||||
const val GROUP_MIDDLE: Byte = 4
|
||||
const val GROUP_END: Byte = 8
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package org.koitharu.kotatsu.details.ui.model
|
||||
|
||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.model.isLocal
|
||||
import org.koitharu.kotatsu.details.data.MangaDetails
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
|
||||
data class HistoryInfo(
|
||||
@@ -8,26 +11,34 @@ data class HistoryInfo(
|
||||
val currentChapter: Int,
|
||||
val history: MangaHistory?,
|
||||
val isIncognitoMode: Boolean,
|
||||
val isChapterMissing: Boolean,
|
||||
val canDownload: Boolean,
|
||||
) {
|
||||
val isValid: Boolean
|
||||
get() = totalChapters >= 0
|
||||
|
||||
val canContinue: Boolean
|
||||
get() = history != null && !isChapterMissing
|
||||
}
|
||||
|
||||
fun HistoryInfo(
|
||||
manga: Manga?,
|
||||
manga: MangaDetails?,
|
||||
branch: String?,
|
||||
history: MangaHistory?,
|
||||
isIncognitoMode: Boolean
|
||||
): HistoryInfo {
|
||||
val chapters = manga?.getChapters(branch)
|
||||
val chapters = manga?.chapters?.get(branch)
|
||||
val currentChapter = if (history != null && !chapters.isNullOrEmpty()) {
|
||||
chapters.indexOfFirst { it.id == history.chapterId }
|
||||
} else {
|
||||
-2
|
||||
}
|
||||
return HistoryInfo(
|
||||
totalChapters = chapters?.size ?: -1,
|
||||
currentChapter = if (history != null && !chapters.isNullOrEmpty()) {
|
||||
chapters.indexOfFirst { it.id == history.chapterId }
|
||||
} else {
|
||||
-1
|
||||
},
|
||||
currentChapter = currentChapter,
|
||||
history = history,
|
||||
isIncognitoMode = isIncognitoMode,
|
||||
isChapterMissing = currentChapter == -1,
|
||||
canDownload = manga?.isLocal == false,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_GRID
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_NEW
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_UNREAD
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import kotlin.experimental.or
|
||||
|
||||
fun MangaChapter.toListItem(
|
||||
isCurrent: Boolean,
|
||||
@@ -16,7 +17,7 @@ fun MangaChapter.toListItem(
|
||||
isBookmarked: Boolean,
|
||||
isGrid: Boolean,
|
||||
): ChapterListItem {
|
||||
var flags = 0
|
||||
var flags: Byte = 0
|
||||
if (isCurrent) flags = flags or FLAG_CURRENT
|
||||
if (isUnread) flags = flags or FLAG_UNREAD
|
||||
if (isNew) flags = flags or FLAG_NEW
|
||||
@@ -27,5 +28,6 @@ fun MangaChapter.toListItem(
|
||||
chapter = this,
|
||||
flags = flags,
|
||||
uploadDateMs = uploadDate,
|
||||
groupPosition = 0,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.details.ui
|
||||
package org.koitharu.kotatsu.details.ui.pager
|
||||
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
@@ -14,6 +14,8 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
|
||||
import org.koitharu.kotatsu.core.util.ext.setValueRounded
|
||||
import org.koitharu.kotatsu.core.util.progress.IntPercentLabelFormatter
|
||||
import org.koitharu.kotatsu.details.ui.DetailsViewModel
|
||||
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesSheet.Companion.TAB_BOOKMARKS
|
||||
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesSheet.Companion.TAB_CHAPTERS
|
||||
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesSheet.Companion.TAB_PAGES
|
||||
import java.lang.ref.WeakReference
|
||||
@@ -42,7 +44,7 @@ class ChapterPagesMenuProvider(
|
||||
menu.findItem(R.id.action_grid_view)?.isChecked = viewModel.isChaptersInGridView.value == true
|
||||
}
|
||||
|
||||
TAB_PAGES -> {
|
||||
TAB_PAGES, TAB_BOOKMARKS -> {
|
||||
menuInflater.inflate(R.menu.opt_pages, menu)
|
||||
menu.findItem(R.id.action_grid_size)?.run {
|
||||
setOnActionExpandListener(this@ChapterPagesMenuProvider)
|
||||
@@ -5,12 +5,11 @@ import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.details.ui.pager.bookmarks.MangaBookmarksFragment
|
||||
import org.koitharu.kotatsu.details.ui.pager.chapters.ChaptersFragment
|
||||
import org.koitharu.kotatsu.details.ui.pager.pages.PagesFragment
|
||||
|
||||
class DetailsPagerAdapter2(
|
||||
class ChaptersPagesAdapter(
|
||||
fragment: Fragment,
|
||||
val isPagesTabEnabled: Boolean,
|
||||
) : FragmentStateAdapter(fragment),
|
||||
@@ -2,29 +2,40 @@ package org.koitharu.kotatsu.details.ui.pager
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetBehavior
|
||||
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetBehavior.Companion.STATE_COLLAPSED
|
||||
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetBehavior.Companion.STATE_DRAGGING
|
||||
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetBehavior.Companion.STATE_EXPANDED
|
||||
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetBehavior.Companion.STATE_SETTLING
|
||||
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetCallback
|
||||
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
|
||||
import org.koitharu.kotatsu.core.ui.util.ActionModeListener
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
||||
import org.koitharu.kotatsu.core.util.ext.doOnPageChanged
|
||||
import org.koitharu.kotatsu.core.util.ext.menuView
|
||||
import org.koitharu.kotatsu.core.util.ext.recyclerView
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.core.util.ext.setTabsEnabled
|
||||
import org.koitharu.kotatsu.core.util.ext.showDistinct
|
||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||
import org.koitharu.kotatsu.databinding.SheetChaptersPagesBinding
|
||||
import org.koitharu.kotatsu.details.ui.ChapterPagesMenuProvider
|
||||
import org.koitharu.kotatsu.details.ui.DetailsViewModel
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ChaptersPagesSheet : BaseAdaptiveSheet<SheetChaptersPagesBinding>(), ActionModeListener {
|
||||
class ChaptersPagesSheet : BaseAdaptiveSheet<SheetChaptersPagesBinding>(), ActionModeListener, AdaptiveSheetCallback {
|
||||
|
||||
@Inject
|
||||
lateinit var settings: AppSettings
|
||||
@@ -40,30 +51,45 @@ class ChaptersPagesSheet : BaseAdaptiveSheet<SheetChaptersPagesBinding>(), Actio
|
||||
disableFitToContents()
|
||||
|
||||
val args = arguments ?: Bundle.EMPTY
|
||||
val adapter = DetailsPagerAdapter2(this, args.getBoolean(ARG_SHOW_PAGES, settings.isPagesTabEnabled))
|
||||
binding.pager.recyclerView?.isNestedScrollingEnabled = false
|
||||
val defaultTab = args.getInt(ARG_TAB, settings.defaultDetailsTab)
|
||||
val adapter = ChaptersPagesAdapter(this, settings.isPagesTabEnabled || defaultTab == TAB_PAGES)
|
||||
binding.pager.offscreenPageLimit = adapter.itemCount
|
||||
binding.pager.adapter = adapter
|
||||
binding.pager.doOnPageChanged(::onPageChanged)
|
||||
TabLayoutMediator(binding.tabs, binding.pager, adapter).attach()
|
||||
binding.pager.setCurrentItem(args.getInt(ARG_TAB, settings.defaultDetailsTab), false)
|
||||
binding.pager.setCurrentItem(defaultTab, false)
|
||||
binding.tabs.isVisible = adapter.itemCount > 1
|
||||
|
||||
val menuProvider = ChapterPagesMenuProvider(viewModel, this, binding.pager, settings)
|
||||
onBackPressedDispatcher.addCallback(viewLifecycleOwner, menuProvider)
|
||||
binding.toolbar.addMenuProvider(menuProvider)
|
||||
|
||||
actionModeDelegate.addListener(this, viewLifecycleOwner)
|
||||
actionModeDelegate?.addListener(this, viewLifecycleOwner)
|
||||
addSheetCallback(this, viewLifecycleOwner)
|
||||
|
||||
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.pager, this))
|
||||
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(binding.pager, null))
|
||||
viewModel.onDownloadStarted.observeEvent(viewLifecycleOwner, DownloadStartedObserver(binding.pager))
|
||||
viewModel.newChaptersCount.observe(viewLifecycleOwner, ::onNewChaptersChanged)
|
||||
}
|
||||
|
||||
override fun onStateChanged(sheet: View, newState: Int) {
|
||||
if (newState == STATE_DRAGGING || newState == STATE_SETTLING) {
|
||||
return
|
||||
}
|
||||
val isActionModeStarted = actionModeDelegate?.isActionModeStarted == true
|
||||
viewBinding?.toolbar?.menuView?.isVisible = newState != STATE_COLLAPSED && !isActionModeStarted
|
||||
}
|
||||
|
||||
override fun onActionModeStarted(mode: ActionMode) {
|
||||
expandAndLock()
|
||||
viewBinding?.toolbar?.menuView?.isEnabled = false
|
||||
viewBinding?.toolbar?.menuView?.isVisible = false
|
||||
}
|
||||
|
||||
override fun onActionModeFinished(mode: ActionMode) {
|
||||
unlock()
|
||||
viewBinding?.toolbar?.menuView?.isEnabled = true
|
||||
val state = behavior?.state ?: STATE_EXPANDED
|
||||
viewBinding?.toolbar?.menuView?.isVisible = state != STATE_COLLAPSED
|
||||
}
|
||||
|
||||
override fun expandAndLock() {
|
||||
@@ -85,6 +111,17 @@ class ChaptersPagesSheet : BaseAdaptiveSheet<SheetChaptersPagesBinding>(), Actio
|
||||
|
||||
private fun onPageChanged(position: Int) {
|
||||
viewBinding?.toolbar?.invalidateMenu()
|
||||
settings.lastDetailsTab = position
|
||||
}
|
||||
|
||||
private fun onNewChaptersChanged(counter: Int) {
|
||||
val tab = viewBinding?.tabs?.getTabAt(0) ?: return
|
||||
if (counter == 0) {
|
||||
tab.removeBadge()
|
||||
} else {
|
||||
val badge = tab.orCreateBadge
|
||||
badge.number = counter
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -93,6 +130,8 @@ class ChaptersPagesSheet : BaseAdaptiveSheet<SheetChaptersPagesBinding>(), Actio
|
||||
const val TAB_PAGES = 1
|
||||
const val TAB_BOOKMARKS = 2
|
||||
private const val ARG_TAB = "tag"
|
||||
|
||||
@Deprecated("")
|
||||
private const val ARG_SHOW_PAGES = "pages"
|
||||
private const val TAG = "ChaptersPagesSheet"
|
||||
|
||||
@@ -100,17 +139,15 @@ class ChaptersPagesSheet : BaseAdaptiveSheet<SheetChaptersPagesBinding>(), Actio
|
||||
ChaptersPagesSheet().showDistinct(fm, TAG)
|
||||
}
|
||||
|
||||
fun show(fm: FragmentManager, showPagesTab: Boolean) {
|
||||
fun show(fm: FragmentManager, defaultTab: Int) {
|
||||
ChaptersPagesSheet().withArgs(1) {
|
||||
putBoolean(ARG_SHOW_PAGES, showPagesTab)
|
||||
}.showDistinct(fm, TAG)
|
||||
}
|
||||
|
||||
fun show(fm: FragmentManager, showPagesTab: Boolean, defaultTab: Int) {
|
||||
ChaptersPagesSheet().withArgs(2) {
|
||||
putBoolean(ARG_SHOW_PAGES, showPagesTab)
|
||||
putInt(ARG_TAB, defaultTab)
|
||||
}.showDistinct(fm, TAG)
|
||||
}
|
||||
|
||||
fun isShown(fm: FragmentManager): Boolean {
|
||||
val sheet = fm.findFragmentByTag(TAG) as? ChaptersPagesSheet
|
||||
return sheet != null && sheet.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
package org.koitharu.kotatsu.details.ui.pager
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.details.ui.pager.chapters.ChaptersFragment
|
||||
import org.koitharu.kotatsu.details.ui.pager.pages.PagesFragment
|
||||
|
||||
class DetailsPagerAdapter(
|
||||
activity: FragmentActivity,
|
||||
settings: AppSettings,
|
||||
) : FragmentStateAdapter(activity),
|
||||
TabLayoutMediator.TabConfigurationStrategy {
|
||||
|
||||
val isPagesTabEnabled = settings.isPagesTabEnabled
|
||||
|
||||
override fun getItemCount(): Int = if (isPagesTabEnabled) 2 else 1
|
||||
|
||||
override fun createFragment(position: Int): Fragment = when (position) {
|
||||
0 -> ChaptersFragment()
|
||||
1 -> PagesFragment()
|
||||
else -> throw IllegalArgumentException("Invalid position $position")
|
||||
}
|
||||
|
||||
override fun onConfigureTab(tab: TabLayout.Tab, position: Int) {
|
||||
tab.setText(
|
||||
when (position) {
|
||||
0 -> R.string.chapters
|
||||
1 -> R.string.pages
|
||||
else -> 0
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -20,14 +20,11 @@ import org.koitharu.kotatsu.core.util.ext.findParentCallback
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.databinding.FragmentMangaBookmarksBinding
|
||||
import org.koitharu.kotatsu.details.ui.DetailsViewModel
|
||||
import org.koitharu.kotatsu.list.ui.MangaListSpanResolver
|
||||
import org.koitharu.kotatsu.list.ui.GridSpanResolver
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderNavigationCallback
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||
import org.koitharu.kotatsu.reader.ui.thumbnails.OnPageSelectListener
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
@@ -44,7 +41,7 @@ class MangaBookmarksFragment : BaseFragment<FragmentMangaBookmarksBinding>(),
|
||||
lateinit var settings: AppSettings
|
||||
|
||||
private var bookmarksAdapter: BookmarksAdapter? = null
|
||||
private var spanResolver: MangaListSpanResolver? = null
|
||||
private var spanResolver: GridSpanResolver? = null
|
||||
|
||||
private val spanSizeLookup = SpanSizeLookup()
|
||||
private val listCommitCallback = Runnable {
|
||||
@@ -62,19 +59,22 @@ class MangaBookmarksFragment : BaseFragment<FragmentMangaBookmarksBinding>(),
|
||||
|
||||
override fun onViewBindingCreated(binding: FragmentMangaBookmarksBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
spanResolver = MangaListSpanResolver(binding.root.resources)
|
||||
spanResolver = GridSpanResolver(binding.root.resources)
|
||||
bookmarksAdapter = BookmarksAdapter(
|
||||
coil = coil,
|
||||
lifecycleOwner = viewLifecycleOwner,
|
||||
clickListener = this@MangaBookmarksFragment,
|
||||
headerClickListener = null,
|
||||
)
|
||||
viewModel.gridScale.observe(viewLifecycleOwner, ::onGridScaleChanged) // before rv initialization
|
||||
with(binding.recyclerView) {
|
||||
addItemDecoration(TypedListSpacingDecoration(context, false))
|
||||
adapter = bookmarksAdapter
|
||||
addOnLayoutChangeListener(spanResolver)
|
||||
spanResolver?.setGridSize(settings.gridSize / 100f, this)
|
||||
(layoutManager as GridLayoutManager).spanSizeLookup = spanSizeLookup
|
||||
(layoutManager as GridLayoutManager).let {
|
||||
it.spanSizeLookup = spanSizeLookup
|
||||
it.spanCount = checkNotNull(spanResolver).spanCount
|
||||
}
|
||||
}
|
||||
viewModel.content.observe(viewLifecycleOwner, checkNotNull(bookmarksAdapter))
|
||||
}
|
||||
@@ -113,6 +113,11 @@ class MangaBookmarksFragment : BaseFragment<FragmentMangaBookmarksBinding>(),
|
||||
}
|
||||
}
|
||||
|
||||
private fun onGridScaleChanged(scale: Float) {
|
||||
spanSizeLookup.invalidateCache()
|
||||
spanResolver?.setGridSize(scale, requireViewBinding().recyclerView)
|
||||
}
|
||||
|
||||
private inner class SpanSizeLookup : GridLayoutManager.SpanSizeLookup() {
|
||||
|
||||
init {
|
||||
|
||||
@@ -15,6 +15,8 @@ import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||
@@ -26,10 +28,17 @@ import javax.inject.Inject
|
||||
@HiltViewModel
|
||||
class MangaBookmarksViewModel @Inject constructor(
|
||||
bookmarksRepository: BookmarksRepository,
|
||||
settings: AppSettings,
|
||||
) : BaseViewModel(), FlowCollector<Manga?> {
|
||||
|
||||
private val manga = MutableStateFlow<Manga?>(null)
|
||||
|
||||
val gridScale = settings.observeAsStateFlow(
|
||||
scope = viewModelScope + Dispatchers.Default,
|
||||
key = AppSettings.KEY_GRID_SIZE_PAGES,
|
||||
valueProducer = { gridSizePages / 100f },
|
||||
)
|
||||
|
||||
val content: StateFlow<List<ListModel>> = manga.filterNotNull().flatMapLatest { m ->
|
||||
bookmarksRepository.observeBookmarks(m)
|
||||
.map { mapList(m, it) }
|
||||
|
||||
@@ -10,11 +10,11 @@ import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.koitharu.kotatsu.R
|
||||
@@ -28,8 +28,6 @@ import org.koitharu.kotatsu.core.util.ext.findParentCallback
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.databinding.FragmentChaptersBinding
|
||||
import org.koitharu.kotatsu.details.ui.ChaptersMenuProvider
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.details.ui.DetailsViewModel
|
||||
import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter
|
||||
import org.koitharu.kotatsu.details.ui.adapter.ChaptersSelectionDecoration
|
||||
@@ -93,14 +91,11 @@ class ChaptersFragment :
|
||||
viewModel.isChaptersEmpty.observe(viewLifecycleOwner) {
|
||||
binding.textViewHolder.isVisible = it
|
||||
}
|
||||
viewModel.onSelectChapter.observeEvent(viewLifecycleOwner) {
|
||||
selectionController?.onItemLongClick(it)
|
||||
}
|
||||
val detailsActivity = activity as? DetailsActivity
|
||||
if (detailsActivity != null) {
|
||||
val menuProvider = ChaptersMenuProvider(viewModel, detailsActivity.bottomSheetMediator)
|
||||
activity?.onBackPressedDispatcher?.addCallback(menuProvider)
|
||||
detailsActivity.secondaryMenuHost.addMenuProvider(menuProvider, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
||||
viewModel.onSelectChapter.observeEvent(viewLifecycleOwner) { chapterId ->
|
||||
chaptersAdapter?.observeItems()?.firstOrNull { items ->
|
||||
items.any { x -> x is ChapterListItem && x.chapter.id == chapterId }
|
||||
}
|
||||
selectionController?.onItemLongClick(chapterId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.reader.ui.thumbnails
|
||||
package org.koitharu.kotatsu.details.ui.pager.pages
|
||||
|
||||
import android.content.Context
|
||||
import android.webkit.MimeTypeMap
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.reader.ui.thumbnails
|
||||
package org.koitharu.kotatsu.details.ui.pager.pages
|
||||
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.reader.ui.thumbnails.adapter
|
||||
package org.koitharu.kotatsu.details.ui.pager.pages
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import coil.ImageLoader
|
||||
@@ -9,13 +9,13 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.util.ext.decodeRegion
|
||||
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
|
||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||
import org.koitharu.kotatsu.core.util.ext.setTextColorAttr
|
||||
import org.koitharu.kotatsu.core.util.ext.source
|
||||
import org.koitharu.kotatsu.databinding.ItemPageThumbBinding
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
fun pageThumbnailAD(
|
||||
@@ -39,9 +39,7 @@ fun pageThumbnailAD(
|
||||
bind {
|
||||
val data: Any = item.page.preview?.takeUnless { it.isEmpty() } ?: item.page.toMangaPage()
|
||||
binding.imageViewThumb.newImageRequest(lifecycleOwner, data)?.run {
|
||||
placeholder(R.drawable.ic_placeholder)
|
||||
fallback(R.drawable.ic_placeholder)
|
||||
error(R.drawable.ic_error_placeholder)
|
||||
defaultPlaceholders(context)
|
||||
size(thumbSize)
|
||||
scale(Scale.FILL)
|
||||
allowRgb565(true)
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.reader.ui.thumbnails.adapter
|
||||
package org.koitharu.kotatsu.details.ui.pager.pages
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
@@ -9,7 +9,6 @@ import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||
import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail
|
||||
|
||||
class PageThumbnailAdapter(
|
||||
coil: ImageLoader,
|
||||
@@ -30,14 +30,13 @@ import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.core.util.ext.showOrHide
|
||||
import org.koitharu.kotatsu.databinding.FragmentPagesBinding
|
||||
import org.koitharu.kotatsu.details.ui.DetailsViewModel
|
||||
import org.koitharu.kotatsu.list.ui.GridSpanResolver
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderNavigationCallback
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail
|
||||
import org.koitharu.kotatsu.reader.ui.thumbnails.adapter.PageThumbnailAdapter
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@@ -56,7 +55,7 @@ class PagesFragment :
|
||||
lateinit var settings: AppSettings
|
||||
|
||||
private var thumbnailsAdapter: PageThumbnailAdapter? = null
|
||||
private var spanResolver: PagesGridSpanResolver? = null
|
||||
private var spanResolver: GridSpanResolver? = null
|
||||
private var scrollListener: ScrollListener? = null
|
||||
|
||||
private val spanSizeLookup = SpanSizeLookup()
|
||||
@@ -83,7 +82,7 @@ class PagesFragment :
|
||||
|
||||
override fun onViewBindingCreated(binding: FragmentPagesBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
spanResolver = PagesGridSpanResolver(binding.root.resources)
|
||||
spanResolver = GridSpanResolver(binding.root.resources)
|
||||
thumbnailsAdapter = PageThumbnailAdapter(
|
||||
coil = coil,
|
||||
lifecycleOwner = viewLifecycleOwner,
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
package org.koitharu.kotatsu.details.ui.pager.pages
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.koitharu.kotatsu.R
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class PagesGridSpanResolver(
|
||||
resources: Resources,
|
||||
) : View.OnLayoutChangeListener {
|
||||
|
||||
var spanCount = 3
|
||||
private set
|
||||
|
||||
private val gridWidth = resources.getDimension(R.dimen.preferred_grid_width)
|
||||
private val spacing = resources.getDimension(R.dimen.grid_spacing)
|
||||
private var cellWidth = -1f
|
||||
|
||||
override fun onLayoutChange(
|
||||
v: View?,
|
||||
left: Int,
|
||||
top: Int,
|
||||
right: Int,
|
||||
bottom: Int,
|
||||
oldLeft: Int,
|
||||
oldTop: Int,
|
||||
oldRight: Int,
|
||||
oldBottom: Int,
|
||||
) {
|
||||
if (cellWidth <= 0f) {
|
||||
return
|
||||
}
|
||||
val rv = v as? RecyclerView ?: return
|
||||
val width = abs(right - left)
|
||||
if (width == 0) {
|
||||
return
|
||||
}
|
||||
resolveGridSpanCount(width)
|
||||
(rv.layoutManager as? GridLayoutManager)?.spanCount = spanCount
|
||||
}
|
||||
|
||||
fun setGridSize(scaleFactor: Float, rv: RecyclerView) {
|
||||
cellWidth = (gridWidth * scaleFactor) + spacing
|
||||
val lm = rv.layoutManager as? GridLayoutManager ?: return
|
||||
val innerWidth = lm.width - lm.paddingEnd - lm.paddingStart
|
||||
if (innerWidth > 0) {
|
||||
resolveGridSpanCount(innerWidth)
|
||||
lm.spanCount = spanCount
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveGridSpanCount(width: Int) {
|
||||
val estimatedCount = (width / cellWidth).roundToInt()
|
||||
spanCount = estimatedCount.coerceAtLeast(2)
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@ import org.koitharu.kotatsu.details.data.MangaDetails
|
||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.reader.domain.ChaptersLoader
|
||||
import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
@@ -76,7 +75,9 @@ class PagesViewModel @Inject constructor(
|
||||
|
||||
private suspend fun doInit(state: State) {
|
||||
chaptersLoader.init(state.details)
|
||||
val initialChapterId = state.history?.chapterId ?: state.details.allChapters.firstOrNull()?.id ?: return
|
||||
val initialChapterId = state.history?.chapterId?.takeIf {
|
||||
chaptersLoader.peekChapter(it) != null
|
||||
} ?: state.details.allChapters.firstOrNull()?.id ?: return
|
||||
if (!chaptersLoader.hasPages(initialChapterId)) {
|
||||
chaptersLoader.loadSingleChapter(initialChapterId)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.lifecycle.LifecycleOwner
|
||||
import coil.ImageLoader
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
|
||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||
import org.koitharu.kotatsu.databinding.ItemScrobblingInfoBinding
|
||||
@@ -24,9 +25,7 @@ fun scrobblingInfoAD(
|
||||
|
||||
bind {
|
||||
binding.imageViewCover.newImageRequest(lifecycleOwner, item.coverUrl)?.run {
|
||||
placeholder(R.drawable.ic_placeholder)
|
||||
fallback(R.drawable.ic_placeholder)
|
||||
error(R.drawable.ic_error_placeholder)
|
||||
defaultPlaceholders(context)
|
||||
enqueueWith(coil)
|
||||
}
|
||||
binding.textViewTitle.setText(item.scrobbler.titleResId)
|
||||
|
||||
@@ -18,6 +18,7 @@ import coil.ImageLoader
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
|
||||
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
|
||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||
@@ -129,9 +130,7 @@ class ScrobblingInfoSheet :
|
||||
binding.imageViewLogo.contentDescription = getString(scrobbling.scrobbler.titleResId)
|
||||
binding.imageViewLogo.setImageResource(scrobbling.scrobbler.iconResId)
|
||||
binding.imageViewCover.newImageRequest(viewLifecycleOwner, scrobbling.coverUrl)?.apply {
|
||||
placeholder(R.drawable.ic_placeholder)
|
||||
fallback(R.drawable.ic_placeholder)
|
||||
error(R.drawable.ic_error_placeholder)
|
||||
defaultPlaceholders(binding.imageViewCover.context)
|
||||
enqueueWith(coil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import kotlinx.coroutines.launch
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
|
||||
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
|
||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||
import org.koitharu.kotatsu.core.util.ext.source
|
||||
@@ -88,9 +89,7 @@ fun downloadItemAD(
|
||||
binding.textViewTitle.text = item.manga?.title ?: getString(R.string.unknown)
|
||||
if ((CoilUtils.result(binding.imageViewCover) as? SuccessResult)?.memoryCacheKey != item.coverCacheKey) {
|
||||
binding.imageViewCover.newImageRequest(lifecycleOwner, item.manga?.coverUrl)?.apply {
|
||||
placeholder(R.drawable.ic_placeholder)
|
||||
fallback(R.drawable.ic_placeholder)
|
||||
error(R.drawable.ic_error_placeholder)
|
||||
defaultPlaceholders(context)
|
||||
allowRgb565(true)
|
||||
transformations(TrimTransformation())
|
||||
memoryCacheKey(item.coverCacheKey)
|
||||
|
||||
@@ -111,7 +111,11 @@ class ExploreFragment :
|
||||
}
|
||||
|
||||
override fun onListHeaderClick(item: ListHeader, view: View) {
|
||||
startActivity(Intent(view.context, SourcesCatalogActivity::class.java))
|
||||
if (item.payload == R.id.nav_suggestions) {
|
||||
startActivity(SuggestionsActivity.newIntent(view.context))
|
||||
} else {
|
||||
startActivity(Intent(view.context, SourcesCatalogActivity::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPrimaryButtonClick(tipView: TipView) {
|
||||
|
||||
@@ -25,10 +25,12 @@ import org.koitharu.kotatsu.explore.domain.ExploreRepository
|
||||
import org.koitharu.kotatsu.explore.ui.model.ExploreButtons
|
||||
import org.koitharu.kotatsu.explore.ui.model.MangaSourceItem
|
||||
import org.koitharu.kotatsu.explore.ui.model.RecommendationsItem
|
||||
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
|
||||
import org.koitharu.kotatsu.list.ui.model.EmptyHint
|
||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||
import org.koitharu.kotatsu.list.ui.model.MangaListModel
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
@@ -116,20 +118,20 @@ class ExploreViewModel @Inject constructor(
|
||||
sourcesRepository.observeNewSources(),
|
||||
) { content, suggestions, grid, randomLoading, newSources ->
|
||||
buildList(content, suggestions, grid, randomLoading, newSources)
|
||||
}
|
||||
}.withErrorHandling()
|
||||
|
||||
private fun buildList(
|
||||
sources: List<MangaSource>,
|
||||
recommendation: Manga?,
|
||||
recommendation: List<Manga>,
|
||||
isGrid: Boolean,
|
||||
randomLoading: Boolean,
|
||||
newSources: Set<MangaSource>,
|
||||
): List<ListModel> {
|
||||
val result = ArrayList<ListModel>(sources.size + 3)
|
||||
result += ExploreButtons(randomLoading)
|
||||
if (recommendation != null) {
|
||||
result += ListHeader(R.string.suggestions)
|
||||
result += RecommendationsItem(recommendation)
|
||||
if (recommendation.isNotEmpty()) {
|
||||
result += ListHeader(R.string.suggestions, R.string.more, R.id.nav_suggestions)
|
||||
result += RecommendationsItem(recommendation.toRecommendationList())
|
||||
}
|
||||
if (sources.isNotEmpty()) {
|
||||
result += ListHeader(
|
||||
@@ -157,16 +159,29 @@ class ExploreViewModel @Inject constructor(
|
||||
private fun getSuggestionFlow() = isSuggestionsEnabled.mapLatest { isEnabled ->
|
||||
if (isEnabled) {
|
||||
runCatchingCancellable {
|
||||
suggestionRepository.getRandom()
|
||||
}.getOrNull()
|
||||
suggestionRepository.getRandomList(SUGGESTIONS_COUNT)
|
||||
}.getOrDefault(emptyList())
|
||||
} else {
|
||||
null
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Manga>.toRecommendationList() = map { manga ->
|
||||
MangaListModel(
|
||||
id = manga.id,
|
||||
title = manga.title,
|
||||
subtitle = manga.tags.joinToString { it.title },
|
||||
coverUrl = manga.coverUrl,
|
||||
manga = manga,
|
||||
counter = 0,
|
||||
progress = PROGRESS_NONE,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TIP_SUGGESTIONS = "suggestions"
|
||||
private const val SUGGESTIONS_COUNT = 8
|
||||
const val TIP_NEW_SOURCES = "new_sources"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ class ExploreAdapter(
|
||||
addDelegate(ListItemType.EXPLORE_BUTTONS, exploreButtonsAD(listener))
|
||||
addDelegate(
|
||||
ListItemType.EXPLORE_SUGGESTION,
|
||||
exploreRecommendationItemAD(coil, listener, mangaClickListener, lifecycleOwner),
|
||||
exploreRecommendationItemAD(coil, mangaClickListener, lifecycleOwner),
|
||||
)
|
||||
addDelegate(ListItemType.HEADER, listHeaderAD(listener))
|
||||
addDelegate(ListItemType.EXPLORE_SOURCE_LIST, exploreSourceListItemAD(coil, clickListener, lifecycleOwner))
|
||||
|
||||
@@ -8,12 +8,15 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.getSummary
|
||||
import org.koitharu.kotatsu.core.model.getTitle
|
||||
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
|
||||
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
|
||||
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
|
||||
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
|
||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||
import org.koitharu.kotatsu.core.util.ext.recyclerView
|
||||
import org.koitharu.kotatsu.core.util.ext.setOnContextClickListenerCompat
|
||||
import org.koitharu.kotatsu.core.util.ext.setProgressIcon
|
||||
import org.koitharu.kotatsu.core.util.ext.source
|
||||
@@ -22,10 +25,13 @@ import org.koitharu.kotatsu.databinding.ItemExploreButtonsBinding
|
||||
import org.koitharu.kotatsu.databinding.ItemExploreSourceGridBinding
|
||||
import org.koitharu.kotatsu.databinding.ItemExploreSourceListBinding
|
||||
import org.koitharu.kotatsu.databinding.ItemRecommendationBinding
|
||||
import org.koitharu.kotatsu.databinding.ItemRecommendationMangaBinding
|
||||
import org.koitharu.kotatsu.explore.ui.model.ExploreButtons
|
||||
import org.koitharu.kotatsu.explore.ui.model.MangaSourceItem
|
||||
import org.koitharu.kotatsu.explore.ui.model.RecommendationsItem
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.MangaListModel
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
|
||||
fun exploreButtonsAD(
|
||||
@@ -51,25 +57,40 @@ fun exploreButtonsAD(
|
||||
|
||||
fun exploreRecommendationItemAD(
|
||||
coil: ImageLoader,
|
||||
clickListener: View.OnClickListener,
|
||||
itemClickListener: OnListItemClickListener<Manga>,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
) = adapterDelegateViewBinding<RecommendationsItem, ListModel, ItemRecommendationBinding>(
|
||||
{ layoutInflater, parent -> ItemRecommendationBinding.inflate(layoutInflater, parent, false) },
|
||||
) {
|
||||
|
||||
binding.buttonMore.setOnClickListener(clickListener)
|
||||
val adapter = BaseListAdapter<MangaListModel>()
|
||||
.addDelegate(ListItemType.MANGA_LIST, recommendationMangaItemAD(coil, itemClickListener, lifecycleOwner))
|
||||
binding.pager.adapter = adapter
|
||||
binding.pager.recyclerView?.isNestedScrollingEnabled = false
|
||||
binding.dots.bindToViewPager(binding.pager)
|
||||
|
||||
bind {
|
||||
adapter.items = item.manga
|
||||
}
|
||||
}
|
||||
|
||||
fun recommendationMangaItemAD(
|
||||
coil: ImageLoader,
|
||||
itemClickListener: OnListItemClickListener<Manga>,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
) = adapterDelegateViewBinding<MangaListModel, MangaListModel, ItemRecommendationMangaBinding>(
|
||||
{ layoutInflater, parent -> ItemRecommendationMangaBinding.inflate(layoutInflater, parent, false) },
|
||||
) {
|
||||
|
||||
binding.root.setOnClickListener { v ->
|
||||
itemClickListener.onItemClick(item.manga, v)
|
||||
}
|
||||
|
||||
bind {
|
||||
binding.textViewTitle.text = item.manga.title
|
||||
binding.textViewSubtitle.textAndVisible = item.summary
|
||||
binding.textViewSubtitle.textAndVisible = item.subtitle
|
||||
binding.imageViewCover.newImageRequest(lifecycleOwner, item.manga.coverUrl)?.run {
|
||||
placeholder(R.drawable.ic_placeholder)
|
||||
fallback(R.drawable.ic_placeholder)
|
||||
error(R.drawable.ic_error_placeholder)
|
||||
defaultPlaceholders(context)
|
||||
allowRgb565(true)
|
||||
transformations(TrimTransformation())
|
||||
source(item.manga.source)
|
||||
@@ -78,6 +99,7 @@ fun exploreRecommendationItemAD(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun exploreSourceListItemAD(
|
||||
coil: ImageLoader,
|
||||
listener: OnListItemClickListener<MangaSourceItem>,
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package org.koitharu.kotatsu.explore.ui.model
|
||||
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.list.ui.model.MangaListModel
|
||||
|
||||
data class RecommendationsItem(
|
||||
val manga: Manga
|
||||
val manga: List<MangaListModel>
|
||||
) : ListModel {
|
||||
val summary: String = manga.tags.joinToString { it.title }
|
||||
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
return other is RecommendationsItem
|
||||
|
||||
@@ -210,6 +210,7 @@ abstract class FavouritesDao {
|
||||
ListSortOrder.UNREAD -> "IFNULL((SELECT percent FROM history WHERE history.manga_id = manga.manga_id), 0) ASC"
|
||||
ListSortOrder.LAST_READ -> "IFNULL((SELECT updated_at FROM history WHERE history.manga_id = manga.manga_id), 0) DESC"
|
||||
ListSortOrder.LONG_AGO_READ -> "IFNULL((SELECT updated_at FROM history WHERE history.manga_id = manga.manga_id), 0) ASC"
|
||||
ListSortOrder.UPDATED -> "IFNULL((SELECT last_chapter_date FROM tracks WHERE tracks.manga_id = manga.manga_id), 0) DESC"
|
||||
|
||||
else -> throw IllegalArgumentException("Sort order $sortOrder is not supported")
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user