Compare commits
164 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dddb00d5ef | ||
|
|
c9d878a0b7 | ||
|
|
dcb92ed1af | ||
|
|
749bc4a837 | ||
|
|
94807b7788 | ||
|
|
0fe7c66850 | ||
|
|
20cd8413dc | ||
|
|
30df4ede6c | ||
|
|
4aa6baf569 | ||
|
|
d8a4303c50 | ||
|
|
b355e2ee88 | ||
|
|
55e3b5fb9b | ||
|
|
a59853e37a | ||
|
|
ccc665d218 | ||
|
|
02650f5c2a | ||
|
|
24172a1137 | ||
|
|
034d69d490 | ||
|
|
12fc0542d3 | ||
|
|
dcf80ed396 | ||
|
|
28badb7f6c | ||
|
|
19cc158ef8 | ||
|
|
a2eeae3319 | ||
|
|
c9336a753d | ||
|
|
ea23468ecd | ||
|
|
143643fcd8 | ||
|
|
25eb05d305 | ||
|
|
bf217b3cbf | ||
|
|
9e2b60e15e | ||
|
|
4dba90361c | ||
|
|
8dea483f64 | ||
|
|
dc2e603356 | ||
|
|
14973298a0 | ||
|
|
7efc47724e | ||
|
|
c51218240e | ||
|
|
2762caaa8f | ||
|
|
70d66e5a90 | ||
|
|
3173e30caf | ||
|
|
0dccc66f54 | ||
|
|
6b3dd23c01 | ||
|
|
1c6a125174 | ||
|
|
15f37644c0 | ||
|
|
c2079ebca5 | ||
|
|
1146269992 | ||
|
|
099362d198 | ||
|
|
22d203fc60 | ||
|
|
19602144ef | ||
|
|
44bbcd7fe3 | ||
|
|
efe5e07c2c | ||
|
|
4e633ff735 | ||
|
|
fef8333763 | ||
|
|
a741f8451a | ||
|
|
55baf5a3f3 | ||
|
|
6390774d86 | ||
|
|
51a5128e70 | ||
|
|
53d81507e4 | ||
|
|
dcf7236ba2 | ||
|
|
a54744abc6 | ||
|
|
22e2411c77 | ||
|
|
3f66c142b8 | ||
|
|
734846a018 | ||
|
|
e75035b33a | ||
|
|
f675c606a2 | ||
|
|
a5199e2f06 | ||
|
|
1b80e48ed4 | ||
|
|
07e81f21c7 | ||
|
|
0dbd01f6fc | ||
|
|
4b453b58dd | ||
|
|
1575bb5242 | ||
|
|
55137cf899 | ||
|
|
f190ff810e | ||
|
|
47c13b46f7 | ||
|
|
2ad9f38906 | ||
|
|
2783c62ace | ||
|
|
c1a65f8055 | ||
|
|
6e5d8e99ca | ||
|
|
020c3b8bba | ||
|
|
76162a06e3 | ||
|
|
19f398d309 | ||
|
|
25ae23963e | ||
|
|
146ba95af6 | ||
|
|
ee10b013a1 | ||
|
|
8c79df3d35 | ||
|
|
2c2db1ca96 | ||
|
|
f556c0b127 | ||
|
|
66645d93f8 | ||
|
|
f2582bce1d | ||
|
|
3ef7c6adb0 | ||
|
|
62e7e5d8c3 | ||
|
|
30e43d3bfe | ||
|
|
0162eaed97 | ||
|
|
15ca4111c0 | ||
|
|
dc45e0f5df | ||
|
|
09b6a967a1 | ||
|
|
1cff0eeac4 | ||
|
|
44349c4ede | ||
|
|
8e8953b07f | ||
|
|
150e3d554f | ||
|
|
be3b5a1897 | ||
|
|
9be0e8595f | ||
|
|
f38370592e | ||
|
|
6a54d42867 | ||
|
|
49d29ae675 | ||
|
|
27d7a6a8cb | ||
|
|
e8d04644f8 | ||
|
|
26b512d42e | ||
|
|
4fb3173185 | ||
|
|
826587b2c9 | ||
|
|
4efdb1d8d1 | ||
|
|
1b9f886d1b | ||
|
|
3241ae5db5 | ||
|
|
30f1b2c73a | ||
|
|
8d35101e98 | ||
|
|
41cfd99d32 | ||
|
|
c8d04e4eb7 | ||
|
|
956831f9d7 | ||
|
|
d65874080b | ||
|
|
bf35a8ffd7 | ||
|
|
eeb8dd8c5b | ||
|
|
299093f863 | ||
|
|
86dea2953a | ||
|
|
81794e6eb2 | ||
|
|
d43887e288 | ||
|
|
e2cf22e054 | ||
|
|
5a75fe77fd | ||
|
|
8c0617c525 | ||
|
|
38b8966c16 | ||
|
|
59f4ff8a3e | ||
|
|
357263b496 | ||
|
|
4af6fc165b | ||
|
|
a4de58b9b3 | ||
|
|
5696ad7fa2 | ||
|
|
63bfca6d3e | ||
|
|
0fecf996e1 | ||
|
|
3df2682332 | ||
|
|
dd9df6e9dc | ||
|
|
0889c2cc28 | ||
|
|
010b1264ae | ||
|
|
66ff32e14d | ||
|
|
addb642cc9 | ||
|
|
720c389dbd | ||
|
|
2191d9c83b | ||
|
|
0ee1cda0e4 | ||
|
|
90226b7b78 | ||
|
|
6d84294533 | ||
|
|
36bd3cc438 | ||
|
|
e0c983f4eb | ||
|
|
ea5ce23335 | ||
|
|
26a33e5d9d | ||
|
|
9ab7159cb9 | ||
|
|
ad21321a1d | ||
|
|
fe2bb05895 | ||
|
|
e48beae324 | ||
|
|
10109ab2c0 | ||
|
|
df17bb5af8 | ||
|
|
b4592015fb | ||
|
|
3fe9ec6918 | ||
|
|
23ac9df844 | ||
|
|
c480992f63 | ||
|
|
85d397def0 | ||
|
|
7c74c87524 | ||
|
|
f86ee7d5c2 | ||
|
|
6e5519419d | ||
|
|
2c53b63847 | ||
|
|
45b5e48676 |
6
.idea/AndroidProjectSystem.xml
generated
Normal file
6
.idea/AndroidProjectSystem.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AndroidProjectSystem">
|
||||
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -18,8 +18,8 @@ android {
|
||||
applicationId 'org.koitharu.kotatsu'
|
||||
minSdk = 21
|
||||
targetSdk = 35
|
||||
versionCode = 697
|
||||
versionName = '7.7.5'
|
||||
versionCode = 700
|
||||
versionName = '8.0-a1'
|
||||
generatedDensities = []
|
||||
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
|
||||
ksp {
|
||||
@@ -74,6 +74,7 @@ android {
|
||||
'-opt-in=kotlinx.coroutines.FlowPreview',
|
||||
'-opt-in=kotlin.contracts.ExperimentalContracts',
|
||||
'-opt-in=coil3.annotation.ExperimentalCoilApi',
|
||||
'-opt-in=coil3.annotation.InternalCoilApi',
|
||||
]
|
||||
}
|
||||
lint {
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
android:allowBackup="true"
|
||||
android:backupAgent="org.koitharu.kotatsu.settings.backup.AppBackupAgent"
|
||||
android:dataExtractionRules="@xml/backup_rules"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:enableOnBackInvokedCallback="@bool/is_predictive_back_enabled"
|
||||
android:fullBackupContent="@xml/backup_content"
|
||||
android:fullBackupOnly="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
@@ -279,6 +279,10 @@
|
||||
<service
|
||||
android:name="org.koitharu.kotatsu.local.ui.LocalIndexUpdateService"
|
||||
android:label="@string/local_manga_processing" />
|
||||
<service
|
||||
android:name="org.koitharu.kotatsu.local.ui.ImportService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:label="@string/importing_manga" />
|
||||
<service
|
||||
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetService"
|
||||
android:label="@string/manga_shelf"
|
||||
|
||||
@@ -29,8 +29,9 @@ class AutoFixUseCase @Inject constructor(
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(mangaId: Long): Pair<Manga, Manga?> {
|
||||
val seed = checkNotNull(mangaDataRepository.findMangaById(mangaId)) { "Manga $mangaId not found" }
|
||||
.getDetailsSafe()
|
||||
val seed = checkNotNull(
|
||||
mangaDataRepository.findMangaById(mangaId, withChapters = true),
|
||||
) { "Manga $mangaId not found" }.getDetailsSafe()
|
||||
if (seed.isHealthy()) {
|
||||
return seed to null // no fix required
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package org.koitharu.kotatsu.alternatives.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
@@ -13,8 +11,7 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||
import org.koitharu.kotatsu.core.model.getTitle
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||
import org.koitharu.kotatsu.core.nav.router
|
||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||
import org.koitharu.kotatsu.core.ui.dialog.buildAlertDialog
|
||||
@@ -22,7 +19,6 @@ import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.databinding.ActivityAlternativesBinding
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
|
||||
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
|
||||
@@ -30,8 +26,6 @@ import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
|
||||
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
@@ -65,7 +59,7 @@ class AlternativesActivity : BaseActivity<ActivityAlternativesBinding>(),
|
||||
viewModel.content.observe(this, listAdapter)
|
||||
viewModel.onMigrated.observeEvent(this) {
|
||||
Toast.makeText(this, R.string.migration_completed, Toast.LENGTH_SHORT).show()
|
||||
startActivity(DetailsActivity.newIntent(this, it))
|
||||
router.openDetails(it)
|
||||
finishAfterTransition()
|
||||
}
|
||||
}
|
||||
@@ -82,16 +76,9 @@ class AlternativesActivity : BaseActivity<ActivityAlternativesBinding>(),
|
||||
|
||||
override fun onItemClick(item: MangaAlternativeModel, view: View) {
|
||||
when (view.id) {
|
||||
R.id.chip_source -> startActivity(
|
||||
MangaListActivity.newIntent(
|
||||
this,
|
||||
item.manga.source,
|
||||
MangaListFilter(query = viewModel.manga.title),
|
||||
),
|
||||
)
|
||||
|
||||
R.id.chip_source -> router.openSearch(item.manga.source, viewModel.manga.title)
|
||||
R.id.button_migrate -> confirmMigration(item.manga)
|
||||
else -> startActivity(DetailsActivity.newIntent(this, item.manga))
|
||||
else -> router.openDetails(item.manga)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,10 +101,4 @@ class AlternativesActivity : BaseActivity<ActivityAlternativesBinding>(),
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun newIntent(context: Context, manga: Manga) = Intent(context, AlternativesActivity::class.java)
|
||||
.putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import org.koitharu.kotatsu.alternatives.domain.AlternativesUseCase
|
||||
import org.koitharu.kotatsu.alternatives.domain.MigrateUseCase
|
||||
import org.koitharu.kotatsu.core.model.chaptersCount
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
@@ -40,7 +40,7 @@ class AlternativesViewModel @Inject constructor(
|
||||
private val settings: AppSettings,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val manga = savedStateHandle.require<ParcelableManga>(MangaIntent.KEY_MANGA).manga
|
||||
val manga = savedStateHandle.require<ParcelableManga>(AppRouter.KEY_MANGA).manga
|
||||
|
||||
val onMigrated = MutableEventFlow<Manga>()
|
||||
val content = MutableStateFlow<List<ListModel>>(listOf(LoadingState))
|
||||
|
||||
@@ -10,7 +10,6 @@ import androidx.core.app.NotificationChannelCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.PendingIntentCompat
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import coil3.ImageLoader
|
||||
import coil3.request.ImageRequest
|
||||
@@ -20,13 +19,13 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.alternatives.domain.AutoFixUseCase
|
||||
import org.koitharu.kotatsu.core.ErrorReporterReceiver
|
||||
import org.koitharu.kotatsu.core.model.getTitle
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.ui.CoroutineIntentService
|
||||
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.toBitmapOrNull
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import javax.inject.Inject
|
||||
@@ -122,7 +121,7 @@ class AutoFixService : CoroutineIntentService() {
|
||||
).toBitmapOrNull(),
|
||||
)
|
||||
notification.setSubText(replacement.title)
|
||||
val intent = DetailsActivity.newIntent(applicationContext, replacement)
|
||||
val intent = AppRouter.detailsIntent(applicationContext, replacement)
|
||||
notification.setContentIntent(
|
||||
PendingIntentCompat.getActivity(
|
||||
applicationContext,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package org.koitharu.kotatsu.bookmarks.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.graphics.Insets
|
||||
@@ -46,9 +44,4 @@ class AllBookmarksActivity :
|
||||
right = insets.right,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun newIntent(context: Context) = Intent(context, AllBookmarksActivity::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksAdapter
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||
import org.koitharu.kotatsu.core.nav.ReaderIntent
|
||||
import org.koitharu.kotatsu.core.nav.router
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
|
||||
@@ -30,7 +32,6 @@ import org.koitharu.kotatsu.core.util.ext.findAppCompatDelegate
|
||||
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.GridSpanResolver
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListHeaderClickListener
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||
@@ -39,7 +40,6 @@ import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
|
||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
@@ -115,26 +115,26 @@ class AllBookmarksFragment :
|
||||
|
||||
override fun onItemClick(item: Bookmark, view: View) {
|
||||
if (selectionController?.onItemClick(item.pageId) != true) {
|
||||
val intent = ReaderActivity.IntentBuilder(view.context)
|
||||
val intent = ReaderIntent.Builder(view.context)
|
||||
.bookmark(item)
|
||||
.incognito(true)
|
||||
.build()
|
||||
startActivity(intent)
|
||||
router.openReader(intent)
|
||||
Toast.makeText(view.context, R.string.incognito_mode, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onListHeaderClick(item: ListHeader, view: View) {
|
||||
val manga = item.payload as? Manga ?: return
|
||||
startActivity(DetailsActivity.newIntent(view.context, manga))
|
||||
router.openDetails(manga)
|
||||
}
|
||||
|
||||
override fun onItemLongClick(item: Bookmark, view: View): Boolean {
|
||||
return selectionController?.onItemLongClick(view, item.pageId) ?: false
|
||||
return selectionController?.onItemLongClick(view, item.pageId) == true
|
||||
}
|
||||
|
||||
override fun onItemContextClick(item: Bookmark, view: View): Boolean {
|
||||
return selectionController?.onItemContextClick(view, item.pageId) ?: false
|
||||
return selectionController?.onItemContextClick(view, item.pageId) == true
|
||||
}
|
||||
|
||||
override fun onRetryClick(error: Throwable) = Unit
|
||||
@@ -208,16 +208,4 @@ class AllBookmarksFragment :
|
||||
invalidateSpanIndexCache()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@Deprecated(
|
||||
"",
|
||||
ReplaceWith(
|
||||
"BookmarksFragment()",
|
||||
"org.koitharu.kotatsu.bookmarks.ui.BookmarksFragment",
|
||||
),
|
||||
)
|
||||
fun newInstance() = AllBookmarksFragment()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,23 @@
|
||||
package org.koitharu.kotatsu.browser
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.webkit.CookieManager
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.nav.router
|
||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.parser.ParserMangaRepository
|
||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.core.util.ext.configureForParser
|
||||
import org.koitharu.kotatsu.core.util.ext.toUriOrNull
|
||||
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import javax.inject.Inject
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
@@ -42,11 +38,10 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)
|
||||
}
|
||||
val mangaSource = MangaSource(intent?.getStringExtra(EXTRA_SOURCE))
|
||||
val mangaSource = MangaSource(intent?.getStringExtra(AppRouter.KEY_SOURCE))
|
||||
val repository = mangaRepositoryFactory.create(mangaSource) as? ParserMangaRepository
|
||||
val userAgent = repository?.getRequestHeaders()?.get(CommonHeaders.USER_AGENT)
|
||||
viewBinding.webView.configureForParser(userAgent)
|
||||
CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true)
|
||||
viewBinding.webView.webViewClient = BrowserClient(this)
|
||||
viewBinding.webView.webChromeClient = ProgressChromeClient(viewBinding.progressBar)
|
||||
onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView)
|
||||
@@ -59,7 +54,7 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
|
||||
finishAfterTransition()
|
||||
} else {
|
||||
onTitleChanged(
|
||||
intent?.getStringExtra(EXTRA_TITLE) ?: getString(R.string.loading_),
|
||||
intent?.getStringExtra(AppRouter.KEY_TITLE) ?: getString(R.string.loading_),
|
||||
url,
|
||||
)
|
||||
viewBinding.webView.loadUrl(url)
|
||||
@@ -80,14 +75,8 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
|
||||
}
|
||||
|
||||
R.id.action_browser -> {
|
||||
val url = viewBinding.webView.url?.toUriOrNull()
|
||||
if (url != null) {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.data = url
|
||||
try {
|
||||
startActivity(Intent.createChooser(intent, item.title))
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
}
|
||||
if (!router.openExternalBrowser(viewBinding.webView.url.orEmpty(), item.title)) {
|
||||
Snackbar.make(viewBinding.webView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -136,17 +125,4 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
|
||||
bottom = insets.bottom,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val EXTRA_TITLE = "title"
|
||||
private const val EXTRA_SOURCE = "source"
|
||||
|
||||
fun newIntent(context: Context, url: String, source: MangaSource?, title: String?): Intent {
|
||||
return Intent(context, BrowserActivity::class.java)
|
||||
.setData(Uri.parse(url))
|
||||
.putExtra(EXTRA_TITLE, title)
|
||||
.putExtra(EXTRA_SOURCE, source?.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
||||
import org.koitharu.kotatsu.core.model.getTitle
|
||||
import org.koitharu.kotatsu.core.model.isNsfw
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
|
||||
@@ -38,7 +39,7 @@ class CaptchaNotifier(
|
||||
.build()
|
||||
manager.createNotificationChannel(channel)
|
||||
|
||||
val intent = CloudFlareActivity.newIntent(context, exception)
|
||||
val intent = AppRouter.cloudFlareResolveIntent(context, exception)
|
||||
.setData(exception.url.toUri())
|
||||
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setContentTitle(channel.name)
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
package org.koitharu.kotatsu.browser.cloudflare
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.webkit.CookieManager
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
@@ -19,19 +16,17 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import kotlinx.coroutines.yield
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.browser.WebViewBackPressedCallback
|
||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.core.util.ext.configureForParser
|
||||
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.network.CloudFlareHelper
|
||||
import javax.inject.Inject
|
||||
import com.google.android.material.R as materialR
|
||||
@@ -62,12 +57,11 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
|
||||
return
|
||||
}
|
||||
cfClient = CloudFlareClient(cookieJar, this, url)
|
||||
viewBinding.webView.configureForParser(intent?.getStringExtra(ARG_UA))
|
||||
viewBinding.webView.configureForParser(intent?.getStringExtra(AppRouter.KEY_USER_AGENT))
|
||||
viewBinding.webView.webViewClient = cfClient
|
||||
onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView).also {
|
||||
onBackPressedDispatcher.addCallback(it)
|
||||
}
|
||||
CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true)
|
||||
if (savedInstanceState == null) {
|
||||
onTitleChanged(getString(R.string.loading_), url)
|
||||
viewBinding.webView.loadUrl(url)
|
||||
@@ -140,7 +134,7 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
|
||||
|
||||
override fun onCheckPassed() {
|
||||
pendingResult = RESULT_OK
|
||||
val source = intent?.getStringExtra(ARG_SOURCE)
|
||||
val source = intent?.getStringExtra(AppRouter.KEY_SOURCE)
|
||||
if (source != null) {
|
||||
CaptchaNotifier(this).dismiss(MangaSource(source))
|
||||
}
|
||||
@@ -182,38 +176,16 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
|
||||
|
||||
class Contract : ActivityResultContract<CloudFlareProtectedException, Boolean>() {
|
||||
override fun createIntent(context: Context, input: CloudFlareProtectedException): Intent {
|
||||
return newIntent(context, input)
|
||||
return AppRouter.cloudFlareResolveIntent(context, input)
|
||||
}
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?): Boolean {
|
||||
return resultCode == Activity.RESULT_OK
|
||||
return resultCode == RESULT_OK
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val TAG = "CloudFlareActivity"
|
||||
private const val ARG_UA = "ua"
|
||||
private const val ARG_SOURCE = "_source"
|
||||
|
||||
fun newIntent(context: Context, exception: CloudFlareProtectedException) = newIntent(
|
||||
context = context,
|
||||
url = exception.url,
|
||||
source = exception.source,
|
||||
headers = exception.headers,
|
||||
)
|
||||
|
||||
private fun newIntent(
|
||||
context: Context,
|
||||
url: String,
|
||||
source: MangaSource?,
|
||||
headers: Headers?,
|
||||
) = Intent(context, CloudFlareActivity::class.java).apply {
|
||||
data = url.toUri()
|
||||
putExtra(ARG_SOURCE, source?.name)
|
||||
headers?.get(CommonHeaders.USER_AGENT)?.let {
|
||||
putExtra(ARG_UA, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.net.Uri
|
||||
import android.os.BadParcelableException
|
||||
import androidx.core.app.PendingIntentCompat
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.report
|
||||
@@ -15,20 +16,19 @@ import org.koitharu.kotatsu.core.util.ext.report
|
||||
class ErrorReporterReceiver : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
val e = intent?.getSerializableExtraCompat<Throwable>(EXTRA_ERROR) ?: return
|
||||
val e = intent?.getSerializableExtraCompat<Throwable>(AppRouter.KEY_ERROR) ?: return
|
||||
e.report()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val EXTRA_ERROR = "err"
|
||||
private const val ACTION_REPORT = "${BuildConfig.APPLICATION_ID}.action.REPORT_ERROR"
|
||||
|
||||
fun getPendingIntent(context: Context, e: Throwable): PendingIntent? = try {
|
||||
val intent = Intent(context, ErrorReporterReceiver::class.java)
|
||||
intent.setAction(ACTION_REPORT)
|
||||
intent.setData(Uri.parse("err://${e.hashCode()}"))
|
||||
intent.putExtra(EXTRA_ERROR, e)
|
||||
intent.putExtra(AppRouter.KEY_ERROR, e)
|
||||
PendingIntentCompat.getBroadcast(context, 0, intent, 0, false)
|
||||
} catch (e: BadParcelableException) {
|
||||
e.printStackTraceDebug()
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package org.koitharu.kotatsu.core.backup
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.CheckResult
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.asRequestBody
|
||||
import okhttp3.Response
|
||||
import okhttp3.internal.closeQuietly
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.network.BaseHttpClient
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.parsers.util.await
|
||||
import org.koitharu.kotatsu.parsers.util.json.getBooleanOrDefault
|
||||
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
|
||||
import org.koitharu.kotatsu.parsers.util.parseJson
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class TelegramBackupUploader @Inject constructor(
|
||||
private val settings: AppSettings,
|
||||
@BaseHttpClient private val client: OkHttpClient,
|
||||
@ApplicationContext private val context: Context,
|
||||
) {
|
||||
|
||||
private val botToken = context.getString(R.string.tg_backup_bot_token)
|
||||
|
||||
suspend fun uploadBackup(file: File) {
|
||||
val requestBody = file.asRequestBody("application/zip".toMediaTypeOrNull())
|
||||
val multipartBody = MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("chat_id", requireChatId())
|
||||
.addFormDataPart("document", file.name, requestBody)
|
||||
.build()
|
||||
val request = Request.Builder()
|
||||
.url(urlOf("sendDocument").build())
|
||||
.post(multipartBody)
|
||||
.build()
|
||||
client.newCall(request).await().consume()
|
||||
}
|
||||
|
||||
suspend fun sendTestMessage() {
|
||||
val request = Request.Builder()
|
||||
.url(urlOf("getMe").build())
|
||||
.build()
|
||||
client.newCall(request).await().consume()
|
||||
sendMessage(context.getString(R.string.backup_tg_echo))
|
||||
}
|
||||
|
||||
@CheckResult
|
||||
fun openBotInApp(router: AppRouter): Boolean {
|
||||
val botUsername = context.getString(R.string.tg_backup_bot_name)
|
||||
return router.openExternalBrowser("tg://resolve?domain=$botUsername") ||
|
||||
router.openExternalBrowser("https://t.me/$botUsername")
|
||||
}
|
||||
|
||||
private suspend fun sendMessage(message: String) {
|
||||
val url = urlOf("sendMessage")
|
||||
.addQueryParameter("chat_id", requireChatId())
|
||||
.addQueryParameter("text", message)
|
||||
.build()
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.build()
|
||||
client.newCall(request).await().consume()
|
||||
}
|
||||
|
||||
private fun requireChatId() = checkNotNull(settings.backupTelegramChatId) {
|
||||
"Telegram chat ID not set in settings"
|
||||
}
|
||||
|
||||
private fun Response.consume() {
|
||||
if (isSuccessful) {
|
||||
closeQuietly()
|
||||
return
|
||||
}
|
||||
val jo = parseJson()
|
||||
if (!jo.getBooleanOrDefault("ok", true)) {
|
||||
throw RuntimeException(jo.getStringOrNull("description"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun urlOf(method: String) = HttpUrl.Builder()
|
||||
.scheme("https")
|
||||
.host("api.telegram.org")
|
||||
.addPathSegment("bot$botToken")
|
||||
.addPathSegment(method)
|
||||
}
|
||||
@@ -12,11 +12,13 @@ import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koitharu.kotatsu.bookmarks.data.BookmarkEntity
|
||||
import org.koitharu.kotatsu.bookmarks.data.BookmarksDao
|
||||
import org.koitharu.kotatsu.core.db.dao.ChaptersDao
|
||||
import org.koitharu.kotatsu.core.db.dao.MangaDao
|
||||
import org.koitharu.kotatsu.core.db.dao.MangaSourcesDao
|
||||
import org.koitharu.kotatsu.core.db.dao.PreferencesDao
|
||||
import org.koitharu.kotatsu.core.db.dao.TagsDao
|
||||
import org.koitharu.kotatsu.core.db.dao.TrackLogsDao
|
||||
import org.koitharu.kotatsu.core.db.entity.ChapterEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
|
||||
@@ -36,6 +38,7 @@ import org.koitharu.kotatsu.core.db.migrations.Migration1To2
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration20To21
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration21To22
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration22To23
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration23To24
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration2To3
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration3To4
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration4To5
|
||||
@@ -63,14 +66,14 @@ import org.koitharu.kotatsu.tracker.data.TrackEntity
|
||||
import org.koitharu.kotatsu.tracker.data.TrackLogEntity
|
||||
import org.koitharu.kotatsu.tracker.data.TracksDao
|
||||
|
||||
const val DATABASE_VERSION = 23
|
||||
const val DATABASE_VERSION = 24
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class,
|
||||
FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class,
|
||||
TrackEntity::class, TrackLogEntity::class, SuggestionEntity::class, BookmarkEntity::class,
|
||||
ScrobblingEntity::class, MangaSourceEntity::class, StatsEntity::class, LocalMangaIndexEntity::class,
|
||||
MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class, ChapterEntity::class,
|
||||
FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class, TrackEntity::class,
|
||||
TrackLogEntity::class, SuggestionEntity::class, BookmarkEntity::class, ScrobblingEntity::class,
|
||||
MangaSourceEntity::class, StatsEntity::class, LocalMangaIndexEntity::class,
|
||||
],
|
||||
version = DATABASE_VERSION,
|
||||
)
|
||||
@@ -103,6 +106,8 @@ abstract class MangaDatabase : RoomDatabase() {
|
||||
abstract fun getStatsDao(): StatsDao
|
||||
|
||||
abstract fun getLocalMangaIndexDao(): LocalMangaIndexDao
|
||||
|
||||
abstract fun getChaptersDao(): ChaptersDao
|
||||
}
|
||||
|
||||
fun getDatabaseMigrations(context: Context): Array<Migration> = arrayOf(
|
||||
@@ -128,6 +133,7 @@ fun getDatabaseMigrations(context: Context): Array<Migration> = arrayOf(
|
||||
Migration20To21(),
|
||||
Migration21To22(),
|
||||
Migration22To23(),
|
||||
Migration23To24(),
|
||||
)
|
||||
|
||||
fun MangaDatabase(context: Context): MangaDatabase = Room
|
||||
|
||||
@@ -7,3 +7,4 @@ const val TABLE_FAVOURITE_CATEGORIES = "favourite_categories"
|
||||
const val TABLE_HISTORY = "history"
|
||||
const val TABLE_MANGA_TAGS = "manga_tags"
|
||||
const val TABLE_SOURCES = "sources"
|
||||
const val TABLE_CHAPTERS = "chapters"
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.koitharu.kotatsu.core.db.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import org.koitharu.kotatsu.core.db.entity.ChapterEntity
|
||||
|
||||
@Dao
|
||||
abstract class ChaptersDao {
|
||||
|
||||
@Query("SELECT * FROM chapters WHERE manga_id = :mangaId ORDER BY `index` ASC")
|
||||
abstract suspend fun findAll(mangaId: Long): List<ChapterEntity>
|
||||
|
||||
@Query("DELETE FROM chapters WHERE manga_id = :mangaId")
|
||||
abstract suspend fun deleteAll(mangaId: Long)
|
||||
|
||||
@Query("DELETE FROM chapters WHERE manga_id NOT IN (SELECT manga_id FROM history WHERE deleted_at = 0) AND manga_id NOT IN (SELECT manga_id FROM favourites WHERE deleted_at = 0)")
|
||||
abstract suspend fun gc()
|
||||
|
||||
@Transaction
|
||||
open suspend fun replaceAll(mangaId: Long, entities: Collection<ChapterEntity>) {
|
||||
deleteAll(mangaId)
|
||||
insert(entities)
|
||||
}
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
protected abstract suspend fun insert(entities: Collection<ChapterEntity>)
|
||||
}
|
||||
@@ -20,6 +20,9 @@ abstract class MangaDao {
|
||||
@Query("SELECT * FROM manga WHERE manga_id = :id")
|
||||
abstract suspend fun find(id: Long): MangaWithTags?
|
||||
|
||||
@Query("SELECT EXISTS(SELECT * FROM manga WHERE manga_id = :id)")
|
||||
abstract suspend operator fun contains(id: Long): Boolean
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM manga WHERE public_url = :publicUrl")
|
||||
abstract suspend fun findByPublicUrl(publicUrl: String): MangaWithTags?
|
||||
@@ -55,6 +58,19 @@ abstract class MangaDao {
|
||||
@Delete
|
||||
abstract suspend fun delete(subjects: Collection<MangaEntity>)
|
||||
|
||||
@Query(
|
||||
"""
|
||||
DELETE FROM manga WHERE NOT EXISTS(SELECT * FROM history WHERE history.manga_id == manga.manga_id)
|
||||
AND NOT EXISTS(SELECT * FROM favourites WHERE favourites.manga_id == manga.manga_id)
|
||||
AND NOT EXISTS(SELECT * FROM bookmarks WHERE bookmarks.manga_id == manga.manga_id)
|
||||
AND NOT EXISTS(SELECT * FROM suggestions WHERE suggestions.manga_id == manga.manga_id)
|
||||
AND NOT EXISTS(SELECT * FROM scrobblings WHERE scrobblings.manga_id == manga.manga_id)
|
||||
AND NOT EXISTS(SELECT * FROM local_index WHERE local_index.manga_id == manga.manga_id)
|
||||
AND manga.manga_id NOT IN (:idsToKeep)
|
||||
""",
|
||||
)
|
||||
abstract suspend fun cleanup(idsToKeep: Set<Long>)
|
||||
|
||||
@Transaction
|
||||
open suspend fun upsert(manga: MangaEntity, tags: Iterable<TagEntity>? = null) {
|
||||
upsert(manga)
|
||||
|
||||
@@ -10,7 +10,6 @@ import androidx.room.Upsert
|
||||
import androidx.sqlite.db.SimpleSQLiteQuery
|
||||
import androidx.sqlite.db.SupportSQLiteQuery
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.intellij.lang.annotations.Language
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
|
||||
import org.koitharu.kotatsu.explore.data.SourcesSortOrder
|
||||
@@ -61,21 +60,11 @@ abstract class MangaSourcesDao {
|
||||
@Query("SELECT * FROM sources WHERE pinned = 1")
|
||||
abstract suspend fun findAllPinned(): List<MangaSourceEntity>
|
||||
|
||||
fun observeEnabled(order: SourcesSortOrder): Flow<List<MangaSourceEntity>> {
|
||||
val orderBy = getOrderBy(order)
|
||||
fun observeAll(enabledOnly: Boolean, order: SourcesSortOrder): Flow<List<MangaSourceEntity>> =
|
||||
observeImpl(getQuery(enabledOnly, order))
|
||||
|
||||
@Language("RoomSql")
|
||||
val query = SimpleSQLiteQuery("SELECT * FROM sources WHERE enabled = 1 ORDER BY pinned DESC, $orderBy")
|
||||
return observeImpl(query)
|
||||
}
|
||||
|
||||
suspend fun findAllEnabled(order: SourcesSortOrder): List<MangaSourceEntity> {
|
||||
val orderBy = getOrderBy(order)
|
||||
|
||||
@Language("RoomSql")
|
||||
val query = SimpleSQLiteQuery("SELECT * FROM sources WHERE enabled = 1 ORDER BY pinned DESC, $orderBy")
|
||||
return findAllImpl(query)
|
||||
}
|
||||
suspend fun findAll(enabledOnly: Boolean, order: SourcesSortOrder): List<MangaSourceEntity> =
|
||||
findAllImpl(getQuery(enabledOnly, order))
|
||||
|
||||
@Transaction
|
||||
open suspend fun setEnabled(source: String, isEnabled: Boolean) {
|
||||
@@ -101,6 +90,17 @@ abstract class MangaSourcesDao {
|
||||
@RawQuery
|
||||
protected abstract suspend fun findAllImpl(query: SupportSQLiteQuery): List<MangaSourceEntity>
|
||||
|
||||
private fun getQuery(enabledOnly: Boolean, order: SourcesSortOrder) = SimpleSQLiteQuery(
|
||||
buildString {
|
||||
append("SELECT * FROM sources ")
|
||||
if (enabledOnly) {
|
||||
append("WHERE enabled = 1 ")
|
||||
}
|
||||
append("ORDER BY pinned DESC, ")
|
||||
append(getOrderBy(order))
|
||||
},
|
||||
)
|
||||
|
||||
private fun getOrderBy(order: SourcesSortOrder) = when (order) {
|
||||
SourcesSortOrder.ALPHABETIC -> "source ASC"
|
||||
SourcesSortOrder.POPULARITY -> "(SELECT COUNT(*) FROM manga WHERE source = sources.source) DESC"
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.koitharu.kotatsu.core.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import org.koitharu.kotatsu.core.db.TABLE_CHAPTERS
|
||||
|
||||
@Entity(
|
||||
tableName = TABLE_CHAPTERS,
|
||||
primaryKeys = ["manga_id", "chapter_id"],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = MangaEntity::class,
|
||||
parentColumns = ["manga_id"],
|
||||
childColumns = ["manga_id"],
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
),
|
||||
],
|
||||
)
|
||||
data class ChapterEntity(
|
||||
@ColumnInfo(name = "chapter_id") val chapterId: Long,
|
||||
@ColumnInfo(name = "manga_id") val mangaId: Long,
|
||||
@ColumnInfo(name = "name") val name: String,
|
||||
@ColumnInfo(name = "number") val number: Float,
|
||||
@ColumnInfo(name = "volume") val volume: Int,
|
||||
@ColumnInfo(name = "url") val url: String,
|
||||
@ColumnInfo(name = "scanlator") val scanlator: String?,
|
||||
@ColumnInfo(name = "upload_date") val uploadDate: Long,
|
||||
@ColumnInfo(name = "branch") val branch: String?,
|
||||
@ColumnInfo(name = "source") val source: String,
|
||||
@ColumnInfo(name = "index") val index: Int,
|
||||
)
|
||||
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.core.db.entity
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.util.ext.longHashCode
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
@@ -21,7 +22,7 @@ fun Collection<TagEntity>.toMangaTags() = mapToSet(TagEntity::toMangaTag)
|
||||
|
||||
fun Collection<TagEntity>.toMangaTagsList() = map(TagEntity::toMangaTag)
|
||||
|
||||
fun MangaEntity.toManga(tags: Set<MangaTag>) = Manga(
|
||||
fun MangaEntity.toManga(tags: Set<MangaTag>, chapters: List<ChapterEntity>?) = Manga(
|
||||
id = this.id,
|
||||
title = this.title,
|
||||
altTitle = this.altTitle,
|
||||
@@ -35,12 +36,27 @@ fun MangaEntity.toManga(tags: Set<MangaTag>) = Manga(
|
||||
author = this.author,
|
||||
source = MangaSource(this.source),
|
||||
tags = tags,
|
||||
chapters = chapters?.toMangaChapters(),
|
||||
)
|
||||
|
||||
fun MangaWithTags.toManga() = manga.toManga(tags.toMangaTags())
|
||||
fun MangaWithTags.toManga(chapters: List<ChapterEntity>? = null) = manga.toManga(tags.toMangaTags(), chapters)
|
||||
|
||||
fun Collection<MangaWithTags>.toMangaList() = map { it.toManga() }
|
||||
|
||||
fun ChapterEntity.toMangaChapter() = MangaChapter(
|
||||
id = chapterId,
|
||||
name = name,
|
||||
number = number,
|
||||
volume = volume,
|
||||
url = url,
|
||||
scanlator = scanlator,
|
||||
uploadDate = uploadDate,
|
||||
branch = branch,
|
||||
source = MangaSource(source),
|
||||
)
|
||||
|
||||
fun Collection<ChapterEntity>.toMangaChapters() = map { it.toMangaChapter() }
|
||||
|
||||
// Model to entity
|
||||
|
||||
fun Manga.toEntity() = MangaEntity(
|
||||
@@ -67,6 +83,22 @@ fun MangaTag.toEntity() = TagEntity(
|
||||
|
||||
fun Collection<MangaTag>.toEntities() = map(MangaTag::toEntity)
|
||||
|
||||
fun Iterable<IndexedValue<MangaChapter>>.toEntities(mangaId: Long) = map { (index, chapter) ->
|
||||
ChapterEntity(
|
||||
chapterId = chapter.id,
|
||||
mangaId = mangaId,
|
||||
name = chapter.name,
|
||||
number = chapter.number,
|
||||
volume = chapter.volume,
|
||||
url = chapter.url,
|
||||
scanlator = chapter.scanlator,
|
||||
uploadDate = chapter.uploadDate,
|
||||
branch = chapter.branch,
|
||||
source = chapter.source.name,
|
||||
index = index,
|
||||
)
|
||||
}
|
||||
|
||||
// Other
|
||||
|
||||
fun SortOrder(name: String, fallback: SortOrder): SortOrder = runCatching {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.koitharu.kotatsu.core.db.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
class Migration23To24 : Migration(23, 24) {
|
||||
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `chapters` (`chapter_id` INTEGER NOT NULL, `manga_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `number` REAL NOT NULL, `volume` INTEGER NOT NULL, `url` TEXT NOT NULL, `scanlator` TEXT, `upload_date` INTEGER NOT NULL, `branch` TEXT, `source` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`manga_id`, `chapter_id`), FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
package org.koitharu.kotatsu.core.exceptions
|
||||
|
||||
class CaughtException(cause: Throwable) : RuntimeException("${cause.javaClass.simpleName}(${cause.message})", cause)
|
||||
class CaughtException(
|
||||
override val cause: Throwable
|
||||
) : RuntimeException("${cause.javaClass.simpleName}(${cause.message})", cause)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.koitharu.kotatsu.core.exceptions
|
||||
|
||||
import okio.IOException
|
||||
|
||||
class WrapperIOException(override val cause: Exception) : IOException(cause)
|
||||
@@ -6,7 +6,6 @@ import androidx.core.util.Consumer
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.core.util.ext.isSerializable
|
||||
import org.koitharu.kotatsu.parsers.exception.ParseException
|
||||
@@ -32,10 +31,10 @@ class DialogErrorObserver(
|
||||
if (canResolve(value)) {
|
||||
dialogBuilder.setPositiveButton(ExceptionResolver.getResolveStringId(value), listener)
|
||||
} else if (value is ParseException) {
|
||||
val fm = fragmentManager
|
||||
if (fm != null && value.isSerializable()) {
|
||||
val router = router()
|
||||
if (router != null && value.isSerializable()) {
|
||||
dialogBuilder.setPositiveButton(R.string.details) { _, _ ->
|
||||
ErrorDetailsDialog.show(fm, value, value.url)
|
||||
router.showErrorDialog(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.util.Consumer
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
@@ -11,6 +12,7 @@ import androidx.lifecycle.coroutineScope
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koitharu.kotatsu.core.nav.router
|
||||
import org.koitharu.kotatsu.core.util.ext.findActivity
|
||||
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
|
||||
|
||||
@@ -33,6 +35,8 @@ abstract class ErrorObserver(
|
||||
return resolver != null && ExceptionResolver.canResolve(error)
|
||||
}
|
||||
|
||||
protected fun router() = fragment?.router ?: (activity as? FragmentActivity)?.router
|
||||
|
||||
private fun isAlive(): Boolean {
|
||||
return when {
|
||||
fragment != null -> fragment.view != null
|
||||
@@ -44,7 +48,7 @@ abstract class ErrorObserver(
|
||||
protected fun resolve(error: Throwable) {
|
||||
if (isAlive()) {
|
||||
lifecycleScope.launch {
|
||||
val isResolved = resolver?.resolve(error) ?: false
|
||||
val isResolved = resolver?.resolve(error) == true
|
||||
if (isActive) {
|
||||
onResolved?.accept(isResolved)
|
||||
}
|
||||
|
||||
@@ -5,19 +5,20 @@ import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultCaller
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.collection.MutableScatterMap
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.alternatives.ui.AlternativesActivity
|
||||
import org.koitharu.kotatsu.browser.BrowserActivity
|
||||
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareActivity
|
||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
||||
import org.koitharu.kotatsu.core.exceptions.ProxyConfigException
|
||||
import org.koitharu.kotatsu.core.exceptions.UnsupportedSourceException
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.nav.router
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog
|
||||
import org.koitharu.kotatsu.core.ui.dialog.buildAlertDialog
|
||||
import org.koitharu.kotatsu.core.util.ext.restartApplication
|
||||
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
|
||||
@@ -26,7 +27,6 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.ScrobblerAuthRequiredException
|
||||
import org.koitharu.kotatsu.scrobbling.common.ui.ScrobblerAuthHelper
|
||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
|
||||
import java.security.cert.CertPathValidatorException
|
||||
import javax.inject.Provider
|
||||
@@ -49,8 +49,8 @@ class ExceptionResolver @AssistedInject constructor(
|
||||
handleActivityResult(CloudFlareActivity.TAG, it)
|
||||
}
|
||||
|
||||
fun showDetails(e: Throwable, url: String?) {
|
||||
ErrorDetailsDialog.show(host.getChildFragmentManager(), e, url)
|
||||
fun showErrorDetails(e: Throwable, url: String? = null) {
|
||||
host.router()?.showErrorDialog(e, url)
|
||||
}
|
||||
|
||||
suspend fun resolve(e: Throwable): Boolean = when (e) {
|
||||
@@ -63,9 +63,7 @@ class ExceptionResolver @AssistedInject constructor(
|
||||
}
|
||||
|
||||
is ProxyConfigException -> {
|
||||
host.withContext {
|
||||
startActivity(SettingsActivity.newProxySettingsIntent(this))
|
||||
}
|
||||
host.router()?.openProxySettings()
|
||||
false
|
||||
}
|
||||
|
||||
@@ -85,9 +83,7 @@ class ExceptionResolver @AssistedInject constructor(
|
||||
true
|
||||
} else {
|
||||
host.withContext {
|
||||
authHelper.startAuth(this, e.scrobbler).onFailure {
|
||||
showDetails(it, null)
|
||||
}
|
||||
authHelper.startAuth(this, e.scrobbler).onFailure(::showErrorDetails)
|
||||
}
|
||||
false
|
||||
}
|
||||
@@ -106,12 +102,12 @@ class ExceptionResolver @AssistedInject constructor(
|
||||
sourceAuthContract.launch(source)
|
||||
}
|
||||
|
||||
private fun openInBrowser(url: String) = host.withContext {
|
||||
startActivity(BrowserActivity.newIntent(this, url, null, null))
|
||||
private fun openInBrowser(url: String) {
|
||||
host.router()?.openBrowser(url, null, null)
|
||||
}
|
||||
|
||||
private fun openAlternatives(manga: Manga) = host.withContext {
|
||||
startActivity(AlternativesActivity.newIntent(this, manga))
|
||||
private fun openAlternatives(manga: Manga) {
|
||||
host.router()?.openAlternatives(manga)
|
||||
}
|
||||
|
||||
private fun handleActivityResult(tag: String, result: Boolean) {
|
||||
@@ -140,6 +136,12 @@ class ExceptionResolver @AssistedInject constructor(
|
||||
getContext()?.apply(block)
|
||||
}
|
||||
|
||||
private fun Host.router(): AppRouter? = when (this) {
|
||||
is FragmentActivity -> router
|
||||
is Fragment -> router
|
||||
else -> null
|
||||
}
|
||||
|
||||
interface Host : ActivityResultCaller {
|
||||
|
||||
fun getChildFragmentManager(): FragmentManager
|
||||
|
||||
@@ -5,7 +5,6 @@ import androidx.core.util.Consumer
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.core.util.ext.isSerializable
|
||||
import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
|
||||
@@ -33,10 +32,10 @@ class SnackbarErrorObserver(
|
||||
resolve(value)
|
||||
}
|
||||
} else if (value is ParseException) {
|
||||
val fm = fragmentManager
|
||||
if (fm != null && value.isSerializable()) {
|
||||
val router = router()
|
||||
if (router != null && value.isSerializable()) {
|
||||
snackbar.setAction(R.string.details) {
|
||||
ErrorDetailsDialog.show(fm, value, value.url)
|
||||
router.showErrorDialog(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,26 +4,26 @@ import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.ImageDecoder
|
||||
import android.os.Build
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.davemorrissey.labs.subscaleview.decoder.ImageDecodeException
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import org.aomedia.avif.android.AvifDecoder
|
||||
import org.aomedia.avif.android.AvifDecoder.Info
|
||||
import org.jetbrains.annotations.Blocking
|
||||
import org.koitharu.kotatsu.core.util.MimeTypes
|
||||
import org.koitharu.kotatsu.core.util.ext.MimeType
|
||||
import org.koitharu.kotatsu.core.util.ext.toByteBuffer
|
||||
import org.koitharu.kotatsu.core.util.ext.toMimeTypeOrNull
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.file.Files
|
||||
|
||||
object BitmapDecoderCompat {
|
||||
|
||||
private const val FORMAT_AVIF = "avif"
|
||||
|
||||
@Blocking
|
||||
fun decode(file: File): Bitmap = when (val format = getMimeType(file)?.subtype) {
|
||||
fun decode(file: File): Bitmap = when (val format = probeMimeType(file)?.subtype) {
|
||||
FORMAT_AVIF -> file.inputStream().use { decodeAvif(it.toByteBuffer()) }
|
||||
else -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
ImageDecoder.decodeBitmap(ImageDecoder.createSource(file))
|
||||
@@ -33,7 +33,7 @@ object BitmapDecoderCompat {
|
||||
}
|
||||
|
||||
@Blocking
|
||||
fun decode(stream: InputStream, type: MediaType?, isMutable: Boolean = false): Bitmap {
|
||||
fun decode(stream: InputStream, type: MimeType?, isMutable: Boolean = false): Bitmap {
|
||||
val format = type?.subtype
|
||||
if (format == FORMAT_AVIF) {
|
||||
return decodeAvif(stream.toByteBuffer())
|
||||
@@ -51,12 +51,20 @@ object BitmapDecoderCompat {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMimeType(file: File): MediaType? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Files.probeContentType(file.toPath())?.toMediaTypeOrNull()
|
||||
} else {
|
||||
MimeTypeMap.getSingleton().getMimeTypeFromExtension(file.extension)?.toMediaTypeOrNull()
|
||||
@Blocking
|
||||
fun probeMimeType(file: File): MimeType? {
|
||||
return MimeTypes.probeMimeType(file) ?: detectBitmapType(file)
|
||||
}
|
||||
|
||||
@Blocking
|
||||
private fun detectBitmapType(file: File): MimeType? = runCatchingCancellable {
|
||||
val options = BitmapFactory.Options().apply {
|
||||
inJustDecodeBounds = true
|
||||
}
|
||||
BitmapFactory.decodeFile(file.path, options)?.recycle()
|
||||
return options.outMimeType?.toMimeTypeOrNull()
|
||||
}.getOrNull()
|
||||
|
||||
private fun checkBitmapNotNull(bitmap: Bitmap?, format: String?): Bitmap =
|
||||
bitmap ?: throw ImageDecodeException(null, format)
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.koitharu.kotatsu.core.image
|
||||
|
||||
import android.net.Uri
|
||||
import android.webkit.MimeTypeMap
|
||||
import coil3.ImageLoader
|
||||
import coil3.decode.DataSource
|
||||
import coil3.decode.ImageSource
|
||||
@@ -12,6 +11,7 @@ import coil3.toAndroidUri
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import okio.Path.Companion.toPath
|
||||
import okio.openZip
|
||||
import org.koitharu.kotatsu.core.util.MimeTypes
|
||||
import org.koitharu.kotatsu.core.util.ext.isZipUri
|
||||
import coil3.Uri as CoilUri
|
||||
|
||||
@@ -25,7 +25,7 @@ class CbzFetcher(
|
||||
val entryName = requireNotNull(uri.fragment)
|
||||
SourceFetchResult(
|
||||
source = ImageSource(entryName.toPath(), options.fileSystem.openZip(filePath)),
|
||||
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(entryName.substringAfterLast('.', "")),
|
||||
mimeType = MimeTypes.getMimeTypeFromExtension(entryName)?.toString(),
|
||||
dataSource = DataSource.DISK,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,11 +2,16 @@ package org.koitharu.kotatsu.core.model
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.ImageSpan
|
||||
import android.text.style.RelativeSizeSpan
|
||||
import android.text.style.SuperscriptSpan
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.text.inSpans
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.parser.external.ExternalMangaSource
|
||||
@@ -100,3 +105,16 @@ fun SpannableStringBuilder.appendNsfwLabel(context: Context) = inSpans(
|
||||
) {
|
||||
append(context.getString(R.string.nsfw))
|
||||
}
|
||||
|
||||
fun SpannableStringBuilder.appendIcon(textView: TextView, @DrawableRes resId: Int): SpannableStringBuilder {
|
||||
val icon = ContextCompat.getDrawable(textView.context, resId) ?: return this
|
||||
icon.setTintList(textView.textColors)
|
||||
val size = textView.lineHeight
|
||||
icon.setBounds(0, 0, size, size)
|
||||
val alignment = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ImageSpan.ALIGN_CENTER
|
||||
} else {
|
||||
ImageSpan.ALIGN_BOTTOM
|
||||
}
|
||||
return inSpans(ImageSpan(icon, alignment)) { append(' ') }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.koitharu.kotatsu.core.model
|
||||
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.list.domain.ListFilterOption
|
||||
|
||||
fun ListFilterOption.toChipModel(isChecked: Boolean) = ChipsView.ChipModel(
|
||||
title = titleText,
|
||||
titleResId = titleResId,
|
||||
icon = iconResId,
|
||||
iconData = getIconData(),
|
||||
isChecked = isChecked,
|
||||
data = this,
|
||||
)
|
||||
@@ -13,6 +13,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
||||
@Parcelize
|
||||
data class ParcelableManga(
|
||||
val manga: Manga,
|
||||
private val withDescription: Boolean = true,
|
||||
) : Parcelable {
|
||||
|
||||
companion object : Parceler<ParcelableManga> {
|
||||
@@ -27,7 +28,7 @@ data class ParcelableManga(
|
||||
ParcelCompat.writeBoolean(parcel, isNsfw)
|
||||
parcel.writeString(coverUrl)
|
||||
parcel.writeString(largeCoverUrl)
|
||||
parcel.writeString(description)
|
||||
parcel.writeString(description.takeIf { withDescription })
|
||||
parcel.writeParcelable(ParcelableMangaTags(tags), flags)
|
||||
parcel.writeSerializable(state)
|
||||
parcel.writeString(author)
|
||||
@@ -52,6 +53,7 @@ data class ParcelableManga(
|
||||
chapters = null,
|
||||
source = MangaSource(parcel.readString()),
|
||||
),
|
||||
withDescription = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
602
app/src/main/kotlin/org/koitharu/kotatsu/core/nav/AppRouter.kt
Normal file
602
app/src/main/kotlin/org/koitharu/kotatsu/core/nav/AppRouter.kt
Normal file
@@ -0,0 +1,602 @@
|
||||
package org.koitharu.kotatsu.core.nav
|
||||
|
||||
import android.accounts.Account
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.view.View
|
||||
import androidx.annotation.CheckResult
|
||||
import androidx.core.net.toUri
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.findFragment
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.alternatives.ui.AlternativesActivity
|
||||
import org.koitharu.kotatsu.bookmarks.ui.AllBookmarksActivity
|
||||
import org.koitharu.kotatsu.browser.BrowserActivity
|
||||
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareActivity
|
||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
import org.koitharu.kotatsu.core.model.MangaSourceInfo
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaListFilter
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaPage
|
||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||
import org.koitharu.kotatsu.core.parser.external.ExternalMangaSource
|
||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||
import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog
|
||||
import org.koitharu.kotatsu.core.util.ext.findActivity
|
||||
import org.koitharu.kotatsu.core.util.ext.mapToArray
|
||||
import org.koitharu.kotatsu.core.util.ext.toUriOrNull
|
||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesSheet
|
||||
import org.koitharu.kotatsu.details.ui.related.RelatedMangaActivity
|
||||
import org.koitharu.kotatsu.details.ui.scrobbling.ScrobblingInfoSheet
|
||||
import org.koitharu.kotatsu.download.ui.dialog.DownloadDialogFragment
|
||||
import org.koitharu.kotatsu.download.ui.list.DownloadsActivity
|
||||
import org.koitharu.kotatsu.favourites.ui.FavouritesActivity
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteDialog
|
||||
import org.koitharu.kotatsu.filter.ui.FilterCoordinator
|
||||
import org.koitharu.kotatsu.filter.ui.sheet.FilterSheetFragment
|
||||
import org.koitharu.kotatsu.filter.ui.tags.TagsCatalogSheet
|
||||
import org.koitharu.kotatsu.history.ui.HistoryActivity
|
||||
import org.koitharu.kotatsu.image.ui.ImageActivity
|
||||
import org.koitharu.kotatsu.list.ui.config.ListConfigBottomSheet
|
||||
import org.koitharu.kotatsu.list.ui.config.ListConfigSection
|
||||
import org.koitharu.kotatsu.local.ui.ImportDialogFragment
|
||||
import org.koitharu.kotatsu.local.ui.info.LocalInfoDialog
|
||||
import org.koitharu.kotatsu.main.ui.welcome.WelcomeSheet
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.util.isNullOrEmpty
|
||||
import org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity
|
||||
import org.koitharu.kotatsu.reader.ui.config.ReaderConfigSheet
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
|
||||
import org.koitharu.kotatsu.scrobbling.common.ui.config.ScrobblerConfigActivity
|
||||
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet
|
||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||
import org.koitharu.kotatsu.search.ui.multi.SearchActivity
|
||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
import org.koitharu.kotatsu.settings.about.AppUpdateActivity
|
||||
import org.koitharu.kotatsu.settings.backup.BackupDialogFragment
|
||||
import org.koitharu.kotatsu.settings.backup.RestoreDialogFragment
|
||||
import org.koitharu.kotatsu.settings.reader.ReaderTapGridConfigActivity
|
||||
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
|
||||
import org.koitharu.kotatsu.settings.sources.catalog.SourcesCatalogActivity
|
||||
import org.koitharu.kotatsu.settings.storage.MangaDirectorySelectDialog
|
||||
import org.koitharu.kotatsu.settings.storage.directories.MangaDirectoriesActivity
|
||||
import org.koitharu.kotatsu.settings.tracker.categories.TrackerCategoriesConfigSheet
|
||||
import org.koitharu.kotatsu.stats.ui.StatsActivity
|
||||
import org.koitharu.kotatsu.stats.ui.sheet.MangaStatsSheet
|
||||
import org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity
|
||||
import org.koitharu.kotatsu.tracker.ui.updates.UpdatesActivity
|
||||
|
||||
class AppRouter private constructor(
|
||||
private val activity: FragmentActivity?,
|
||||
private val fragment: Fragment?,
|
||||
) {
|
||||
|
||||
constructor(activity: FragmentActivity) : this(activity, null)
|
||||
|
||||
constructor(fragment: Fragment) : this(null, fragment)
|
||||
|
||||
/** Activities **/
|
||||
|
||||
fun openList(source: MangaSource, filter: MangaListFilter?) {
|
||||
startActivity(listIntent(contextOrNull() ?: return, source, filter))
|
||||
}
|
||||
|
||||
fun openList(tag: MangaTag) = openList(tag.source, MangaListFilter(tags = setOf(tag)))
|
||||
|
||||
fun openSearch(query: String) {
|
||||
startActivity(
|
||||
Intent(contextOrNull() ?: return, SearchActivity::class.java)
|
||||
.putExtra(KEY_QUERY, query),
|
||||
)
|
||||
}
|
||||
|
||||
fun openSearch(source: MangaSource, query: String) = openList(source, MangaListFilter(query = query))
|
||||
|
||||
fun openDetails(manga: Manga) {
|
||||
startActivity(detailsIntent(contextOrNull() ?: return, manga))
|
||||
}
|
||||
|
||||
fun openDetails(mangaId: Long) {
|
||||
startActivity(detailsIntent(contextOrNull() ?: return, mangaId))
|
||||
}
|
||||
|
||||
fun openReader(manga: Manga, anchor: View? = null) {
|
||||
openReader(
|
||||
ReaderIntent.Builder(contextOrNull() ?: return)
|
||||
.manga(manga)
|
||||
.build(),
|
||||
anchor,
|
||||
)
|
||||
}
|
||||
|
||||
fun openReader(intent: ReaderIntent, anchor: View? = null) {
|
||||
startActivity(intent.intent, anchor?.let { view -> scaleUpActivityOptionsOf(view) })
|
||||
}
|
||||
|
||||
fun openAlternatives(manga: Manga) {
|
||||
startActivity(
|
||||
Intent(contextOrNull() ?: return, AlternativesActivity::class.java)
|
||||
.putExtra(KEY_MANGA, ParcelableManga(manga)),
|
||||
)
|
||||
}
|
||||
|
||||
fun openRelated(manga: Manga) {
|
||||
startActivity(
|
||||
Intent(contextOrNull(), RelatedMangaActivity::class.java)
|
||||
.putExtra(KEY_MANGA, ParcelableManga(manga)),
|
||||
)
|
||||
}
|
||||
|
||||
fun openImage(url: String, source: MangaSource?, anchor: View? = null) {
|
||||
startActivity(
|
||||
Intent(contextOrNull(), ImageActivity::class.java)
|
||||
.setData(url.toUri())
|
||||
.putExtra(KEY_SOURCE, source?.name),
|
||||
anchor?.let { scaleUpActivityOptionsOf(it) },
|
||||
)
|
||||
}
|
||||
|
||||
fun openBookmarks() = startActivity(AllBookmarksActivity::class.java)
|
||||
|
||||
fun openAppUpdate() = startActivity(AppUpdateActivity::class.java)
|
||||
|
||||
fun openSuggestions() {
|
||||
startActivity(suggestionsIntent(contextOrNull() ?: return))
|
||||
}
|
||||
|
||||
fun openSourcesCatalog() = startActivity(SourcesCatalogActivity::class.java)
|
||||
|
||||
fun openDownloads() = startActivity(DownloadsActivity::class.java)
|
||||
|
||||
fun openDirectoriesSettings() = startActivity(MangaDirectoriesActivity::class.java)
|
||||
|
||||
fun openBrowser(url: String, source: MangaSource?, title: String?) {
|
||||
startActivity(
|
||||
Intent(contextOrNull() ?: return, BrowserActivity::class.java)
|
||||
.setData(url.toUri())
|
||||
.putExtra(KEY_TITLE, title)
|
||||
.putExtra(KEY_SOURCE, source?.name),
|
||||
)
|
||||
}
|
||||
|
||||
fun openColorFilterConfig(manga: Manga, page: MangaPage) {
|
||||
startActivity(
|
||||
Intent(contextOrNull(), ColorFilterConfigActivity::class.java)
|
||||
.putExtra(KEY_MANGA, ParcelableManga(manga))
|
||||
.putExtra(KEY_PAGES, ParcelableMangaPage(page)),
|
||||
)
|
||||
}
|
||||
|
||||
fun openHistory() = startActivity(HistoryActivity::class.java)
|
||||
|
||||
fun openFavorites() = startActivity(FavouritesActivity::class.java)
|
||||
|
||||
fun openFavorites(category: FavouriteCategory) {
|
||||
startActivity(
|
||||
Intent(contextOrNull() ?: return, FavouritesActivity::class.java)
|
||||
.putExtra(KEY_ID, category.id)
|
||||
.putExtra(KEY_TITLE, category.title),
|
||||
)
|
||||
}
|
||||
|
||||
fun openFavoriteCategories() = startActivity(FavouriteCategoriesActivity::class.java)
|
||||
|
||||
fun openFavoriteCategoryEdit(categoryId: Long) {
|
||||
startActivity(
|
||||
Intent(contextOrNull() ?: return, FavouritesCategoryEditActivity::class.java)
|
||||
.putExtra(KEY_ID, categoryId),
|
||||
)
|
||||
}
|
||||
|
||||
fun openFavoriteCategoryCreate() = openFavoriteCategoryEdit(FavouritesCategoryEditActivity.NO_ID)
|
||||
|
||||
fun openMangaUpdates() {
|
||||
startActivity(mangaUpdatesIntent(contextOrNull() ?: return))
|
||||
}
|
||||
|
||||
fun openSettings() = startActivity(SettingsActivity::class.java)
|
||||
|
||||
fun openReaderSettings() {
|
||||
startActivity(readerSettingsIntent(contextOrNull() ?: return))
|
||||
}
|
||||
|
||||
fun openProxySettings() {
|
||||
startActivity(proxySettingsIntent(contextOrNull() ?: return))
|
||||
}
|
||||
|
||||
fun openDownloadsSetting() {
|
||||
startActivity(downloadsSettingsIntent(contextOrNull() ?: return))
|
||||
}
|
||||
|
||||
fun openSourceSettings(source: MangaSource) {
|
||||
startActivity(sourceSettingsIntent(contextOrNull() ?: return, source))
|
||||
}
|
||||
|
||||
fun openSuggestionsSettings() {
|
||||
startActivity(suggestionsSettingsIntent(contextOrNull() ?: return))
|
||||
}
|
||||
|
||||
fun openSourcesSettings() {
|
||||
startActivity(sourcesSettingsIntent(contextOrNull() ?: return))
|
||||
}
|
||||
|
||||
fun openReaderTapGridSettings() = startActivity(ReaderTapGridConfigActivity::class.java)
|
||||
|
||||
fun openScrobblerSettings(scrobbler: ScrobblerService) {
|
||||
startActivity(
|
||||
Intent(contextOrNull() ?: return, ScrobblerConfigActivity::class.java)
|
||||
.putExtra(KEY_ID, scrobbler.id),
|
||||
)
|
||||
}
|
||||
|
||||
fun openSourceAuth(source: MangaSource) {
|
||||
startActivity(sourceAuthIntent(contextOrNull() ?: return, source))
|
||||
}
|
||||
|
||||
fun openManageSources() {
|
||||
startActivity(
|
||||
manageSourcesIntent(contextOrNull() ?: return),
|
||||
)
|
||||
}
|
||||
|
||||
fun openStatistic() = startActivity(StatsActivity::class.java)
|
||||
|
||||
@CheckResult
|
||||
fun openExternalBrowser(url: String, chooserTitle: CharSequence? = null): Boolean {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.data = url.toUriOrNull() ?: return false
|
||||
return startActivitySafe(
|
||||
if (!chooserTitle.isNullOrEmpty()) {
|
||||
Intent.createChooser(intent, chooserTitle)
|
||||
} else {
|
||||
intent
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@CheckResult
|
||||
fun openSystemSyncSettings(account: Account): Boolean {
|
||||
val args = Bundle(1)
|
||||
args.putParcelable(ACCOUNT_KEY, account)
|
||||
val intent = Intent(ACTION_ACCOUNT_SYNC_SETTINGS)
|
||||
intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, args)
|
||||
return startActivitySafe(intent)
|
||||
}
|
||||
|
||||
/** Dialogs **/
|
||||
|
||||
fun showDownloadDialog(manga: Manga, snackbarHost: View?) = showDownloadDialog(setOf(manga), snackbarHost)
|
||||
|
||||
fun showDownloadDialog(manga: Collection<Manga>, snackbarHost: View?) {
|
||||
if (manga.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val fm = getFragmentManager() ?: return
|
||||
if (snackbarHost != null) {
|
||||
getLifecycleOwner()?.let { lifecycleOwner ->
|
||||
DownloadDialogFragment.registerCallback(fm, lifecycleOwner, snackbarHost)
|
||||
}
|
||||
} else {
|
||||
DownloadDialogFragment.unregisterCallback(fm)
|
||||
}
|
||||
DownloadDialogFragment().withArgs(1) {
|
||||
putParcelableArray(KEY_MANGA, manga.mapToArray { ParcelableManga(it, withDescription = false) })
|
||||
}.showDistinct()
|
||||
}
|
||||
|
||||
fun showLocalInfoDialog(manga: Manga) {
|
||||
LocalInfoDialog().withArgs(1) {
|
||||
putParcelable(KEY_MANGA, ParcelableManga(manga))
|
||||
}.showDistinct()
|
||||
}
|
||||
|
||||
fun showDirectorySelectDialog() {
|
||||
MangaDirectorySelectDialog().showDistinct()
|
||||
}
|
||||
|
||||
fun showFavoriteDialog(manga: Manga) = showFavoriteDialog(setOf(manga))
|
||||
|
||||
fun showFavoriteDialog(manga: Collection<Manga>) {
|
||||
if (manga.isEmpty()) {
|
||||
return
|
||||
}
|
||||
FavoriteDialog().withArgs(1) {
|
||||
putParcelableArrayList(
|
||||
KEY_MANGA_LIST,
|
||||
manga.mapTo(ArrayList(manga.size)) { ParcelableManga(it, withDescription = false) },
|
||||
)
|
||||
}.showDistinct()
|
||||
}
|
||||
|
||||
fun showErrorDialog(error: Throwable, url: String? = null) {
|
||||
ErrorDetailsDialog().withArgs(2) {
|
||||
putSerializable(KEY_ERROR, error)
|
||||
putString(KEY_URL, url)
|
||||
}.show()
|
||||
}
|
||||
|
||||
fun showBackupRestoreDialog(fileUri: Uri) {
|
||||
RestoreDialogFragment().withArgs(1) {
|
||||
putString(KEY_FILE, fileUri.toString())
|
||||
}.show()
|
||||
}
|
||||
|
||||
fun showBackupCreateDialog() {
|
||||
BackupDialogFragment().show()
|
||||
}
|
||||
|
||||
fun showImportDialog() {
|
||||
ImportDialogFragment().showDistinct()
|
||||
}
|
||||
|
||||
fun showFilterSheet(): Boolean = if (isFilterSupported()) {
|
||||
FilterSheetFragment().showDistinct()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
fun showTagsCatalogSheet(excludeMode: Boolean) {
|
||||
if (!isFilterSupported()) {
|
||||
return
|
||||
}
|
||||
TagsCatalogSheet().withArgs(1) {
|
||||
putBoolean(KEY_EXCLUDE, excludeMode)
|
||||
}.showDistinct()
|
||||
}
|
||||
|
||||
fun showListConfigSheet(section: ListConfigSection) {
|
||||
ListConfigBottomSheet().withArgs(1) {
|
||||
putParcelable(KEY_LIST_SECTION, section)
|
||||
}.showDistinct()
|
||||
}
|
||||
|
||||
fun showStatisticSheet(manga: Manga) {
|
||||
MangaStatsSheet().withArgs(1) {
|
||||
putParcelable(KEY_MANGA, ParcelableManga(manga))
|
||||
}.showDistinct()
|
||||
}
|
||||
|
||||
fun showReaderConfigSheet(mode: ReaderMode) {
|
||||
ReaderConfigSheet().withArgs(1) {
|
||||
putInt(KEY_READER_MODE, mode.id)
|
||||
}.showDistinct()
|
||||
}
|
||||
|
||||
fun showWelcomeSheet() {
|
||||
WelcomeSheet().showDistinct()
|
||||
}
|
||||
|
||||
fun showChapterPagesSheet() {
|
||||
ChaptersPagesSheet().showDistinct()
|
||||
}
|
||||
|
||||
fun showChapterPagesSheet(defaultTab: Int) {
|
||||
ChaptersPagesSheet().withArgs(1) {
|
||||
putInt(KEY_TAB, defaultTab)
|
||||
}.showDistinct()
|
||||
}
|
||||
|
||||
fun showScrobblingSelectorSheet(manga: Manga, scrobblerService: ScrobblerService?) {
|
||||
ScrobblingSelectorSheet().withArgs(2) {
|
||||
putParcelable(KEY_MANGA, ParcelableManga(manga))
|
||||
if (scrobblerService != null) {
|
||||
putInt(KEY_ID, scrobblerService.id)
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
|
||||
fun showScrobblingInfoSheet(index: Int) {
|
||||
ScrobblingInfoSheet().withArgs(1) {
|
||||
putInt(KEY_INDEX, index)
|
||||
}.showDistinct()
|
||||
}
|
||||
|
||||
fun showTrackerCategoriesConfigSheet() {
|
||||
TrackerCategoriesConfigSheet().showDistinct()
|
||||
}
|
||||
|
||||
/** Public utils **/
|
||||
|
||||
fun isFilterSupported(): Boolean = when {
|
||||
fragment != null -> fragment.activity is FilterCoordinator.Owner
|
||||
activity != null -> activity is FilterCoordinator.Owner
|
||||
else -> false
|
||||
}
|
||||
|
||||
fun isChapterPagesSheetShown(): Boolean {
|
||||
val sheet = getFragmentManager()?.findFragmentByTag(fragmentTag<ChaptersPagesSheet>()) as? ChaptersPagesSheet
|
||||
return sheet?.dialog?.isShowing == true
|
||||
}
|
||||
|
||||
fun closeWelcomeSheet(): Boolean {
|
||||
val fm = fragment?.parentFragmentManager ?: activity?.supportFragmentManager ?: return false
|
||||
val sheet = fm.findFragmentByTag(fragmentTag<WelcomeSheet>()) as? WelcomeSheet ?: return false
|
||||
sheet.dismissAllowingStateLoss()
|
||||
return true
|
||||
}
|
||||
|
||||
/** Private utils **/
|
||||
|
||||
private fun startActivity(intent: Intent, options: Bundle? = null) {
|
||||
fragment?.startActivity(intent, options)
|
||||
?: activity?.startActivity(intent, options)
|
||||
}
|
||||
|
||||
private fun startActivitySafe(intent: Intent): Boolean = try {
|
||||
startActivity(intent)
|
||||
true
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
false
|
||||
}
|
||||
|
||||
private fun startActivity(activityClass: Class<out Activity>) {
|
||||
startActivity(Intent(contextOrNull() ?: return, activityClass))
|
||||
}
|
||||
|
||||
private fun getFragmentManager(): FragmentManager? {
|
||||
return fragment?.childFragmentManager ?: activity?.supportFragmentManager
|
||||
}
|
||||
|
||||
private fun contextOrNull(): Context? = activity ?: fragment?.context
|
||||
|
||||
private fun getLifecycleOwner(): LifecycleOwner? = activity ?: fragment?.viewLifecycleOwner
|
||||
|
||||
private fun DialogFragment.showDistinct(): Boolean {
|
||||
val fm = this@AppRouter.getFragmentManager() ?: return false
|
||||
val tag = javaClass.fragmentTag()
|
||||
val existing = fm.findFragmentByTag(tag) as? DialogFragment?
|
||||
if (existing != null && existing.isVisible && existing.arguments == this.arguments) {
|
||||
return false
|
||||
}
|
||||
show(fm, tag)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun DialogFragment.show() {
|
||||
show(
|
||||
this@AppRouter.getFragmentManager() ?: return,
|
||||
javaClass.fragmentTag(),
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun from(view: View): AppRouter? = runCatching {
|
||||
AppRouter(view.findFragment<Fragment>())
|
||||
}.getOrElse {
|
||||
(view.context.findActivity() as? FragmentActivity)?.let(::AppRouter)
|
||||
}
|
||||
|
||||
fun detailsIntent(context: Context, manga: Manga) = Intent(context, DetailsActivity::class.java)
|
||||
.putExtra(KEY_MANGA, ParcelableManga(manga))
|
||||
|
||||
fun detailsIntent(context: Context, mangaId: Long) = Intent(context, DetailsActivity::class.java)
|
||||
.putExtra(KEY_ID, mangaId)
|
||||
|
||||
fun listIntent(context: Context, source: MangaSource, filter: MangaListFilter?): Intent =
|
||||
Intent(context, MangaListActivity::class.java)
|
||||
.setAction(ACTION_MANGA_EXPLORE)
|
||||
.putExtra(KEY_SOURCE, source.name)
|
||||
.apply {
|
||||
if (!filter.isNullOrEmpty()) {
|
||||
putExtra(KEY_FILTER, ParcelableMangaListFilter(filter))
|
||||
}
|
||||
}
|
||||
|
||||
fun cloudFlareResolveIntent(context: Context, exception: CloudFlareProtectedException): Intent =
|
||||
Intent(context, CloudFlareActivity::class.java).apply {
|
||||
data = exception.url.toUri()
|
||||
putExtra(KEY_SOURCE, exception.source?.name)
|
||||
exception.headers.get(CommonHeaders.USER_AGENT)?.let {
|
||||
putExtra(KEY_USER_AGENT, it)
|
||||
}
|
||||
}
|
||||
|
||||
fun suggestionsIntent(context: Context) = Intent(context, SuggestionsActivity::class.java)
|
||||
|
||||
fun mangaUpdatesIntent(context: Context) = Intent(context, UpdatesActivity::class.java)
|
||||
|
||||
fun readerSettingsIntent(context: Context) =
|
||||
Intent(context, SettingsActivity::class.java)
|
||||
.setAction(ACTION_READER)
|
||||
|
||||
fun suggestionsSettingsIntent(context: Context) =
|
||||
Intent(context, SettingsActivity::class.java)
|
||||
.setAction(ACTION_SUGGESTIONS)
|
||||
|
||||
fun trackerSettingsIntent(context: Context) =
|
||||
Intent(context, SettingsActivity::class.java)
|
||||
.setAction(ACTION_TRACKER)
|
||||
|
||||
fun proxySettingsIntent(context: Context) =
|
||||
Intent(context, SettingsActivity::class.java)
|
||||
.setAction(ACTION_PROXY)
|
||||
|
||||
fun historySettingsIntent(context: Context) =
|
||||
Intent(context, SettingsActivity::class.java)
|
||||
.setAction(ACTION_HISTORY)
|
||||
|
||||
fun sourcesSettingsIntent(context: Context) =
|
||||
Intent(context, SettingsActivity::class.java)
|
||||
.setAction(ACTION_SOURCES)
|
||||
|
||||
fun manageSourcesIntent(context: Context) =
|
||||
Intent(context, SettingsActivity::class.java)
|
||||
.setAction(ACTION_MANAGE_SOURCES)
|
||||
|
||||
fun downloadsSettingsIntent(context: Context) =
|
||||
Intent(context, SettingsActivity::class.java)
|
||||
.setAction(ACTION_MANAGE_DOWNLOADS)
|
||||
|
||||
fun sourceSettingsIntent(context: Context, source: MangaSource): Intent = when (source) {
|
||||
is MangaSourceInfo -> sourceSettingsIntent(context, source.mangaSource)
|
||||
is ExternalMangaSource -> Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
.setData(Uri.fromParts("package", source.packageName, null))
|
||||
|
||||
else -> Intent(context, SettingsActivity::class.java)
|
||||
.setAction(ACTION_SOURCE)
|
||||
.putExtra(KEY_SOURCE, source.name)
|
||||
}
|
||||
|
||||
fun sourceAuthIntent(context: Context, source: MangaSource): Intent {
|
||||
return Intent(context, SourceAuthActivity::class.java)
|
||||
.putExtra(KEY_SOURCE, source.name)
|
||||
}
|
||||
|
||||
const val KEY_EXCLUDE = "exclude"
|
||||
const val KEY_FILTER = "filter"
|
||||
const val KEY_ID = "id"
|
||||
const val KEY_LIST_SECTION = "list_section"
|
||||
const val KEY_MANGA = "manga"
|
||||
const val KEY_MANGA_LIST = "manga_list"
|
||||
const val KEY_PAGES = "pages"
|
||||
const val KEY_QUERY = "query"
|
||||
const val KEY_READER_MODE = "reader_mode"
|
||||
const val KEY_SOURCE = "source"
|
||||
const val KEY_TAB = "tab"
|
||||
const val KEY_TITLE = "title"
|
||||
const val KEY_USER_AGENT = "user_agent"
|
||||
const val KEY_URL = "url"
|
||||
const val KEY_ERROR = "error"
|
||||
const val KEY_FILE = "file"
|
||||
const val KEY_INDEX = "index"
|
||||
const val KEY_DATA = "data"
|
||||
|
||||
const val ACTION_HISTORY = "${BuildConfig.APPLICATION_ID}.action.MANAGE_HISTORY"
|
||||
const val ACTION_MANAGE_DOWNLOADS = "${BuildConfig.APPLICATION_ID}.action.MANAGE_DOWNLOADS"
|
||||
const val ACTION_MANAGE_SOURCES = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCES_LIST"
|
||||
const val ACTION_MANGA_EXPLORE = "${BuildConfig.APPLICATION_ID}.action.EXPLORE_MANGA"
|
||||
const val ACTION_PROXY = "${BuildConfig.APPLICATION_ID}.action.MANAGE_PROXY"
|
||||
const val ACTION_READER = "${BuildConfig.APPLICATION_ID}.action.MANAGE_READER_SETTINGS"
|
||||
const val ACTION_SOURCE = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCE_SETTINGS"
|
||||
const val ACTION_SOURCES = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCES"
|
||||
const val ACTION_SUGGESTIONS = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SUGGESTIONS"
|
||||
const val ACTION_TRACKER = "${BuildConfig.APPLICATION_ID}.action.MANAGE_TRACKER"
|
||||
|
||||
private const val ACCOUNT_KEY = "account"
|
||||
private const val ACTION_ACCOUNT_SYNC_SETTINGS = "android.settings.ACCOUNT_SYNC_SETTINGS"
|
||||
private const val EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args"
|
||||
|
||||
private fun Class<out Fragment>.fragmentTag() = name // TODO
|
||||
|
||||
private inline fun <reified F : Fragment> fragmentTag() = F::class.java.fragmentTag()
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
package org.koitharu.kotatsu.core.parser
|
||||
package org.koitharu.kotatsu.core.nav
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter.Companion.KEY_ID
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter.Companion.KEY_MANGA
|
||||
import org.koitharu.kotatsu.core.util.ext.getParcelableCompat
|
||||
import org.koitharu.kotatsu.core.util.ext.getParcelableExtraCompat
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
@@ -25,7 +26,7 @@ class MangaIntent private constructor(
|
||||
constructor(savedStateHandle: SavedStateHandle) : this(
|
||||
manga = savedStateHandle.get<ParcelableManga>(KEY_MANGA)?.manga,
|
||||
id = savedStateHandle[KEY_ID] ?: ID_NONE,
|
||||
uri = savedStateHandle[BaseActivity.EXTRA_DATA],
|
||||
uri = savedStateHandle[AppRouter.KEY_DATA],
|
||||
)
|
||||
|
||||
constructor(args: Bundle?) : this(
|
||||
@@ -41,9 +42,6 @@ class MangaIntent private constructor(
|
||||
|
||||
const val ID_NONE = 0L
|
||||
|
||||
const val KEY_MANGA = "manga"
|
||||
const val KEY_ID = "id"
|
||||
|
||||
fun of(manga: Manga) = MangaIntent(manga, manga.id, null)
|
||||
}
|
||||
}
|
||||
39
app/src/main/kotlin/org/koitharu/kotatsu/core/nav/NavUtil.kt
Normal file
39
app/src/main/kotlin/org/koitharu/kotatsu/core/nav/NavUtil.kt
Normal file
@@ -0,0 +1,39 @@
|
||||
package org.koitharu.kotatsu.core.nav
|
||||
|
||||
import android.app.ActivityOptions
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
|
||||
|
||||
inline val FragmentActivity.router: AppRouter
|
||||
get() = AppRouter(this)
|
||||
|
||||
inline val Fragment.router: AppRouter
|
||||
get() = AppRouter(this)
|
||||
|
||||
tailrec fun Fragment.dismissParentDialog(): Boolean {
|
||||
return when (val parent = parentFragment) {
|
||||
null -> return false
|
||||
is DialogFragment -> {
|
||||
parent.dismiss()
|
||||
true
|
||||
}
|
||||
|
||||
else -> parent.dismissParentDialog()
|
||||
}
|
||||
}
|
||||
|
||||
fun scaleUpActivityOptionsOf(view: View): Bundle? = if (view.context.isAnimationsEnabled) {
|
||||
ActivityOptions.makeScaleUpAnimation(
|
||||
view,
|
||||
0,
|
||||
0,
|
||||
view.width,
|
||||
view.height,
|
||||
).toBundle()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package org.koitharu.kotatsu.core.nav
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
|
||||
@JvmInline
|
||||
value class ReaderIntent private constructor(
|
||||
val intent: Intent,
|
||||
) {
|
||||
|
||||
class Builder(context: Context) {
|
||||
|
||||
private val intent = Intent(context, ReaderActivity::class.java)
|
||||
.setAction(ACTION_MANGA_READ)
|
||||
|
||||
fun manga(manga: Manga) = apply {
|
||||
intent.putExtra(AppRouter.KEY_MANGA, ParcelableManga(manga))
|
||||
}
|
||||
|
||||
fun mangaId(mangaId: Long) = apply {
|
||||
intent.putExtra(AppRouter.KEY_ID, mangaId)
|
||||
}
|
||||
|
||||
fun incognito(incognito: Boolean) = apply {
|
||||
intent.putExtra(EXTRA_INCOGNITO, incognito)
|
||||
}
|
||||
|
||||
fun branch(branch: String?) = apply {
|
||||
intent.putExtra(EXTRA_BRANCH, branch)
|
||||
}
|
||||
|
||||
fun state(state: ReaderState?) = apply {
|
||||
intent.putExtra(EXTRA_STATE, state)
|
||||
}
|
||||
|
||||
fun bookmark(bookmark: Bookmark) = manga(
|
||||
bookmark.manga,
|
||||
).state(
|
||||
ReaderState(
|
||||
chapterId = bookmark.chapterId,
|
||||
page = bookmark.page,
|
||||
scroll = bookmark.scroll,
|
||||
),
|
||||
)
|
||||
|
||||
fun build() = ReaderIntent(intent)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ACTION_MANGA_READ = "${BuildConfig.APPLICATION_ID}.action.READ_MANGA"
|
||||
const val EXTRA_STATE = "state"
|
||||
const val EXTRA_BRANCH = "branch"
|
||||
const val EXTRA_INCOGNITO = "incognito"
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ class AppProxySelector(
|
||||
if (type == Proxy.Type.DIRECT) {
|
||||
return Proxy.NO_PROXY
|
||||
}
|
||||
if (address.isNullOrEmpty() || port == 0) {
|
||||
if (address.isNullOrEmpty() || port < 0 || port > 0xFFFF) {
|
||||
throw ProxyConfigException()
|
||||
}
|
||||
cachedProxy?.let {
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
package org.koitharu.kotatsu.core.network
|
||||
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.Response
|
||||
import okio.IOException
|
||||
import org.koitharu.kotatsu.core.exceptions.WrapperIOException
|
||||
import org.koitharu.kotatsu.core.network.CommonHeaders.CONTENT_ENCODING
|
||||
|
||||
class GZipInterceptor : Interceptor {
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val newRequest = chain.request().newBuilder()
|
||||
newRequest.addHeader(CONTENT_ENCODING, "gzip")
|
||||
return try {
|
||||
override fun intercept(chain: Interceptor.Chain): Response = try {
|
||||
val request = chain.request()
|
||||
if (request.body is MultipartBody) {
|
||||
chain.proceed(request)
|
||||
} else {
|
||||
val newRequest = request.newBuilder()
|
||||
newRequest.addHeader(CONTENT_ENCODING, "gzip")
|
||||
chain.proceed(newRequest.build())
|
||||
} catch (e: NullPointerException) {
|
||||
throw IOException(e)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
throw WrapperIOException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.db.TABLE_HISTORY
|
||||
import org.koitharu.kotatsu.core.model.getTitle
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.nav.ReaderIntent
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
@@ -36,8 +38,6 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -155,9 +155,10 @@ class AppShortcutManager @Inject constructor(
|
||||
.setIcon(icon)
|
||||
.setLongLived(true)
|
||||
.setIntent(
|
||||
ReaderActivity.IntentBuilder(context)
|
||||
ReaderIntent.Builder(context)
|
||||
.mangaId(manga.id)
|
||||
.build(),
|
||||
.build()
|
||||
.intent,
|
||||
)
|
||||
.build()
|
||||
}
|
||||
@@ -181,7 +182,7 @@ class AppShortcutManager @Inject constructor(
|
||||
.setLongLabel(title)
|
||||
.setIcon(icon)
|
||||
.setLongLived(true)
|
||||
.setIntent(MangaListActivity.newIntent(context, source, null))
|
||||
.setIntent(AppRouter.listIntent(context, source, null))
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ import org.koitharu.kotatsu.core.db.entity.toManga
|
||||
import org.koitharu.kotatsu.core.db.entity.toMangaTags
|
||||
import org.koitharu.kotatsu.core.model.LocalMangaSource
|
||||
import org.koitharu.kotatsu.core.model.isLocal
|
||||
import org.koitharu.kotatsu.core.nav.MangaIntent
|
||||
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||
import org.koitharu.kotatsu.core.util.ext.toFileOrNull
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
@@ -27,6 +29,7 @@ import javax.inject.Provider
|
||||
class MangaDataRepository @Inject constructor(
|
||||
private val db: MangaDatabase,
|
||||
private val resolverProvider: Provider<MangaLinkResolver>,
|
||||
private val appShortcutManagerProvider: Provider<AppShortcutManager>,
|
||||
) {
|
||||
|
||||
suspend fun saveReaderMode(manga: Manga, mode: ReaderMode) {
|
||||
@@ -45,8 +48,8 @@ class MangaDataRepository @Inject constructor(
|
||||
entity.copy(
|
||||
cfBrightness = colorFilter?.brightness ?: 0f,
|
||||
cfContrast = colorFilter?.contrast ?: 0f,
|
||||
cfInvert = colorFilter?.isInverted ?: false,
|
||||
cfGrayscale = colorFilter?.isGrayscale ?: false,
|
||||
cfInvert = colorFilter?.isInverted == true,
|
||||
cfGrayscale = colorFilter?.isGrayscale == true,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -70,8 +73,13 @@ class MangaDataRepository @Inject constructor(
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
suspend fun findMangaById(mangaId: Long): Manga? {
|
||||
return db.getMangaDao().find(mangaId)?.toManga()
|
||||
suspend fun findMangaById(mangaId: Long, withChapters: Boolean): Manga? {
|
||||
val chapters = if (withChapters) {
|
||||
db.getChaptersDao().findAll(mangaId).takeUnless { it.isEmpty() }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
return db.getMangaDao().find(mangaId)?.toManga(chapters)
|
||||
}
|
||||
|
||||
suspend fun findMangaByPublicUrl(publicUrl: String): Manga? {
|
||||
@@ -80,7 +88,7 @@ class MangaDataRepository @Inject constructor(
|
||||
|
||||
suspend fun resolveIntent(intent: MangaIntent): Manga? = when {
|
||||
intent.manga != null -> intent.manga
|
||||
intent.mangaId != 0L -> findMangaById(intent.mangaId)
|
||||
intent.mangaId != 0L -> findMangaById(intent.mangaId, true)
|
||||
intent.uri != null -> resolverProvider.get().resolve(intent.uri)
|
||||
else -> null
|
||||
}
|
||||
@@ -97,10 +105,26 @@ class MangaDataRepository @Inject constructor(
|
||||
val tags = manga.tags.toEntities()
|
||||
db.getTagsDao().upsert(tags)
|
||||
db.getMangaDao().upsert(manga.toEntity(), tags)
|
||||
if (!manga.isLocal) {
|
||||
manga.chapters?.let { chapters ->
|
||||
db.getChaptersDao().replaceAll(manga.id, chapters.withIndex().toEntities(manga.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateChapters(manga: Manga) {
|
||||
val chapters = manga.chapters
|
||||
if (!chapters.isNullOrEmpty() && manga.id in db.getMangaDao()) {
|
||||
db.getChaptersDao().replaceAll(manga.id, chapters.withIndex().toEntities(manga.id))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun gcChaptersCache() {
|
||||
db.getChaptersDao().gc()
|
||||
}
|
||||
|
||||
suspend fun findTags(source: MangaSource): Set<MangaTag> {
|
||||
return db.getTagsDao().findTags(source.name).toMangaTags()
|
||||
}
|
||||
@@ -114,6 +138,14 @@ class MangaDataRepository @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun cleanupDatabase() {
|
||||
db.withTransaction {
|
||||
gcChaptersCache()
|
||||
val idsFromShortcuts = appShortcutManagerProvider.get().getMangaShortcuts()
|
||||
db.getMangaDao().cleanup(idsFromShortcuts)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MangaPrefsEntity.getColorFilterOrNull(): ReaderColorFilter? {
|
||||
return if (cfBrightness != 0f || cfContrast != 0f || cfInvert || cfGrayscale) {
|
||||
ReaderColorFilter(cfBrightness, cfContrast, cfInvert, cfGrayscale)
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.koitharu.kotatsu.core.util.ext.configureForParser
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.sanitizeHeaderValue
|
||||
import org.koitharu.kotatsu.core.util.ext.toList
|
||||
import org.koitharu.kotatsu.core.util.ext.toMimeType
|
||||
import org.koitharu.kotatsu.core.util.ext.use
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.bitmap.Bitmap
|
||||
@@ -78,13 +79,14 @@ class MangaLoaderContextImpl @Inject constructor(
|
||||
|
||||
override fun redrawImageResponse(response: Response, redraw: (image: Bitmap) -> Bitmap): Response {
|
||||
return response.map { body ->
|
||||
BitmapDecoderCompat.decode(body.byteStream(), body.contentType(), isMutable = true).use { bitmap ->
|
||||
(redraw(BitmapWrapper.create(bitmap)) as BitmapWrapper).use { result ->
|
||||
Buffer().also {
|
||||
result.compressTo(it.outputStream())
|
||||
}.asResponseBody("image/jpeg".toMediaType())
|
||||
BitmapDecoderCompat.decode(body.byteStream(), body.contentType()?.toMimeType(), isMutable = true)
|
||||
.use { bitmap ->
|
||||
(redraw(BitmapWrapper.create(bitmap)) as BitmapWrapper).use { result ->
|
||||
Buffer().also {
|
||||
result.compressTo(it.outputStream())
|
||||
}.asResponseBody("image/jpeg".toMediaType())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ class ExternalMangaRepository(
|
||||
|
||||
override var defaultSortOrder: SortOrder
|
||||
get() = capabilities?.availableSortOrders?.firstOrNull() ?: SortOrder.ALPHABETICAL
|
||||
set(_) = Unit
|
||||
set(value) = Unit
|
||||
|
||||
override suspend fun getFilterOptions(): MangaListFilterOptions = filterOptions.get()
|
||||
|
||||
|
||||
@@ -299,6 +299,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
get() = prefs.getInt(KEY_SOURCES_VERSION, 0)
|
||||
set(value) = prefs.edit { putInt(KEY_SOURCES_VERSION, value) }
|
||||
|
||||
var isAllSourcesEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_SOURCES_ENABLED_ALL, false)
|
||||
set(value) = prefs.edit { putBoolean(KEY_SOURCES_ENABLED_ALL, value) }
|
||||
|
||||
val isPagesNumbersEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_PAGES_NUMBERS, false)
|
||||
|
||||
@@ -363,9 +367,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
val isReaderBarEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_READER_BAR, true)
|
||||
|
||||
val isReaderSliderEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_READER_SLIDER, true)
|
||||
|
||||
val isReaderKeepScreenOn: Boolean
|
||||
get() = prefs.getBoolean(KEY_READER_SCREEN_ON, true)
|
||||
|
||||
@@ -489,6 +490,12 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
get() = prefs.getString(KEY_BACKUP_PERIODICAL_OUTPUT, null)?.toUriOrNull()
|
||||
set(value) = prefs.edit { putString(KEY_BACKUP_PERIODICAL_OUTPUT, value?.toString()) }
|
||||
|
||||
val isBackupTelegramUploadEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_BACKUP_TG_ENABLED, false)
|
||||
|
||||
val backupTelegramChatId: String?
|
||||
get() = prefs.getString(KEY_BACKUP_TG_CHAT, null)?.nullIfEmpty()
|
||||
|
||||
val isReadingTimeEstimationEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_READING_TIME, true)
|
||||
|
||||
@@ -664,7 +671,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_SYNC = "sync"
|
||||
const val KEY_SYNC_SETTINGS = "sync_settings"
|
||||
const val KEY_READER_BAR = "reader_bar"
|
||||
const val KEY_READER_SLIDER = "reader_slider"
|
||||
const val KEY_READER_BACKGROUND = "reader_background"
|
||||
const val KEY_READER_SCREEN_ON = "reader_screen_on"
|
||||
const val KEY_SHORTCUTS = "dynamic_shortcuts"
|
||||
@@ -715,7 +721,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_FEED_HEADER = "feed_header"
|
||||
const val KEY_SEARCH_SUGGESTION_TYPES = "search_suggest_types"
|
||||
const val KEY_SOURCES_VERSION = "sources_version"
|
||||
const val KEY_SOURCES_ENABLED_ALL = "sources_enabled_all"
|
||||
const val KEY_QUICK_FILTER = "quick_filter"
|
||||
const val KEY_BACKUP_TG_ENABLED = "backup_periodic_tg_enabled"
|
||||
const val KEY_BACKUP_TG_CHAT = "backup_periodic_tg_chat_id"
|
||||
|
||||
// keys for non-persistent preferences
|
||||
const val KEY_APP_VERSION = "app_version"
|
||||
@@ -729,6 +738,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_PROXY_TEST = "proxy_test"
|
||||
const val KEY_OPEN_BROWSER = "open_browser"
|
||||
const val KEY_HANDLE_LINKS = "handle_links"
|
||||
const val KEY_BACKUP_TG_OPEN = "backup_periodic_tg_open"
|
||||
const val KEY_BACKUP_TG_TEST = "backup_periodic_tg_test"
|
||||
const val KEY_CLEAR_MANGA_DATA = "manga_data_clear"
|
||||
|
||||
// old keys are for migration only
|
||||
private const val KEY_IMAGES_PROXY_OLD = "images_proxy"
|
||||
|
||||
@@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.flowOf
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate
|
||||
import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
|
||||
import org.koitharu.kotatsu.core.util.ext.isWebViewUnavailable
|
||||
@@ -159,7 +160,7 @@ abstract class BaseActivity<B : ViewBinding> :
|
||||
override fun isNsfwContent(): Flow<Boolean> = flowOf(false)
|
||||
|
||||
private fun putDataToExtras(intent: Intent?) {
|
||||
intent?.putExtra(EXTRA_DATA, intent.data)
|
||||
intent?.putExtra(AppRouter.KEY_DATA, intent.data)
|
||||
}
|
||||
|
||||
protected fun setContentViewWebViewSafe(viewBindingProducer: () -> B): Boolean {
|
||||
@@ -178,9 +179,4 @@ abstract class BaseActivity<B : ViewBinding> :
|
||||
}
|
||||
|
||||
protected fun hasViewBinding() = ::viewBinding.isInitialized
|
||||
|
||||
companion object {
|
||||
|
||||
const val EXTRA_DATA = "data"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package org.koitharu.kotatsu.core.ui
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.annotation.CallSuper
|
||||
@@ -14,10 +12,8 @@ import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.get
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner
|
||||
@@ -89,14 +85,6 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
|
||||
(activity as? SettingsActivity)?.setSectionTitle(title)
|
||||
}
|
||||
|
||||
protected fun startActivitySafe(intent: Intent): Boolean = try {
|
||||
startActivity(intent)
|
||||
true
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
Snackbar.make(listView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT).show()
|
||||
false
|
||||
}
|
||||
|
||||
private fun focusPreference(key: String) {
|
||||
val pref = findPreference<Preference>(key)
|
||||
if (pref == null) {
|
||||
|
||||
@@ -10,14 +10,14 @@ import androidx.core.text.HtmlCompat
|
||||
import androidx.core.text.htmlEncode
|
||||
import androidx.core.text.method.LinkMovementMethodCompat
|
||||
import androidx.core.text.parseAsHtml
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.ui.AlertDialogFragment
|
||||
import org.koitharu.kotatsu.core.util.ext.getCauseUrl
|
||||
import org.koitharu.kotatsu.core.util.ext.isReportable
|
||||
import org.koitharu.kotatsu.core.util.ext.report
|
||||
import org.koitharu.kotatsu.core.util.ext.requireSerializable
|
||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||
import org.koitharu.kotatsu.databinding.DialogErrorDetailsBinding
|
||||
|
||||
class ErrorDetailsDialog : AlertDialogFragment<DialogErrorDetailsBinding>() {
|
||||
@@ -27,7 +27,7 @@ class ErrorDetailsDialog : AlertDialogFragment<DialogErrorDetailsBinding>() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val args = requireArguments()
|
||||
exception = args.requireSerializable(ARG_ERROR)
|
||||
exception = args.requireSerializable(AppRouter.KEY_ERROR)
|
||||
}
|
||||
|
||||
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): DialogErrorDetailsBinding {
|
||||
@@ -41,7 +41,7 @@ class ErrorDetailsDialog : AlertDialogFragment<DialogErrorDetailsBinding>() {
|
||||
text = context.getString(
|
||||
R.string.manga_error_description_pattern,
|
||||
exception.message?.htmlEncode().orEmpty(),
|
||||
arguments?.getString(ARG_URL),
|
||||
arguments?.getString(AppRouter.KEY_URL) ?: exception.getCauseUrl(),
|
||||
).parseAsHtml(HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||
}
|
||||
}
|
||||
@@ -71,16 +71,4 @@ class ErrorDetailsDialog : AlertDialogFragment<DialogErrorDetailsBinding>() {
|
||||
ClipData.newPlainText(getString(R.string.error), exception.stackTraceToString()),
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "ErrorDetailsDialog"
|
||||
private const val ARG_ERROR = "error"
|
||||
private const val ARG_URL = "url"
|
||||
|
||||
fun show(fm: FragmentManager, error: Throwable, url: String?) = ErrorDetailsDialog().withArgs(2) {
|
||||
putSerializable(ARG_ERROR, error)
|
||||
putString(ARG_URL, url)
|
||||
}.show(fm, TAG)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,16 @@ import android.graphics.Canvas
|
||||
import android.graphics.drawable.Animatable
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||
import coil3.Image
|
||||
import coil3.asImage
|
||||
import coil3.getExtra
|
||||
import coil3.request.ImageRequest
|
||||
import com.google.android.material.animation.ArgbEvaluatorCompat
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.util.KotatsuColors
|
||||
import org.koitharu.kotatsu.core.model.getTitle
|
||||
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
|
||||
import org.koitharu.kotatsu.core.util.ext.mangaSourceKey
|
||||
import kotlin.math.abs
|
||||
|
||||
class AnimatedFaviconDrawable(
|
||||
@@ -23,12 +28,12 @@ class AnimatedFaviconDrawable(
|
||||
private val period = context.getAnimationDuration(R.integer.config_longAnimTime) * 2
|
||||
private val timeAnimator = TimeAnimator()
|
||||
|
||||
private val colorHigh = MaterialColors.harmonize(KotatsuColors.random(name), colorBackground)
|
||||
private val colorLow = ArgbEvaluatorCompat.getInstance().evaluate(0.3f, colorHigh, colorBackground)
|
||||
private var colorHigh = MaterialColors.harmonize(colorForeground, currentBackgroundColor)
|
||||
private var colorLow = ArgbEvaluatorCompat.getInstance().evaluate(0.3f, colorHigh, currentBackgroundColor)
|
||||
|
||||
init {
|
||||
timeAnimator.setTimeListener(this)
|
||||
updateColor()
|
||||
onStateChange(state)
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
@@ -39,9 +44,11 @@ class AnimatedFaviconDrawable(
|
||||
super.draw(canvas)
|
||||
}
|
||||
|
||||
override fun setAlpha(alpha: Int) = Unit
|
||||
|
||||
override fun getAlpha(): Int = 255
|
||||
// override fun setAlpha(alpha: Int) = Unit
|
||||
//
|
||||
// override fun getAlpha(): Int = 255
|
||||
//
|
||||
// override fun isOpaque(): Boolean = false
|
||||
|
||||
override fun onTimeUpdate(animation: TimeAnimator?, totalTime: Long, deltaTime: Long) {
|
||||
callback?.also {
|
||||
@@ -60,13 +67,33 @@ class AnimatedFaviconDrawable(
|
||||
|
||||
override fun isRunning(): Boolean = timeAnimator.isStarted
|
||||
|
||||
override fun onStateChange(state: IntArray): Boolean {
|
||||
val res = super.onStateChange(state)
|
||||
colorHigh = MaterialColors.harmonize(currentForegroundColor, currentBackgroundColor)
|
||||
colorLow = ArgbEvaluatorCompat.getInstance().evaluate(0.3f, colorHigh, currentBackgroundColor)
|
||||
updateColor()
|
||||
return res
|
||||
}
|
||||
|
||||
private fun updateColor() {
|
||||
if (period <= 0f) {
|
||||
return
|
||||
}
|
||||
val ph = period / 2
|
||||
val fraction = abs((System.currentTimeMillis() % period) - ph) / ph.toFloat()
|
||||
colorForeground = ArgbEvaluatorCompat.getInstance()
|
||||
currentForegroundColor = ArgbEvaluatorCompat.getInstance()
|
||||
.evaluate(interpolator.getInterpolation(fraction), colorLow, colorHigh)
|
||||
}
|
||||
|
||||
class Factory(
|
||||
@StyleRes private val styleResId: Int,
|
||||
) : ((ImageRequest) -> Image?) {
|
||||
|
||||
override fun invoke(request: ImageRequest): Image? {
|
||||
val source = request.getExtra(mangaSourceKey) ?: return null
|
||||
val context = request.context
|
||||
val title = source.getTitle(context)
|
||||
return AnimatedFaviconDrawable(context, styleResId, title).asImage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.graphics.ColorFilter
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||
import com.google.android.material.animation.ArgbEvaluatorCompat
|
||||
import org.koitharu.kotatsu.R
|
||||
@@ -23,6 +24,7 @@ class AnimatedPlaceholderDrawable(context: Context) : Drawable(), Animatable, Ti
|
||||
private val interpolator = FastOutSlowInInterpolator()
|
||||
private val period = context.getAnimationDuration(R.integer.config_longAnimTime) * 2
|
||||
private val timeAnimator = TimeAnimator()
|
||||
private var currentAlpha: Int = 255
|
||||
|
||||
init {
|
||||
timeAnimator.setTimeListener(this)
|
||||
@@ -38,16 +40,17 @@ class AnimatedPlaceholderDrawable(context: Context) : Drawable(), Animatable, Ti
|
||||
}
|
||||
|
||||
override fun setAlpha(alpha: Int) {
|
||||
// this.alpha = alpha FIXME coil's crossfade
|
||||
currentAlpha = alpha
|
||||
updateColor()
|
||||
}
|
||||
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) = Unit
|
||||
|
||||
@Suppress("DeprecatedCallableAddReplaceWith")
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun getOpacity(): Int = PixelFormat.OPAQUE
|
||||
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
|
||||
|
||||
override fun getAlpha(): Int = 255
|
||||
override fun getAlpha(): Int = currentAlpha
|
||||
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) = Unit
|
||||
|
||||
override fun onTimeUpdate(animation: TimeAnimator?, totalTime: Long, deltaTime: Long) {
|
||||
callback?.also {
|
||||
@@ -72,7 +75,10 @@ class AnimatedPlaceholderDrawable(context: Context) : Drawable(), Animatable, Ti
|
||||
}
|
||||
val ph = period / 2
|
||||
val fraction = abs((System.currentTimeMillis() % period) - ph) / ph.toFloat()
|
||||
currentColor = ArgbEvaluatorCompat.getInstance()
|
||||
.evaluate(interpolator.getInterpolation(fraction), colorLow, colorHigh)
|
||||
currentColor = ColorUtils.setAlphaComponent(
|
||||
ArgbEvaluatorCompat.getInstance()
|
||||
.evaluate(interpolator.getInterpolation(fraction), colorLow, colorHigh),
|
||||
currentAlpha
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,47 @@
|
||||
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.Paint
|
||||
import android.graphics.Path
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.core.content.withStyledAttributes
|
||||
import androidx.core.graphics.withClip
|
||||
import coil3.Image
|
||||
import coil3.asImage
|
||||
import coil3.getExtra
|
||||
import coil3.request.ImageRequest
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.getTitle
|
||||
import org.koitharu.kotatsu.core.util.KotatsuColors
|
||||
import org.koitharu.kotatsu.core.util.ext.hasFocusStateSpecified
|
||||
import org.koitharu.kotatsu.core.util.ext.mangaSourceKey
|
||||
|
||||
open class FaviconDrawable(
|
||||
context: Context,
|
||||
@StyleRes styleResId: Int,
|
||||
name: String,
|
||||
) : Drawable() {
|
||||
) : PaintDrawable() {
|
||||
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
protected var colorBackground = Color.WHITE
|
||||
override val paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.SUBPIXEL_TEXT_FLAG)
|
||||
protected var currentBackgroundColor = Color.WHITE
|
||||
private set
|
||||
private var colorBackground: ColorStateList = ColorStateList.valueOf(currentBackgroundColor)
|
||||
protected var colorForeground = Color.DKGRAY
|
||||
private var colorStroke = Color.LTGRAY
|
||||
protected var currentForegroundColor = Color.DKGRAY
|
||||
protected var currentStrokeColor = Color.LTGRAY
|
||||
private set
|
||||
private var colorStroke: ColorStateList = ColorStateList.valueOf(currentStrokeColor)
|
||||
private val letter = name.take(1).uppercase()
|
||||
private var cornerSize = 0f
|
||||
private var intrinsicSize = -1
|
||||
private val textBounds = Rect()
|
||||
private val tempRect = Rect()
|
||||
private val boundsF = RectF()
|
||||
@@ -36,14 +49,17 @@ open class FaviconDrawable(
|
||||
|
||||
init {
|
||||
context.withStyledAttributes(styleResId, R.styleable.FaviconFallbackDrawable) {
|
||||
colorBackground = getColor(R.styleable.FaviconFallbackDrawable_backgroundColor, colorBackground)
|
||||
colorStroke = getColor(R.styleable.FaviconFallbackDrawable_strokeColor, colorStroke)
|
||||
colorBackground = getColorStateList(R.styleable.FaviconFallbackDrawable_backgroundColor) ?: colorBackground
|
||||
colorStroke = getColorStateList(R.styleable.FaviconFallbackDrawable_strokeColor) ?: colorStroke
|
||||
cornerSize = getDimension(R.styleable.FaviconFallbackDrawable_cornerSize, cornerSize)
|
||||
paint.strokeWidth = getDimension(R.styleable.FaviconFallbackDrawable_strokeWidth, 0f) * 2f
|
||||
intrinsicSize = getDimensionPixelSize(R.styleable.FaviconFallbackDrawable_drawableSize, intrinsicSize)
|
||||
}
|
||||
paint.textAlign = Paint.Align.CENTER
|
||||
paint.isFakeBoldText = true
|
||||
colorForeground = MaterialColors.harmonize(KotatsuColors.random(name), colorBackground)
|
||||
colorForeground = KotatsuColors.random(name)
|
||||
currentForegroundColor = MaterialColors.harmonize(colorForeground, colorBackground.defaultColor)
|
||||
onStateChange(state)
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
@@ -67,31 +83,42 @@ open class FaviconDrawable(
|
||||
clipPath.close()
|
||||
}
|
||||
|
||||
override fun setAlpha(alpha: Int) {
|
||||
paint.alpha = alpha
|
||||
}
|
||||
override fun getIntrinsicWidth(): Int = intrinsicSize
|
||||
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) {
|
||||
paint.colorFilter = colorFilter
|
||||
}
|
||||
override fun getIntrinsicHeight(): Int = intrinsicSize
|
||||
|
||||
@Suppress("DeprecatedCallableAddReplaceWith")
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun getOpacity() = PixelFormat.TRANSPARENT
|
||||
override fun isOpaque(): Boolean = cornerSize == 0f && colorBackground.isOpaque
|
||||
|
||||
override fun isStateful(): Boolean = colorStroke.isStateful || colorBackground.isStateful
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
override fun hasFocusStateSpecified(): Boolean =
|
||||
colorBackground.hasFocusStateSpecified() || colorStroke.hasFocusStateSpecified()
|
||||
|
||||
override fun onStateChange(state: IntArray): Boolean {
|
||||
val prevStrokeColor = currentStrokeColor
|
||||
val prevBackgroundColor = currentBackgroundColor
|
||||
currentStrokeColor = colorStroke.getColorForState(state, colorStroke.defaultColor)
|
||||
currentBackgroundColor = colorBackground.getColorForState(state, colorBackground.defaultColor)
|
||||
if (currentBackgroundColor != prevBackgroundColor) {
|
||||
currentForegroundColor = MaterialColors.harmonize(colorForeground, currentBackgroundColor)
|
||||
}
|
||||
return prevBackgroundColor != currentBackgroundColor || prevStrokeColor != currentStrokeColor
|
||||
}
|
||||
|
||||
private fun doDraw(canvas: Canvas) {
|
||||
// background
|
||||
paint.color = colorBackground
|
||||
paint.color = currentBackgroundColor
|
||||
paint.style = Paint.Style.FILL
|
||||
canvas.drawPaint(paint)
|
||||
// letter
|
||||
paint.color = colorForeground
|
||||
paint.color = currentForegroundColor
|
||||
val cx = (boundsF.left + boundsF.right) * 0.6f
|
||||
val ty = boundsF.bottom * 0.7f + textBounds.height() * 0.5f - textBounds.bottom
|
||||
canvas.drawText(letter, cx, ty, paint)
|
||||
if (paint.strokeWidth > 0f) {
|
||||
// stroke
|
||||
paint.color = colorStroke
|
||||
paint.color = currentStrokeColor
|
||||
paint.style = Paint.Style.STROKE
|
||||
canvas.drawPath(clipPath, paint)
|
||||
}
|
||||
@@ -103,4 +130,16 @@ open class FaviconDrawable(
|
||||
paint.getTextBounds(text, 0, text.length, tempRect)
|
||||
return testTextSize * width / tempRect.width()
|
||||
}
|
||||
|
||||
class Factory(
|
||||
@StyleRes private val styleResId: Int,
|
||||
) : ((ImageRequest) -> Image?) {
|
||||
|
||||
override fun invoke(request: ImageRequest): Image? {
|
||||
val source = request.getExtra(mangaSourceKey) ?: return null
|
||||
val context = request.context
|
||||
val title = source.getTitle(context)
|
||||
return FaviconDrawable(context, styleResId, title).asImage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.koitharu.kotatsu.core.ui.image
|
||||
|
||||
import android.graphics.ColorFilter
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.drawable.Drawable
|
||||
|
||||
@Suppress("OVERRIDE_DEPRECATION")
|
||||
abstract class PaintDrawable : Drawable() {
|
||||
|
||||
protected abstract val paint: Paint
|
||||
|
||||
override fun setAlpha(alpha: Int) {
|
||||
paint.alpha = alpha
|
||||
}
|
||||
|
||||
override fun getAlpha(): Int {
|
||||
return paint.alpha
|
||||
}
|
||||
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) {
|
||||
paint.colorFilter = colorFilter
|
||||
}
|
||||
|
||||
override fun getColorFilter(): ColorFilter? {
|
||||
return paint.colorFilter
|
||||
}
|
||||
|
||||
override fun setDither(dither: Boolean) {
|
||||
paint.isDither = dither
|
||||
}
|
||||
|
||||
override fun setFilterBitmap(filter: Boolean) {
|
||||
paint.isFilterBitmap = filter
|
||||
}
|
||||
|
||||
override fun isFilterBitmap(): Boolean {
|
||||
return paint.isFilterBitmap
|
||||
}
|
||||
|
||||
override fun getOpacity(): Int {
|
||||
if (paint.colorFilter != null) {
|
||||
return PixelFormat.TRANSLUCENT
|
||||
}
|
||||
return when (paint.alpha) {
|
||||
0 -> PixelFormat.TRANSPARENT
|
||||
255 -> if (isOpaque()) PixelFormat.OPAQUE else PixelFormat.TRANSLUCENT
|
||||
else -> PixelFormat.TRANSLUCENT
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun isOpaque() = false
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package org.koitharu.kotatsu.core.ui.image
|
||||
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PointF
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.graphics.PaintCompat
|
||||
import org.koitharu.kotatsu.core.util.ext.hasFocusStateSpecified
|
||||
|
||||
class TextDrawable(
|
||||
val text: String,
|
||||
) : PaintDrawable() {
|
||||
|
||||
override val paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.SUBPIXEL_TEXT_FLAG)
|
||||
private val textBounds = Rect()
|
||||
private val textPoint = PointF()
|
||||
|
||||
var textSize: Float
|
||||
get() = paint.textSize
|
||||
set(value) {
|
||||
paint.textSize = value
|
||||
measureTextBounds()
|
||||
}
|
||||
|
||||
var textColor: ColorStateList = ColorStateList.valueOf(Color.BLACK)
|
||||
set(value) {
|
||||
field = value
|
||||
onStateChange(state)
|
||||
}
|
||||
|
||||
init {
|
||||
onStateChange(state)
|
||||
measureTextBounds()
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
canvas.drawText(text, textPoint.x, textPoint.y, paint)
|
||||
}
|
||||
|
||||
override fun onBoundsChange(bounds: Rect) {
|
||||
textPoint.set(
|
||||
bounds.exactCenterX() - textBounds.exactCenterX(),
|
||||
bounds.exactCenterY() - textBounds.exactCenterY(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun getIntrinsicWidth(): Int = textBounds.width()
|
||||
|
||||
override fun getIntrinsicHeight(): Int = textBounds.height()
|
||||
|
||||
override fun isStateful(): Boolean = textColor.isStateful
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
override fun hasFocusStateSpecified(): Boolean = textColor.hasFocusStateSpecified()
|
||||
|
||||
override fun onStateChange(state: IntArray): Boolean {
|
||||
val prevColor = paint.color
|
||||
paint.color = textColor.getColorForState(state, textColor.defaultColor)
|
||||
return paint.color != prevColor
|
||||
}
|
||||
|
||||
private fun measureTextBounds() {
|
||||
paint.getTextBounds(text, 0, text.length, textBounds)
|
||||
onBoundsChange(bounds)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun compound(textView: TextView, text: String): TextDrawable? {
|
||||
val drawable = TextDrawable(text)
|
||||
drawable.textSize = textView.textSize
|
||||
drawable.textColor = textView.textColors
|
||||
return drawable.takeIf {
|
||||
PaintCompat.hasGlyph(drawable.paint, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package org.koitharu.kotatsu.core.ui.image
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.Gravity
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.GravityInt
|
||||
import coil3.target.GenericViewTarget
|
||||
|
||||
class TextViewTarget(
|
||||
override val view: TextView,
|
||||
@GravityInt compoundDrawable: Int,
|
||||
) : GenericViewTarget<TextView>() {
|
||||
|
||||
private val drawableIndex: Int = when (compoundDrawable) {
|
||||
Gravity.START -> 0
|
||||
Gravity.TOP -> 2
|
||||
Gravity.END -> 3
|
||||
Gravity.BOTTOM -> 4
|
||||
else -> -1
|
||||
}
|
||||
|
||||
override var drawable: Drawable?
|
||||
get() = if (drawableIndex != -1) {
|
||||
view.compoundDrawablesRelative[drawableIndex]
|
||||
} else {
|
||||
null
|
||||
}
|
||||
set(value) {
|
||||
if (drawableIndex == -1) {
|
||||
return
|
||||
}
|
||||
val drawables = view.compoundDrawablesRelative
|
||||
drawables[drawableIndex] = value
|
||||
view.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||
drawables[0],
|
||||
drawables[1],
|
||||
drawables[2],
|
||||
drawables[3],
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package org.koitharu.kotatsu.core.ui.widgets
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.os.Parcelable.Creator
|
||||
import android.util.AttributeSet
|
||||
import androidx.core.content.withStyledAttributes
|
||||
import androidx.customview.view.AbsSavedState
|
||||
import com.google.android.material.shape.MaterialShapeDrawable
|
||||
import com.google.android.material.shape.ShapeAppearanceModel
|
||||
import com.google.android.material.textview.MaterialTextView
|
||||
import org.koitharu.kotatsu.R
|
||||
|
||||
class BadgeView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : MaterialTextView(context, attrs, R.attr.badgeViewStyle) {
|
||||
|
||||
private var maxCharacterCount = Int.MAX_VALUE
|
||||
|
||||
var number: Int = 0
|
||||
set(value) {
|
||||
field = value
|
||||
updateText()
|
||||
}
|
||||
|
||||
init {
|
||||
context.withStyledAttributes(attrs, R.styleable.BadgeView, R.attr.badgeViewStyle) {
|
||||
maxCharacterCount = getInt(R.styleable.BadgeView_maxCharacterCount, maxCharacterCount)
|
||||
number = getInt(R.styleable.BadgeView_number, number)
|
||||
val shape = ShapeAppearanceModel.builder(
|
||||
context,
|
||||
getResourceId(R.styleable.BadgeView_shapeAppearance, 0),
|
||||
0,
|
||||
).build()
|
||||
background = MaterialShapeDrawable(shape).also { bg ->
|
||||
bg.fillColor = getColorStateList(R.styleable.BadgeView_backgroundColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(): Parcelable? {
|
||||
val superState = super.onSaveInstanceState() ?: return null
|
||||
return SavedState(superState, number)
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(state: Parcelable?) {
|
||||
if (state is SavedState) {
|
||||
super.onRestoreInstanceState(state.superState)
|
||||
number = state.number
|
||||
} else {
|
||||
super.onRestoreInstanceState(state)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateText() {
|
||||
if (number <= 0) {
|
||||
text = null
|
||||
return
|
||||
}
|
||||
val numberString = number.toString()
|
||||
text = if (numberString.length > maxCharacterCount) {
|
||||
buildString(maxCharacterCount) {
|
||||
repeat(maxCharacterCount - 1) { append('9') }
|
||||
append('+')
|
||||
}
|
||||
} else {
|
||||
numberString
|
||||
}
|
||||
}
|
||||
|
||||
private class SavedState : AbsSavedState {
|
||||
|
||||
val number: Int
|
||||
|
||||
constructor(superState: Parcelable, number: Int) : super(superState) {
|
||||
this.number = number
|
||||
}
|
||||
|
||||
constructor(source: Parcel, classLoader: ClassLoader?) : super(source, classLoader) {
|
||||
number = source.readInt()
|
||||
}
|
||||
|
||||
override fun writeToParcel(out: Parcel, flags: Int) {
|
||||
super.writeToParcel(out, flags)
|
||||
out.writeInt(number)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@Suppress("unused")
|
||||
@JvmField
|
||||
val CREATOR: Creator<SavedState> = object : Creator<SavedState> {
|
||||
override fun createFromParcel(`in`: Parcel) = SavedState(`in`, SavedState::class.java.classLoader)
|
||||
|
||||
override fun newArray(size: Int): Array<SavedState?> = arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.core.ui.widgets
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.View.OnClickListener
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
@@ -34,7 +33,7 @@ import com.google.android.material.R as materialR
|
||||
class ChipsView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = com.google.android.material.R.attr.chipGroupStyle,
|
||||
defStyleAttr: Int = materialR.attr.chipGroupStyle,
|
||||
) : ChipGroup(context, attrs, defStyleAttr) {
|
||||
|
||||
@Inject
|
||||
@@ -49,6 +48,7 @@ class ChipsView @JvmOverloads constructor(
|
||||
onChipCloseClickListener?.onChipCloseClick(chip, data) ?: onChipClickListener?.onChipClick(chip, data)
|
||||
}
|
||||
private val chipStyle: Int
|
||||
private val iconsVisible: Boolean
|
||||
var onChipClickListener: OnChipClickListener? = null
|
||||
set(value) {
|
||||
field = value
|
||||
@@ -60,6 +60,7 @@ class ChipsView @JvmOverloads constructor(
|
||||
init {
|
||||
val ta = context.obtainStyledAttributes(attrs, R.styleable.ChipsView, defStyleAttr, 0)
|
||||
chipStyle = ta.getResourceId(R.styleable.ChipsView_chipStyle, R.style.Widget_Kotatsu_Chip)
|
||||
iconsVisible = ta.getBoolean(R.styleable.ChipsView_chipIconVisible, true)
|
||||
ta.recycle()
|
||||
|
||||
if (isInEditMode) {
|
||||
@@ -170,12 +171,7 @@ class ChipsView @JvmOverloads constructor(
|
||||
|
||||
private fun bindIcon(model: ChipModel) {
|
||||
when {
|
||||
model.isChecked -> {
|
||||
imageRequest?.dispose()
|
||||
imageRequest = null
|
||||
chipIcon = null
|
||||
isChipIconVisible = false
|
||||
}
|
||||
model.isChecked -> disposeIcon()
|
||||
|
||||
model.isLoading -> {
|
||||
imageRequest?.dispose()
|
||||
@@ -184,6 +180,8 @@ class ChipsView @JvmOverloads constructor(
|
||||
setProgressIcon()
|
||||
}
|
||||
|
||||
!iconsVisible -> disposeIcon()
|
||||
|
||||
model.iconData != null -> {
|
||||
val placeholder = model.icon.ifZero { materialR.drawable.navigation_empty_icon }
|
||||
imageRequest = ImageRequest.Builder(context)
|
||||
@@ -207,14 +205,16 @@ class ChipsView @JvmOverloads constructor(
|
||||
isChipIconVisible = true
|
||||
}
|
||||
|
||||
else -> {
|
||||
imageRequest?.dispose()
|
||||
imageRequest = null
|
||||
chipIcon = null
|
||||
isChipIconVisible = false
|
||||
}
|
||||
else -> disposeIcon()
|
||||
}
|
||||
}
|
||||
|
||||
private fun disposeIcon() {
|
||||
imageRequest?.dispose()
|
||||
imageRequest = null
|
||||
chipIcon = null
|
||||
isChipIconVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
private inner class InternalChipClickListener : OnClickListener {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.koitharu.kotatsu.core.ui.widgets
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
@@ -11,7 +12,7 @@ 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.getThemeColorStateList
|
||||
import org.koitharu.kotatsu.core.util.ext.measureDimension
|
||||
import org.koitharu.kotatsu.core.util.ext.resolveDp
|
||||
import org.koitharu.kotatsu.parsers.util.toIntUp
|
||||
@@ -30,6 +31,7 @@ class DotsIndicator @JvmOverloads constructor(
|
||||
private var smallDotAlpha = 0.6f
|
||||
private var positionOffset: Float = 0f
|
||||
private var position: Int = 0
|
||||
private var dotsColor: ColorStateList = ColorStateList.valueOf(Color.DKGRAY)
|
||||
private val inset = context.resources.resolveDp(1f)
|
||||
|
||||
var max: Int = 6
|
||||
@@ -52,10 +54,10 @@ class DotsIndicator @JvmOverloads constructor(
|
||||
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),
|
||||
)
|
||||
dotsColor = getColorStateList(R.styleable.DotsIndicator_dotColor)
|
||||
?: context.getThemeColorStateList(materialR.attr.colorOnBackground)
|
||||
?: dotsColor
|
||||
paint.color = dotsColor.getColorForState(drawableState, dotsColor.defaultColor)
|
||||
indicatorSize = getDimension(R.styleable.DotsIndicator_dotSize, indicatorSize)
|
||||
dotSpacing = getDimension(R.styleable.DotsIndicator_dotSpacing, dotSpacing)
|
||||
smallDotScale = getFloat(R.styleable.DotsIndicator_dotScale, smallDotScale).coerceIn(0f, 1f)
|
||||
@@ -89,6 +91,13 @@ class DotsIndicator @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun drawableStateChanged() {
|
||||
if (dotsColor.isStateful) {
|
||||
paint.color = dotsColor.getColorForState(drawableState, dotsColor.defaultColor)
|
||||
}
|
||||
super.drawableStateChanged()
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val dotSize = getDotSize()
|
||||
val desiredHeight = (dotSize + paddingTop + paddingBottom).toIntUp()
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
|
||||
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
@Deprecated("")
|
||||
class ProgressButton @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
|
||||
@@ -4,14 +4,14 @@ import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import com.google.android.material.button.MaterialButtonGroup
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.databinding.ViewZoomBinding
|
||||
|
||||
class ZoomControl @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
) : LinearLayout(context, attrs), View.OnClickListener {
|
||||
) : MaterialButtonGroup(context, attrs), View.OnClickListener {
|
||||
|
||||
private val binding = ViewZoomBinding.inflate(LayoutInflater.from(context), this)
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@ package org.koitharu.kotatsu.core.util
|
||||
|
||||
import android.content.Context
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.widget.EditText
|
||||
import androidx.annotation.CallSuper
|
||||
import org.koitharu.kotatsu.core.ui.util.DefaultTextWatcher
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
abstract class EditTextValidator : TextWatcher {
|
||||
abstract class EditTextValidator : DefaultTextWatcher {
|
||||
|
||||
private var editTextRef: WeakReference<EditText>? = null
|
||||
|
||||
@@ -17,10 +17,6 @@ abstract class EditTextValidator : TextWatcher {
|
||||
"EditTextValidator is not attached to EditText"
|
||||
}
|
||||
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
|
||||
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
|
||||
|
||||
@CallSuper
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
val editText = editTextRef?.get() ?: return
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.koitharu.kotatsu.core.util
|
||||
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import org.koitharu.kotatsu.core.util.ext.indexOfContains
|
||||
import org.koitharu.kotatsu.core.util.ext.iterator
|
||||
|
||||
class LocaleStringComparator : Comparator<String?> {
|
||||
|
||||
private val deviceLocales: List<String?>
|
||||
|
||||
init {
|
||||
val localeList = LocaleListCompat.getAdjustedDefault()
|
||||
deviceLocales = buildList(localeList.size() + 1) {
|
||||
add(null)
|
||||
val set = HashSet<String?>(localeList.size() + 1)
|
||||
set.add(null)
|
||||
for (locale in localeList) {
|
||||
val lang = locale.getDisplayLanguage(locale)
|
||||
if (set.add(lang)) {
|
||||
add(lang)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun compare(a: String?, b: String?): Int {
|
||||
val indexA = deviceLocales.indexOfContains(a, true)
|
||||
val indexB = deviceLocales.indexOfContains(b, true)
|
||||
return when {
|
||||
indexA < 0 && indexB < 0 -> compareValues(a, b)
|
||||
indexA < 0 -> 1
|
||||
indexB < 0 -> -1
|
||||
else -> compareValues(indexA, indexB)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.koitharu.kotatsu.core.util
|
||||
|
||||
import android.graphics.Paint
|
||||
import androidx.core.graphics.PaintCompat
|
||||
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
|
||||
import java.util.Locale
|
||||
|
||||
object LocaleUtils {
|
||||
|
||||
private val paint = Paint()
|
||||
|
||||
fun getEmojiFlag(locale: Locale): String? {
|
||||
val code = when (val c = locale.country.ifNullOrEmpty { locale.toLanguageTag() }.uppercase(Locale.ENGLISH)) {
|
||||
"EN" -> "GB"
|
||||
"JA" -> "JP"
|
||||
else -> c
|
||||
}
|
||||
val emoji = countryCodeToEmojiFlag(code)
|
||||
return if (PaintCompat.hasGlyph(paint, emoji)) {
|
||||
emoji
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun countryCodeToEmojiFlag(countryCode: String): String {
|
||||
return countryCode.map { char ->
|
||||
Character.codePointAt("$char", 0) - 0x41 + 0x1F1E6
|
||||
}.map { codePoint ->
|
||||
Character.toChars(codePoint)
|
||||
}.joinToString(separator = "") { charArray ->
|
||||
String(charArray)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.koitharu.kotatsu.core.util
|
||||
|
||||
import android.os.Build
|
||||
import android.webkit.MimeTypeMap
|
||||
import org.jetbrains.annotations.Blocking
|
||||
import org.koitharu.kotatsu.core.util.ext.MimeType
|
||||
import org.koitharu.kotatsu.core.util.ext.toMimeTypeOrNull
|
||||
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
|
||||
import org.koitharu.kotatsu.parsers.util.removeSuffix
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import coil3.util.MimeTypeMap as CoilMimeTypeMap
|
||||
|
||||
object MimeTypes {
|
||||
|
||||
fun getMimeTypeFromExtension(fileName: String): MimeType? {
|
||||
return CoilMimeTypeMap.getMimeTypeFromExtension(getNormalizedExtension(fileName) ?: return null)
|
||||
?.toMimeTypeOrNull()
|
||||
}
|
||||
|
||||
fun getMimeTypeFromUrl(url: String): MimeType? {
|
||||
return CoilMimeTypeMap.getMimeTypeFromUrl(url)?.toMimeTypeOrNull()
|
||||
}
|
||||
|
||||
fun getExtension(mimeType: MimeType?): String? {
|
||||
return MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType?.toString() ?: return null)?.nullIfEmpty()
|
||||
}
|
||||
|
||||
@Blocking
|
||||
fun probeMimeType(file: File): MimeType? {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
runCatchingCancellable {
|
||||
Files.probeContentType(file.toPath())?.toMimeTypeOrNull()
|
||||
}.getOrNull()?.let { return it }
|
||||
}
|
||||
return getMimeTypeFromExtension(file.name)
|
||||
}
|
||||
|
||||
fun getNormalizedExtension(name: String): String? = name
|
||||
.lowercase()
|
||||
.removeSuffix('~')
|
||||
.removeSuffix(".tmp")
|
||||
.substringAfterLast('.', "")
|
||||
.takeIf { it.length in 2..5 }
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.ActivityManager
|
||||
import android.app.ActivityManager.MemoryInfo
|
||||
import android.app.ActivityOptions
|
||||
import android.app.LocaleConfig
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
@@ -23,19 +22,18 @@ import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.net.ConnectivityManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import android.view.View
|
||||
import android.view.ViewPropertyAnimator
|
||||
import android.view.Window
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.WebView
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.annotation.CheckResult
|
||||
import androidx.annotation.IntegerRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.appcompat.app.AppCompatDialog
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
@@ -86,12 +84,14 @@ suspend fun CoroutineWorker.trySetForeground(): Boolean = runCatchingCancellable
|
||||
setForeground(info)
|
||||
}.isSuccess
|
||||
|
||||
@CheckResult
|
||||
fun <I> ActivityResultLauncher<I>.resolve(context: Context, input: I): ResolveInfo? {
|
||||
val pm = context.packageManager
|
||||
val intent = contract.createIntent(context, input)
|
||||
return pm.resolveActivity(intent, 0)
|
||||
}
|
||||
|
||||
@CheckResult
|
||||
fun <I> ActivityResultLauncher<I>.tryLaunch(
|
||||
input: I,
|
||||
options: ActivityOptionsCompat? = null,
|
||||
@@ -171,7 +171,7 @@ fun Context.getAnimationDuration(@IntegerRes resId: Int): Long {
|
||||
}
|
||||
|
||||
fun Context.isLowRamDevice(): Boolean {
|
||||
return activityManager?.isLowRamDevice ?: false
|
||||
return activityManager?.isLowRamDevice == true
|
||||
}
|
||||
|
||||
fun Context.isPowerSaveMode(): Boolean {
|
||||
@@ -185,18 +185,6 @@ val Context.ramAvailable: Long
|
||||
return result.availMem
|
||||
}
|
||||
|
||||
fun scaleUpActivityOptionsOf(view: View): Bundle? = if (view.context.isAnimationsEnabled) {
|
||||
ActivityOptions.makeScaleUpAnimation(
|
||||
view,
|
||||
0,
|
||||
0,
|
||||
view.width,
|
||||
view.height,
|
||||
).toBundle()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
@SuppressLint("DiscouragedApi")
|
||||
fun Context.getLocalesConfig(): LocaleListCompat {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
@@ -277,6 +265,9 @@ fun WebView.configureForParser(userAgentOverride: String?) = with(settings) {
|
||||
if (userAgentOverride != null) {
|
||||
userAgentString = userAgentOverride
|
||||
}
|
||||
val cookieManager = CookieManager.getInstance()
|
||||
cookieManager.setAcceptCookie(true)
|
||||
cookieManager.setAcceptThirdPartyCookies(this@configureForParser, true)
|
||||
}
|
||||
|
||||
fun Context.restartApplication() {
|
||||
|
||||
@@ -110,3 +110,5 @@ fun <T : Parcelable> Parcelable.Creator<T>.unmarshall(bytes: ByteArray): T {
|
||||
parcel.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
inline fun buildBundle(capacity: Int, block: Bundle.() -> Unit): Bundle = Bundle(capacity).apply(block)
|
||||
|
||||
@@ -108,3 +108,7 @@ fun <T, R> Collection<T>.mapSortedByCount(isDescending: Boolean = true, mapper:
|
||||
}
|
||||
return sorted.map { it.first }
|
||||
}
|
||||
|
||||
fun Collection<CharSequence?>.indexOfContains(element: CharSequence?, ignoreCase: Boolean): Int = indexOfFirst { x ->
|
||||
(x == null && element == null) || (x != null && element != null && x.contains(element, ignoreCase))
|
||||
}
|
||||
|
||||
@@ -7,15 +7,13 @@ import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.storage.StorageManager
|
||||
import android.provider.OpenableColumns
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.core.database.getStringOrNull
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import org.jetbrains.annotations.Blocking
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.fs.FileSequence
|
||||
import org.koitharu.kotatsu.core.util.MimeTypes
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.nio.file.attribute.BasicFileAttributes
|
||||
@@ -41,12 +39,6 @@ fun ZipFile.readText(entry: ZipEntry) = getInputStream(entry).use { output ->
|
||||
output.bufferedReader().use(BufferedReader::readText)
|
||||
}
|
||||
|
||||
val ZipEntry.mimeType: MediaType?
|
||||
get() {
|
||||
val ext = name.substringAfterLast('.')
|
||||
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext)?.toMediaTypeOrNull()
|
||||
}
|
||||
|
||||
fun File.getStorageName(context: Context): String = runCatching {
|
||||
val manager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
@@ -115,3 +107,6 @@ fun File.walkCompat(includeDirectories: Boolean): Sequence<File> = if (Build.VER
|
||||
val walk = walk()
|
||||
if (includeDirectories) walk else walk.filter { it.isFile }
|
||||
}
|
||||
|
||||
val File.normalizedExtension: String?
|
||||
get() = MimeTypes.getNormalizedExtension(name)
|
||||
|
||||
@@ -2,9 +2,7 @@ package org.koitharu.kotatsu.core.util.ext
|
||||
|
||||
import android.os.Bundle
|
||||
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.coroutineScope
|
||||
|
||||
@@ -18,36 +16,10 @@ inline fun <T : Fragment> T.withArgs(size: Int, block: Bundle.() -> Unit): T {
|
||||
val Fragment.viewLifecycleScope
|
||||
inline get() = viewLifecycleOwner.lifecycle.coroutineScope
|
||||
|
||||
fun DialogFragment.showAllowStateLoss(manager: FragmentManager, tag: String?) {
|
||||
if (!manager.isStateSaved) {
|
||||
show(manager, tag)
|
||||
}
|
||||
}
|
||||
|
||||
fun Fragment.addMenuProvider(provider: MenuProvider) {
|
||||
requireActivity().addMenuProvider(provider, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
||||
}
|
||||
|
||||
fun DialogFragment.showDistinct(fm: FragmentManager, tag: String) {
|
||||
val existing = fm.findFragmentByTag(tag) as? DialogFragment?
|
||||
if (existing != null && existing.isVisible && existing.arguments == this.arguments) {
|
||||
return
|
||||
}
|
||||
show(fm, tag)
|
||||
}
|
||||
|
||||
tailrec fun Fragment.dismissParentDialog(): Boolean {
|
||||
return when (val parent = parentFragment) {
|
||||
null -> return false
|
||||
is DialogFragment -> {
|
||||
parent.dismiss()
|
||||
true
|
||||
}
|
||||
|
||||
else -> parent.dismissParentDialog()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
tailrec fun <T> Fragment.findParentCallback(cls: Class<T>): T? {
|
||||
val parent = parentFragment
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.koitharu.kotatsu.core.util.ext
|
||||
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Rect
|
||||
import kotlin.math.roundToInt
|
||||
@@ -18,3 +19,7 @@ inline fun <R> Bitmap.use(block: (Bitmap) -> R) = try {
|
||||
} finally {
|
||||
recycle()
|
||||
}
|
||||
|
||||
fun ColorStateList.hasFocusStateSpecified(): Boolean {
|
||||
return getColorForState(intArrayOf(android.R.attr.state_focused), defaultColor) != defaultColor
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.koitharu.kotatsu.core.util.ext
|
||||
|
||||
import okhttp3.MediaType
|
||||
|
||||
private const val TYPE_IMAGE = "image"
|
||||
private val REGEX_MIME = Regex("^\\w+/([-+.\\w]+|\\*)$", RegexOption.IGNORE_CASE)
|
||||
|
||||
@JvmInline
|
||||
value class MimeType(private val value: String) {
|
||||
|
||||
val type: String?
|
||||
get() = value.substringBefore('/', "").takeIfSpecified()
|
||||
|
||||
val subtype: String?
|
||||
get() = value.substringAfterLast('/', "").takeIfSpecified()
|
||||
|
||||
private fun String.takeIfSpecified(): String? = takeUnless {
|
||||
it.isEmpty() || it == "*"
|
||||
}
|
||||
|
||||
override fun toString(): String = value
|
||||
}
|
||||
|
||||
fun MediaType.toMimeType(): MimeType = MimeType("$type/$subtype")
|
||||
|
||||
fun String.toMimeTypeOrNull(): MimeType? = if (REGEX_MIME.matches(this)) {
|
||||
MimeType(lowercase())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val MimeType.isImage: Boolean
|
||||
get() = type == TYPE_IMAGE
|
||||
@@ -24,6 +24,7 @@ import org.koitharu.kotatsu.core.exceptions.ProxyConfigException
|
||||
import org.koitharu.kotatsu.core.exceptions.SyncApiException
|
||||
import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
|
||||
import org.koitharu.kotatsu.core.exceptions.UnsupportedSourceException
|
||||
import org.koitharu.kotatsu.core.exceptions.WrapperIOException
|
||||
import org.koitharu.kotatsu.core.exceptions.WrongPasswordException
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.io.NullOutputStream
|
||||
@@ -54,6 +55,8 @@ fun Throwable.getDisplayMessage(resources: Resources): String = getDisplayMessag
|
||||
?: resources.getString(R.string.error_occurred)
|
||||
|
||||
private fun Throwable.getDisplayMessageOrNull(resources: Resources): String? = when (this) {
|
||||
is CaughtException -> cause.getDisplayMessageOrNull(resources)
|
||||
is WrapperIOException -> cause.getDisplayMessageOrNull(resources)
|
||||
is ScrobblerAuthRequiredException -> resources.getString(
|
||||
R.string.scrobbler_auth_required,
|
||||
resources.getString(scrobbler.titleResId),
|
||||
@@ -141,7 +144,8 @@ fun Throwable.getCauseUrl(): String? = when (this) {
|
||||
is ParseException -> url
|
||||
is NotFoundException -> url
|
||||
is TooManyRequestExceptions -> url
|
||||
is CaughtException -> cause?.getCauseUrl()
|
||||
is CaughtException -> cause.getCauseUrl()
|
||||
is WrapperIOException -> cause.getCauseUrl()
|
||||
is NoDataReceivedException -> url
|
||||
is CloudFlareBlockedException -> url
|
||||
is CloudFlareProtectedException -> url
|
||||
@@ -175,7 +179,10 @@ fun Throwable.isReportable(): Boolean {
|
||||
return true
|
||||
}
|
||||
if (this is CaughtException) {
|
||||
return cause?.isReportable() == true
|
||||
return cause.isReportable()
|
||||
}
|
||||
if (this is WrapperIOException) {
|
||||
return cause.isReportable()
|
||||
}
|
||||
if (ExceptionResolver.canResolve(this)) {
|
||||
return false
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package org.koitharu.kotatsu.core.util.iterator
|
||||
|
||||
import org.koitharu.kotatsu.R
|
||||
|
||||
class MappingIterator<T, R>(
|
||||
private val upstream: Iterator<T>,
|
||||
private val mapper: (T) -> R,
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package org.koitharu.kotatsu.details.domain
|
||||
|
||||
import org.koitharu.kotatsu.core.util.LocaleStringComparator
|
||||
import org.koitharu.kotatsu.details.ui.model.MangaBranch
|
||||
|
||||
class BranchComparator : Comparator<MangaBranch> {
|
||||
|
||||
override fun compare(o1: MangaBranch, o2: MangaBranch): Int = compareValues(o1.name, o2.name)
|
||||
private val delegate = LocaleStringComparator()
|
||||
|
||||
override fun compare(o1: MangaBranch, o2: MangaBranch): Int = delegate.compare(o1.name, o2.name)
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import okio.IOException
|
||||
import org.koitharu.kotatsu.core.model.isLocal
|
||||
import org.koitharu.kotatsu.core.nav.MangaIntent
|
||||
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
|
||||
@@ -43,7 +43,14 @@ class DetailsLoadUseCase @Inject constructor(
|
||||
operator fun invoke(intent: MangaIntent): Flow<MangaDetails> = channelFlow {
|
||||
val manga = requireNotNull(mangaDataRepository.resolveIntent(intent)) {
|
||||
"Cannot resolve intent $intent"
|
||||
}.let { m ->
|
||||
if (m.chapters.isNullOrEmpty()) {
|
||||
getCachedDetails(m.id) ?: m
|
||||
} else {
|
||||
m
|
||||
}
|
||||
}
|
||||
send(MangaDetails(manga, null, null, false))
|
||||
val local = if (!manga.isLocal) {
|
||||
async {
|
||||
localMangaRepository.findSavedManga(manga)
|
||||
@@ -51,9 +58,9 @@ class DetailsLoadUseCase @Inject constructor(
|
||||
} else {
|
||||
null
|
||||
}
|
||||
send(MangaDetails(manga, null, null, false))
|
||||
try {
|
||||
val details = getDetails(manga)
|
||||
launch { mangaDataRepository.updateChapters(details) }
|
||||
launch { updateTracker(details) }
|
||||
send(
|
||||
MangaDetails(
|
||||
@@ -122,4 +129,8 @@ class DetailsLoadUseCase @Inject constructor(
|
||||
}.onFailure { e ->
|
||||
e.printStackTraceDebug()
|
||||
}
|
||||
|
||||
private suspend fun getCachedDetails(mangaId: Long): Manga? = runCatchingCancellable {
|
||||
mangaDataRepository.findMangaById(mangaId, withChapters = true)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ class ReadingTimeUseCase @Inject constructor(
|
||||
private val statsRepository: StatsRepository,
|
||||
) {
|
||||
|
||||
suspend fun invoke(manga: MangaDetails?, branch: String?, history: MangaHistory?): ReadingTime? {
|
||||
suspend operator fun invoke(manga: MangaDetails?, branch: String?, history: MangaHistory?): ReadingTime? {
|
||||
if (!settings.isReadingTimeEstimationEnabled) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ fun MangaDetails.mapChapters(
|
||||
return result
|
||||
}
|
||||
|
||||
fun List<ChapterListItem>.withVolumeHeaders(context: Context): List<ListModel> {
|
||||
fun List<ChapterListItem>.withVolumeHeaders(context: Context): MutableList<ListModel> {
|
||||
var prevVolume = 0
|
||||
val result = ArrayList<ListModel>((size * 1.4).toInt())
|
||||
for (item in this) {
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
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.Gravity
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup.MarginLayoutParams
|
||||
@@ -18,8 +12,6 @@ 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
|
||||
@@ -37,9 +29,11 @@ import coil3.request.lifecycle
|
||||
import coil3.request.placeholder
|
||||
import coil3.request.target
|
||||
import coil3.request.transformations
|
||||
import coil3.size.Precision
|
||||
import coil3.size.Scale
|
||||
import coil3.transform.RoundedCornersTransformation
|
||||
import coil3.util.CoilUtils
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
@@ -55,28 +49,30 @@ import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
import org.koitharu.kotatsu.core.model.LocalMangaSource
|
||||
import org.koitharu.kotatsu.core.model.UnknownMangaSource
|
||||
import org.koitharu.kotatsu.core.model.getTitle
|
||||
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.nav.ReaderIntent
|
||||
import org.koitharu.kotatsu.core.nav.router
|
||||
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.OnContextClickListenerCompat
|
||||
import org.koitharu.kotatsu.core.ui.image.ChipIconTarget
|
||||
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
|
||||
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
|
||||
import org.koitharu.kotatsu.core.ui.image.TextDrawable
|
||||
import org.koitharu.kotatsu.core.ui.image.TextViewTarget
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.sheet.BottomSheetCollapseCallback
|
||||
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.LocaleUtils
|
||||
import org.koitharu.kotatsu.core.util.ext.crossfade
|
||||
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
|
||||
import org.koitharu.kotatsu.core.util.ext.drawable
|
||||
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.ifNullOrEmpty
|
||||
import org.koitharu.kotatsu.core.util.ext.isTextTruncated
|
||||
import org.koitharu.kotatsu.core.util.ext.joinToStringWithLimit
|
||||
@@ -84,41 +80,29 @@ import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
|
||||
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.setNavigationBarTransparentCompat
|
||||
import org.koitharu.kotatsu.core.util.ext.setOnContextClickListenerCompat
|
||||
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
||||
import org.koitharu.kotatsu.databinding.ActivityDetailsBinding
|
||||
import org.koitharu.kotatsu.databinding.LayoutDetailsTableBinding
|
||||
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.dialog.DownloadDialogFragment
|
||||
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.MangaListMapper
|
||||
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.MangaListModel
|
||||
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.MangaListFilter
|
||||
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.stats.ui.sheet.MangaStatsSheet
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.roundToInt
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
@AndroidEntryPoint
|
||||
@@ -140,30 +124,24 @@ class DetailsActivity :
|
||||
|
||||
private val viewModel: DetailsViewModel by viewModels()
|
||||
private lateinit var menuProvider: DetailsMenuProvider
|
||||
private lateinit var infoBinding: LayoutDetailsTableBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(ActivityDetailsBinding.inflate(layoutInflater))
|
||||
infoBinding = LayoutDetailsTableBinding.bind(viewBinding.root)
|
||||
supportActionBar?.run {
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
setDisplayShowTitleEnabled(false)
|
||||
}
|
||||
viewBinding.buttonRead.setOnClickListener(this)
|
||||
viewBinding.buttonRead.setOnLongClickListener(this)
|
||||
viewBinding.buttonRead.setOnContextClickListenerCompat(this)
|
||||
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.chipFavorite.setOnClickListener(this)
|
||||
infoBinding.textViewLocal.setOnClickListener(this)
|
||||
infoBinding.textViewAuthor.setOnClickListener(this)
|
||||
infoBinding.textViewSource.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.swipeRefreshLayout.setOnRefreshListener(this)
|
||||
viewBinding.textViewDescription.viewTreeObserver.addOnDrawListener(this)
|
||||
@@ -172,16 +150,19 @@ class DetailsActivity :
|
||||
TitleScrollCoordinator(viewBinding.textViewTitle).attach(viewBinding.scrollView)
|
||||
viewBinding.containerBottomSheet?.let { sheet ->
|
||||
onBackPressedDispatcher.addCallback(BottomSheetCollapseCallback(sheet))
|
||||
BottomSheetBehavior.from(sheet)
|
||||
.addBottomSheetCallback(DetailsBottomSheetCallback(viewBinding.swipeRefreshLayout))
|
||||
}
|
||||
TitleExpandListener(viewBinding.textViewTitle).attach()
|
||||
|
||||
val appRouter = router
|
||||
viewModel.mangaDetails.filterNotNull().observe(this, ::onMangaUpdated)
|
||||
viewModel.onMangaRemoved.observeEvent(this, ::onMangaRemoved)
|
||||
viewModel.onError
|
||||
.filterNot { ChaptersPagesSheet.isShown(supportFragmentManager) }
|
||||
.filterNot { appRouter.isChapterPagesSheetShown() }
|
||||
.observeEvent(this, DetailsErrorObserver(this, viewModel, exceptionResolver))
|
||||
viewModel.onActionDone
|
||||
.filterNot { ChaptersPagesSheet.isShown(supportFragmentManager) }
|
||||
.filterNot { appRouter.isChapterPagesSheetShown() }
|
||||
.observeEvent(this, ReversibleActionObserver(viewBinding.scrollView, null))
|
||||
combine(viewModel.historyInfo, viewModel.isLoading, ::Pair).observe(this) {
|
||||
onHistoryChanged(it.first, it.second)
|
||||
@@ -190,24 +171,24 @@ class DetailsActivity :
|
||||
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.isStatsAvailable.observe(this, menuInvalidator)
|
||||
viewModel.remoteManga.observe(this, menuInvalidator)
|
||||
viewModel.branches.observe(this) {
|
||||
viewBinding.infoLayout.chipBranch.isVisible = it.size > 1 || !it.firstOrNull()?.name.isNullOrEmpty()
|
||||
viewBinding.infoLayout.chipBranch.isCloseIconVisible = it.size > 1
|
||||
val branch = it.singleOrNull()
|
||||
infoBinding.textViewTranslation.textAndVisible = branch?.name
|
||||
infoBinding.textViewTranslation.drawableStart = branch?.locale?.let {
|
||||
LocaleUtils.getEmojiFlag(it)
|
||||
}?.let {
|
||||
TextDrawable.compound(infoBinding.textViewTranslation, it)
|
||||
}
|
||||
infoBinding.textViewTranslationLabel.isVisible = infoBinding.textViewTranslation.isVisible
|
||||
}
|
||||
viewModel.chapters.observe(this, PrefetchObserver(this))
|
||||
viewModel.onDownloadStarted
|
||||
.filterNot { ChaptersPagesSheet.isShown(supportFragmentManager) }
|
||||
.filterNot { appRouter.isChapterPagesSheetShown() }
|
||||
.observeEvent(this, DownloadStartedObserver(viewBinding.scrollView))
|
||||
|
||||
DownloadDialogFragment.registerCallback(this, viewBinding.scrollView)
|
||||
menuProvider = DetailsMenuProvider(
|
||||
activity = this,
|
||||
viewModel = viewModel,
|
||||
@@ -221,63 +202,32 @@ class DetailsActivity :
|
||||
|
||||
override fun onClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.button_read -> openReader(isIncognitoMode = false)
|
||||
R.id.chip_branch -> showBranchPopupMenu(v)
|
||||
R.id.button_download -> {
|
||||
R.id.textView_author -> {
|
||||
val manga = viewModel.manga.value ?: return
|
||||
DownloadDialogFragment.show(supportFragmentManager, listOf(manga))
|
||||
router.openSearch(manga.source, manga.author ?: return)
|
||||
}
|
||||
|
||||
R.id.chip_author -> {
|
||||
R.id.textView_source -> {
|
||||
val manga = viewModel.manga.value ?: return
|
||||
startActivity(
|
||||
MangaListActivity.newIntent(
|
||||
context = v.context,
|
||||
source = manga.source,
|
||||
filter = MangaListFilter(query = manga.author),
|
||||
),
|
||||
)
|
||||
router.openList(manga.source, null)
|
||||
}
|
||||
|
||||
R.id.chip_source -> {
|
||||
R.id.textView_local -> {
|
||||
val manga = viewModel.manga.value ?: return
|
||||
startActivity(
|
||||
MangaListActivity.newIntent(
|
||||
context = v.context,
|
||||
source = manga.source,
|
||||
filter = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
R.id.chip_size -> {
|
||||
val manga = viewModel.manga.value ?: return
|
||||
LocalInfoDialog.show(supportFragmentManager, manga)
|
||||
router.showLocalInfoDialog(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
|
||||
}
|
||||
router.showFavoriteDialog(manga)
|
||||
}
|
||||
|
||||
R.id.imageView_cover -> {
|
||||
val manga = viewModel.manga.value ?: return
|
||||
startActivity(
|
||||
ImageActivity.newIntent(
|
||||
v.context,
|
||||
manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl },
|
||||
manga.source,
|
||||
),
|
||||
scaleUpActivityOptionsOf(v),
|
||||
router.openImage(
|
||||
url = manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl },
|
||||
source = manga.source,
|
||||
anchor = v,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -293,12 +243,12 @@ class DetailsActivity :
|
||||
|
||||
R.id.button_scrobbling_more -> {
|
||||
val manga = viewModel.manga.value ?: return
|
||||
ScrobblingSelectorSheet.show(supportFragmentManager, manga, null)
|
||||
router.showScrobblingSelectorSheet(manga, null)
|
||||
}
|
||||
|
||||
R.id.button_related_more -> {
|
||||
val manga = viewModel.manga.value ?: return
|
||||
startActivity(RelatedMangaActivity.newIntent(v.context, manga))
|
||||
router.openRelated(manga)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -306,7 +256,7 @@ class DetailsActivity :
|
||||
override fun onChipClick(chip: Chip, data: Any?) {
|
||||
val tag = data as? MangaTag ?: return
|
||||
// TODO dialog
|
||||
startActivity(MangaListActivity.newIntent(this, tag.source, MangaListFilter(tags = setOf(tag))))
|
||||
router.openList(tag)
|
||||
}
|
||||
|
||||
override fun onContextClick(v: View): Boolean = onLongClick(v)
|
||||
@@ -344,9 +294,7 @@ class DetailsActivity :
|
||||
}
|
||||
|
||||
override fun onItemClick(item: Bookmark, view: View) {
|
||||
startActivity(
|
||||
ReaderActivity.IntentBuilder(view.context).bookmark(item).incognito(true).build(),
|
||||
)
|
||||
router.openReader(ReaderIntent.Builder(view.context).bookmark(item).incognito(true).build())
|
||||
Toast.makeText(view.context, R.string.incognito_mode, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
@@ -378,7 +326,7 @@ class DetailsActivity :
|
||||
}
|
||||
|
||||
private fun onFavoritesChanged(categories: Set<FavouriteCategory>) {
|
||||
val chip = viewBinding.infoLayout.chipFavorite
|
||||
val chip = viewBinding.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)
|
||||
@@ -387,18 +335,14 @@ class DetailsActivity :
|
||||
}
|
||||
}
|
||||
|
||||
private fun onReadingTimeChanged(time: ReadingTime?) {
|
||||
val chip = viewBinding.infoLayout.chipTime
|
||||
chip.textAndVisible = time?.formatShort(chip.resources)
|
||||
}
|
||||
|
||||
private fun onLocalSizeChanged(size: Long) {
|
||||
val chip = viewBinding.infoLayout.chipSize
|
||||
if (size == 0L) {
|
||||
chip.isVisible = false
|
||||
infoBinding.textViewLocal.isVisible = false
|
||||
infoBinding.textViewLocalLabel.isVisible = false
|
||||
} else {
|
||||
chip.text = FileSize.BYTES.format(chip.context, size)
|
||||
chip.isVisible = true
|
||||
infoBinding.textViewLocal.text = FileSize.BYTES.format(this, size)
|
||||
infoBinding.textViewLocal.isVisible = true
|
||||
infoBinding.textViewLocalLabel.isVisible = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -417,7 +361,7 @@ class DetailsActivity :
|
||||
coil, this,
|
||||
StaticItemSizeResolver(resources.getDimensionPixelSize(R.dimen.smaller_grid_width)),
|
||||
) { item, view ->
|
||||
startActivity(newIntent(view.context, item))
|
||||
router.openDetails(item)
|
||||
},
|
||||
).also { rv.adapter = it }
|
||||
adapter.items = related
|
||||
@@ -434,7 +378,7 @@ class DetailsActivity :
|
||||
if (adapter != null) {
|
||||
adapter.items = scrobblings
|
||||
} else {
|
||||
adapter = ScrollingInfoAdapter(this, coil, supportFragmentManager)
|
||||
adapter = ScrollingInfoAdapter(this, coil, router)
|
||||
adapter.items = scrobblings
|
||||
viewBinding.recyclerViewScrobbling.adapter = adapter
|
||||
viewBinding.recyclerViewScrobbling.addItemDecoration(ScrobblingItemDecoration())
|
||||
@@ -442,62 +386,59 @@ class DetailsActivity :
|
||||
}
|
||||
|
||||
private fun onMangaUpdated(details: MangaDetails) {
|
||||
val manga = details.toManga()
|
||||
loadCover(manga)
|
||||
with(viewBinding) {
|
||||
val manga = details.toManga()
|
||||
// Main
|
||||
loadCover(manga)
|
||||
textViewTitle.text = manga.title
|
||||
textViewSubtitle.textAndVisible = manga.altTitle
|
||||
infoLayout.chipAuthor.textAndVisible = manga.author?.ellipsize(AUTHOR_LABEL_LIMIT)
|
||||
textViewNsfw.isVisible = manga.isNsfw
|
||||
textViewDescription.text = details.description.ifNullOrEmpty { getString(R.string.no_description) }
|
||||
}
|
||||
with(infoBinding) {
|
||||
textViewAuthor.textAndVisible = manga.author
|
||||
textViewAuthorLabel.isVisible = textViewAuthor.isVisible
|
||||
if (manga.hasRating) {
|
||||
ratingBar.rating = manga.rating * ratingBar.numStars
|
||||
ratingBar.isVisible = true
|
||||
ratingBarRating.rating = manga.rating * ratingBarRating.numStars
|
||||
ratingBarRating.isVisible = true
|
||||
textViewRatingLabel.isVisible = true
|
||||
} else {
|
||||
ratingBar.isVisible = false
|
||||
ratingBarRating.isVisible = false
|
||||
textViewRatingLabel.isVisible = false
|
||||
}
|
||||
|
||||
manga.state?.let { state ->
|
||||
textViewState.textAndVisible = resources.getString(state.titleResId)
|
||||
imageViewState.setImageResource(state.iconResId)
|
||||
imageViewState.isVisible = true
|
||||
textViewStateLabel.isVisible = textViewState.isVisible
|
||||
} ?: run {
|
||||
textViewState.isVisible = false
|
||||
imageViewState.isVisible = false
|
||||
textViewStateLabel.isVisible = false
|
||||
}
|
||||
|
||||
if (manga.source == LocalMangaSource || manga.source == UnknownMangaSource) {
|
||||
infoLayout.chipSource.isVisible = false
|
||||
textViewSource.isVisible = false
|
||||
textViewSourceLabel.isVisible = false
|
||||
} else {
|
||||
infoLayout.chipSource.text = manga.source.getTitle(this@DetailsActivity)
|
||||
infoLayout.chipSource.isVisible = true
|
||||
textViewSource.textAndVisible = manga.source.getTitle(this@DetailsActivity)
|
||||
textViewSourceLabel.isVisible = textViewSource.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)
|
||||
.mangaSourceExtra(manga.source)
|
||||
.transformations(RoundedCornersTransformation(resources.getDimension(R.dimen.chip_icon_corner)))
|
||||
.allowRgb565(true)
|
||||
.enqueueWith(coil)
|
||||
}
|
||||
|
||||
title = manga.title
|
||||
invalidateOptionsMenu()
|
||||
val faviconPlaceholderFactory = FaviconDrawable.Factory(R.style.FaviconDrawable_Chip)
|
||||
ImageRequest.Builder(this@DetailsActivity)
|
||||
.data(manga.source.faviconUri())
|
||||
.lifecycle(this@DetailsActivity)
|
||||
.crossfade(false)
|
||||
.precision(Precision.EXACT)
|
||||
.size(resources.getDimensionPixelSize(materialR.dimen.m3_chip_icon_size))
|
||||
.target(TextViewTarget(textViewSource, Gravity.START))
|
||||
.placeholder(faviconPlaceholderFactory)
|
||||
.error(faviconPlaceholderFactory)
|
||||
.fallback(faviconPlaceholderFactory)
|
||||
.mangaSourceExtra(manga.source)
|
||||
.transformations(RoundedCornersTransformation(resources.getDimension(R.dimen.chip_icon_corner)))
|
||||
.allowRgb565(true)
|
||||
.enqueueWith(coil)
|
||||
}
|
||||
bindTags(manga)
|
||||
title = manga.title
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
private fun onMangaRemoved(manga: Manga) {
|
||||
@@ -527,69 +468,32 @@ class DetailsActivity :
|
||||
}
|
||||
}
|
||||
|
||||
private fun onHistoryChanged(info: HistoryInfo, isLoading: Boolean) = with(viewBinding) {
|
||||
buttonRead.setTitle(if (info.canContinue) R.string._continue else R.string.read)
|
||||
buttonRead.subtitle = when {
|
||||
private fun onHistoryChanged(info: HistoryInfo, isLoading: Boolean) = with(infoBinding) {
|
||||
textViewChapters.text = 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.currentChapter >= 0 -> getString(
|
||||
R.string.chapter_d_of_d,
|
||||
info.currentChapter + 1,
|
||||
info.totalChapters,
|
||||
).withEstimatedTime(info.estimatedTime)
|
||||
|
||||
info.totalChapters == 0 -> getString(R.string.no_chapters)
|
||||
info.totalChapters == -1 -> getString(R.string.error_occurred)
|
||||
else -> resources.getQuantityString(R.plurals.chapters, info.totalChapters, info.totalChapters)
|
||||
.withEstimatedTime(info.estimatedTime)
|
||||
}
|
||||
val isFirstCall = buttonRead.tag == null
|
||||
buttonRead.tag = Unit
|
||||
buttonRead.setProgress(info.percent.coerceIn(0f, 1f), !isFirstCall)
|
||||
buttonDownload?.isEnabled = info.isValid && info.canDownload
|
||||
buttonRead.isEnabled = info.isValid
|
||||
}
|
||||
|
||||
private fun showBranchPopupMenu(v: View) {
|
||||
val branches = viewModel.branches.value
|
||||
if (branches.size <= 1) {
|
||||
return
|
||||
textViewProgress.textAndVisible = if (info.percent <= 0f) {
|
||||
null
|
||||
} else {
|
||||
getString(R.string.percent_string_pattern, (info.percent * 100f).toInt().toString())
|
||||
}
|
||||
val menu = PopupMenu(v.context, v)
|
||||
for ((i, branch) in branches.withIndex()) {
|
||||
val title = buildSpannedString {
|
||||
if (branch.isCurrent) {
|
||||
inSpans(
|
||||
ImageSpan(
|
||||
this@DetailsActivity,
|
||||
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()
|
||||
progress.setProgressCompat(
|
||||
(progress.max * info.percent.coerceIn(0f, 1f)).roundToInt(),
|
||||
true,
|
||||
)
|
||||
textViewProgressLabel.isVisible = info.history != null
|
||||
textViewProgress.isVisible = info.history != null
|
||||
progress.isVisible = info.history != null
|
||||
}
|
||||
|
||||
private fun openReader(isIncognitoMode: Boolean) {
|
||||
@@ -598,8 +502,8 @@ class DetailsActivity :
|
||||
Snackbar.make(viewBinding.scrollView, R.string.chapter_is_missing, Snackbar.LENGTH_SHORT)
|
||||
.show()
|
||||
} else {
|
||||
startActivity(
|
||||
ReaderActivity.IntentBuilder(this)
|
||||
router.openReader(
|
||||
ReaderIntent.Builder(this)
|
||||
.manga(manga)
|
||||
.branch(viewModel.selectedBranchValue)
|
||||
.incognito(isIncognitoMode)
|
||||
@@ -642,6 +546,14 @@ class DetailsActivity :
|
||||
request.enqueueWith(coil)
|
||||
}
|
||||
|
||||
private fun String.withEstimatedTime(time: ReadingTime?): String {
|
||||
if (time == null) {
|
||||
return this
|
||||
}
|
||||
val timeFormatted = time.formatShort(resources)
|
||||
return getString(R.string.chapters_time_pattern, this, timeFormatted)
|
||||
}
|
||||
|
||||
private class PrefetchObserver(
|
||||
private val context: Context,
|
||||
) : FlowCollector<List<ChapterListItem>?> {
|
||||
@@ -663,16 +575,5 @@ class DetailsActivity :
|
||||
companion object {
|
||||
|
||||
private const val FAV_LABEL_LIMIT = 16
|
||||
private const val AUTHOR_LABEL_LIMIT = 16
|
||||
|
||||
fun newIntent(context: Context, manga: Manga): Intent {
|
||||
return Intent(context, DetailsActivity::class.java)
|
||||
.putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga))
|
||||
}
|
||||
|
||||
fun newIntent(context: Context, mangaId: Long): Intent {
|
||||
return Intent(context, DetailsActivity::class.java)
|
||||
.putExtra(MangaIntent.KEY_ID, mangaId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.koitharu.kotatsu.details.ui
|
||||
|
||||
import android.view.View
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
|
||||
class DetailsBottomSheetCallback(
|
||||
private val swipeRefreshLayout: SwipeRefreshLayout,
|
||||
) : BottomSheetBehavior.BottomSheetCallback() {
|
||||
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
swipeRefreshLayout.isEnabled = newState == BottomSheetBehavior.STATE_COLLAPSED
|
||||
}
|
||||
|
||||
override fun onSlide(bottomSheet: View, slideOffset: Float) = Unit
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.UnsupportedSourceException
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ErrorObserver
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.core.util.ext.isNetworkError
|
||||
import org.koitharu.kotatsu.core.util.ext.isSerializable
|
||||
@@ -38,10 +37,10 @@ class DetailsErrorObserver(
|
||||
}
|
||||
|
||||
value is ParseException -> {
|
||||
val fm = fragmentManager
|
||||
if (fm != null && value.isSerializable()) {
|
||||
val router = router()
|
||||
if (router != null && value.isSerializable()) {
|
||||
snackbar.setAction(R.string.details) {
|
||||
ErrorDetailsDialog.show(fm, value, value.url)
|
||||
router.showErrorDialog(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,16 +14,11 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.alternatives.ui.AlternativesActivity
|
||||
import org.koitharu.kotatsu.browser.BrowserActivity
|
||||
import org.koitharu.kotatsu.core.model.LocalMangaSource
|
||||
import org.koitharu.kotatsu.core.model.isLocal
|
||||
import org.koitharu.kotatsu.core.nav.router
|
||||
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
||||
import org.koitharu.kotatsu.core.util.ShareHelper
|
||||
import org.koitharu.kotatsu.download.ui.dialog.DownloadDialogFragment
|
||||
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet
|
||||
import org.koitharu.kotatsu.search.ui.multi.SearchActivity
|
||||
import org.koitharu.kotatsu.stats.ui.sheet.MangaStatsSheet
|
||||
|
||||
class DetailsMenuProvider(
|
||||
private val activity: FragmentActivity,
|
||||
@@ -49,23 +44,21 @@ class DetailsMenuProvider(
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
val manga = viewModel.getMangaOrNull() ?: return false
|
||||
when (menuItem.itemId) {
|
||||
R.id.action_share -> {
|
||||
viewModel.manga.value?.let {
|
||||
val shareHelper = ShareHelper(activity)
|
||||
if (it.isLocal) {
|
||||
shareHelper.shareCbz(listOf(it.url.toUri().toFile()))
|
||||
} else {
|
||||
shareHelper.shareMangaLink(it)
|
||||
}
|
||||
val shareHelper = ShareHelper(activity)
|
||||
if (manga.isLocal) {
|
||||
shareHelper.shareCbz(listOf(manga.url.toUri().toFile()))
|
||||
} else {
|
||||
shareHelper.shareMangaLink(manga)
|
||||
}
|
||||
}
|
||||
|
||||
R.id.action_delete -> {
|
||||
val title = viewModel.manga.value?.title.orEmpty()
|
||||
MaterialAlertDialogBuilder(activity)
|
||||
.setTitle(R.string.delete_manga)
|
||||
.setMessage(activity.getString(R.string.text_delete_local_manga, title))
|
||||
.setMessage(activity.getString(R.string.text_delete_local_manga, manga.title))
|
||||
.setPositiveButton(R.string.delete) { _, _ ->
|
||||
viewModel.deleteLocal()
|
||||
}
|
||||
@@ -74,52 +67,38 @@ class DetailsMenuProvider(
|
||||
}
|
||||
|
||||
R.id.action_save -> {
|
||||
DownloadDialogFragment.show(activity.supportFragmentManager, listOfNotNull(viewModel.manga.value))
|
||||
activity.router.showDownloadDialog(manga, snackbarHost)
|
||||
}
|
||||
|
||||
R.id.action_browser -> {
|
||||
viewModel.manga.value?.let {
|
||||
activity.startActivity(BrowserActivity.newIntent(activity, it.publicUrl, it.source, it.title))
|
||||
}
|
||||
activity.router.openBrowser(url = manga.publicUrl, source = manga.source, title = manga.title)
|
||||
}
|
||||
|
||||
R.id.action_online -> {
|
||||
viewModel.remoteManga.value?.let {
|
||||
activity.startActivity(DetailsActivity.newIntent(activity, it))
|
||||
}
|
||||
activity.router.openDetails(manga)
|
||||
}
|
||||
|
||||
R.id.action_related -> {
|
||||
viewModel.manga.value?.let {
|
||||
activity.startActivity(SearchActivity.newIntent(activity, it.title))
|
||||
}
|
||||
activity.router.openSearch(manga.title)
|
||||
}
|
||||
|
||||
R.id.action_alternatives -> {
|
||||
viewModel.manga.value?.let {
|
||||
activity.startActivity(AlternativesActivity.newIntent(activity, it))
|
||||
}
|
||||
activity.router.openAlternatives(manga)
|
||||
}
|
||||
|
||||
R.id.action_stats -> {
|
||||
viewModel.manga.value?.let {
|
||||
MangaStatsSheet.show(activity.supportFragmentManager, it)
|
||||
}
|
||||
activity.router.showStatisticSheet(manga)
|
||||
}
|
||||
|
||||
R.id.action_scrobbling -> {
|
||||
viewModel.manga.value?.let {
|
||||
ScrobblingSelectorSheet.show(activity.supportFragmentManager, it, null)
|
||||
}
|
||||
activity.router.showScrobblingSelectorSheet(manga, null)
|
||||
}
|
||||
|
||||
R.id.action_shortcut -> {
|
||||
viewModel.manga.value?.let {
|
||||
activity.lifecycleScope.launch {
|
||||
if (!appShortcutManager.requestPinShortcut(it)) {
|
||||
Snackbar.make(snackbarHost, R.string.operation_not_supported, Snackbar.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
activity.lifecycleScope.launch {
|
||||
if (!appShortcutManager.requestPinShortcut(manga)) {
|
||||
Snackbar.make(snackbarHost, R.string.operation_not_supported, Snackbar.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
|
||||
import org.koitharu.kotatsu.core.model.getPreferredBranch
|
||||
import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||
import org.koitharu.kotatsu.core.nav.MangaIntent
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
||||
@@ -112,12 +112,14 @@ class DetailsViewModel @Inject constructor(
|
||||
history,
|
||||
interactor.observeIncognitoMode(manga),
|
||||
) { m, b, h, im ->
|
||||
HistoryInfo(m, b, h, im)
|
||||
}.stateIn(
|
||||
scope = viewModelScope + Dispatchers.Default,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = HistoryInfo(null, null, null, false),
|
||||
)
|
||||
val estimatedTime = readingTimeUseCase.invoke(m, b, h)
|
||||
HistoryInfo(m, b, h, im, estimatedTime)
|
||||
}.withErrorHandling()
|
||||
.stateIn(
|
||||
scope = viewModelScope + Dispatchers.Default,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = HistoryInfo(null, null, null, false, null),
|
||||
)
|
||||
|
||||
val localSize = mangaDetails
|
||||
.map { it?.local }
|
||||
@@ -170,14 +172,6 @@ class DetailsViewModel @Inject constructor(
|
||||
}.sortedWith(BranchComparator())
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
val readingTime = combine(
|
||||
mangaDetails,
|
||||
selectedBranch,
|
||||
history,
|
||||
) { m, b, h ->
|
||||
readingTimeUseCase.invoke(m, b, h)
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, null)
|
||||
|
||||
val selectedBranchValue: String?
|
||||
get() = selectedBranch.value
|
||||
|
||||
@@ -222,14 +216,6 @@ class DetailsViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun startChaptersSelection() {
|
||||
val chapters = chapters.value
|
||||
val chapter = chapters.find {
|
||||
it.isUnread && !it.isDownloaded
|
||||
} ?: chapters.firstOrNull() ?: return
|
||||
onSelectChapter.call(chapter.chapter.id)
|
||||
}
|
||||
|
||||
fun removeFromHistory() {
|
||||
launchJob(Dispatchers.Default) {
|
||||
val handle = historyRepository.delete(setOf(mangaId))
|
||||
@@ -240,14 +226,15 @@ class DetailsViewModel @Inject constructor(
|
||||
private fun doLoad() = launchLoadingJob(Dispatchers.Default) {
|
||||
detailsLoadUseCase.invoke(intent)
|
||||
.onEachWhile {
|
||||
if (it.allChapters.isEmpty()) {
|
||||
return@onEachWhile false
|
||||
if (it.allChapters.isNotEmpty()) {
|
||||
val manga = it.toManga()
|
||||
// find default branch
|
||||
val hist = historyRepository.getOne(manga)
|
||||
selectedBranch.value = manga.getPreferredBranch(hist)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
val manga = it.toManga()
|
||||
// find default branch
|
||||
val hist = historyRepository.getOne(manga)
|
||||
selectedBranch.value = manga.getPreferredBranch(hist)
|
||||
true
|
||||
}.collect {
|
||||
mangaDetails.value = it
|
||||
}
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
package org.koitharu.kotatsu.details.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.text.style.DynamicDrawableSpan
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.ImageSpan
|
||||
import android.text.style.RelativeSizeSpan
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.text.buildSpannedString
|
||||
import androidx.core.text.inSpans
|
||||
import androidx.core.view.MenuCompat
|
||||
import androidx.core.view.get
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.button.MaterialSplitButton
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.isLocal
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.nav.ReaderIntent
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.details.ui.model.HistoryInfo
|
||||
|
||||
class ReadButtonDelegate(
|
||||
private val splitButton: MaterialSplitButton,
|
||||
private val viewModel: DetailsViewModel,
|
||||
private val router: AppRouter,
|
||||
) : View.OnClickListener, PopupMenu.OnMenuItemClickListener, PopupMenu.OnDismissListener {
|
||||
|
||||
private val buttonRead = splitButton[0] as MaterialButton
|
||||
private val buttonMenu = splitButton[1] as MaterialButton
|
||||
|
||||
private val context: Context
|
||||
get() = buttonRead.context
|
||||
|
||||
override fun onClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.button_read -> openReader(isIncognitoMode = false)
|
||||
R.id.button_read_menu -> showMenu()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_incognito -> openReader(isIncognitoMode = true)
|
||||
R.id.action_forget -> viewModel.removeFromHistory()
|
||||
R.id.action_download -> {
|
||||
router.showDownloadDialog(
|
||||
manga = setOf(viewModel.getMangaOrNull() ?: return false),
|
||||
snackbarHost = splitButton,
|
||||
)
|
||||
}
|
||||
|
||||
Menu.NONE -> {
|
||||
val branch = viewModel.branches.value.getOrNull(item.order) ?: return false
|
||||
viewModel.setSelectedBranch(branch.name)
|
||||
}
|
||||
|
||||
else -> return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDismiss(menu: PopupMenu?) {
|
||||
buttonMenu.isChecked = false
|
||||
}
|
||||
|
||||
fun attach(lifecycleOwner: LifecycleOwner) {
|
||||
buttonRead.setOnClickListener(this)
|
||||
buttonMenu.setOnClickListener(this)
|
||||
combine(viewModel.isLoading, viewModel.historyInfo, ::Pair)
|
||||
.observe(lifecycleOwner) { (isLoading, historyInfo) ->
|
||||
onHistoryChanged(isLoading, historyInfo)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showMenu() {
|
||||
val menu = PopupMenu(context, buttonMenu)
|
||||
menu.inflate(R.menu.popup_read)
|
||||
prepareMenu(menu.menu)
|
||||
menu.setOnMenuItemClickListener(this)
|
||||
menu.setForceShowIcon(true)
|
||||
menu.setOnDismissListener(this)
|
||||
buttonMenu.isChecked = true
|
||||
menu.show()
|
||||
}
|
||||
|
||||
private fun prepareMenu(menu: Menu) {
|
||||
MenuCompat.setGroupDividerEnabled(menu, true)
|
||||
menu.populateBranchList()
|
||||
val history = viewModel.historyInfo.value
|
||||
menu.findItem(R.id.action_incognito)?.isVisible = !history.isIncognitoMode
|
||||
menu.findItem(R.id.action_forget)?.isVisible = history.history != null
|
||||
menu.findItem(R.id.action_download)?.isVisible = viewModel.getMangaOrNull()?.isLocal == false
|
||||
}
|
||||
|
||||
private fun openReader(isIncognitoMode: Boolean) {
|
||||
val manga = viewModel.manga.value ?: return
|
||||
if (viewModel.historyInfo.value.isChapterMissing) {
|
||||
Snackbar.make(buttonRead, R.string.chapter_is_missing, Snackbar.LENGTH_SHORT)
|
||||
.show() // TODO
|
||||
} else {
|
||||
router.openReader(
|
||||
ReaderIntent.Builder(context)
|
||||
.manga(manga)
|
||||
.branch(viewModel.selectedBranchValue)
|
||||
.incognito(isIncognitoMode)
|
||||
.build(),
|
||||
)
|
||||
if (isIncognitoMode) {
|
||||
Toast.makeText(context, R.string.incognito_mode, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onHistoryChanged(isLoading: Boolean, info: HistoryInfo) {
|
||||
buttonRead.setText(
|
||||
when {
|
||||
isLoading -> R.string.loading_
|
||||
info.isIncognitoMode -> R.string.incognito
|
||||
info.canContinue -> R.string._continue
|
||||
else -> R.string.read
|
||||
},
|
||||
)
|
||||
splitButton.isEnabled = !isLoading && info.isValid
|
||||
}
|
||||
|
||||
private fun Menu.populateBranchList() {
|
||||
val branches = viewModel.branches.value
|
||||
if (branches.size <= 1) {
|
||||
return
|
||||
}
|
||||
for ((i, branch) in branches.withIndex()) {
|
||||
val title = buildSpannedString {
|
||||
if (branch.isCurrent) {
|
||||
inSpans(
|
||||
ImageSpan(
|
||||
context,
|
||||
R.drawable.ic_current_chapter,
|
||||
DynamicDrawableSpan.ALIGN_BASELINE,
|
||||
),
|
||||
) {
|
||||
append(' ')
|
||||
}
|
||||
append(' ')
|
||||
}
|
||||
append(branch.name ?: context.getString(R.string.system_default))
|
||||
append(' ')
|
||||
append(' ')
|
||||
inSpans(
|
||||
ForegroundColorSpan(
|
||||
context.getThemeColor(
|
||||
android.R.attr.textColorSecondary,
|
||||
Color.LTGRAY,
|
||||
),
|
||||
),
|
||||
RelativeSizeSpan(0.74f),
|
||||
) {
|
||||
append(branch.count.toString())
|
||||
}
|
||||
}
|
||||
val item = add(R.id.group_branches, Menu.NONE, i, title)
|
||||
item.isCheckable = true
|
||||
item.isChecked = branch.isSelected
|
||||
}
|
||||
setGroupCheckable(R.id.group_branches, true, true)
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.details.ui.model
|
||||
|
||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||
import org.koitharu.kotatsu.details.data.MangaDetails
|
||||
import org.koitharu.kotatsu.details.data.ReadingTime
|
||||
|
||||
data class HistoryInfo(
|
||||
val totalChapters: Int,
|
||||
@@ -10,6 +11,7 @@ data class HistoryInfo(
|
||||
val isIncognitoMode: Boolean,
|
||||
val isChapterMissing: Boolean,
|
||||
val canDownload: Boolean,
|
||||
val estimatedTime: ReadingTime?,
|
||||
) {
|
||||
val isValid: Boolean
|
||||
get() = totalChapters >= 0
|
||||
@@ -29,7 +31,8 @@ fun HistoryInfo(
|
||||
manga: MangaDetails?,
|
||||
branch: String?,
|
||||
history: MangaHistory?,
|
||||
isIncognitoMode: Boolean
|
||||
isIncognitoMode: Boolean,
|
||||
estimatedTime: ReadingTime?,
|
||||
): HistoryInfo {
|
||||
val chapters = if (manga?.chapters?.isEmpty() == true) {
|
||||
emptyList()
|
||||
@@ -48,5 +51,6 @@ fun HistoryInfo(
|
||||
isIncognitoMode = isIncognitoMode,
|
||||
isChapterMissing = history != null && manga?.isLoaded == true && manga.allChapters.none { it.id == history.chapterId },
|
||||
canDownload = manga?.isLocal == false,
|
||||
estimatedTime = estimatedTime,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.details.ui.model
|
||||
|
||||
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import java.util.Locale
|
||||
|
||||
data class MangaBranch(
|
||||
val name: String?,
|
||||
@@ -10,6 +11,8 @@ data class MangaBranch(
|
||||
val isCurrent: Boolean,
|
||||
) : ListModel {
|
||||
|
||||
val locale: Locale? by lazy(::findAppropriateLocale)
|
||||
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
return other is MangaBranch && other.name == name
|
||||
}
|
||||
@@ -25,4 +28,16 @@ data class MangaBranch(
|
||||
override fun toString(): String {
|
||||
return "$name: $count"
|
||||
}
|
||||
|
||||
private fun findAppropriateLocale(): Locale? {
|
||||
if (name.isNullOrEmpty()) {
|
||||
return null
|
||||
}
|
||||
return Locale.getAvailableLocales().find { lc ->
|
||||
name.contains(lc.getDisplayName(lc), ignoreCase = true) ||
|
||||
name.contains(lc.getDisplayName(Locale.ENGLISH), ignoreCase = true) ||
|
||||
name.contains(lc.getDisplayLanguage(lc), ignoreCase = true) ||
|
||||
name.contains(lc.getDisplayLanguage(Locale.ENGLISH), ignoreCase = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,13 +25,21 @@ class ChaptersPagesAdapter(
|
||||
}
|
||||
|
||||
override fun onConfigureTab(tab: TabLayout.Tab, position: Int) {
|
||||
tab.setText(
|
||||
tab.setIcon(
|
||||
when (position) {
|
||||
0 -> R.string.chapters
|
||||
1 -> if (isPagesTabEnabled) R.string.pages else R.string.bookmarks
|
||||
2 -> R.string.bookmarks
|
||||
0 -> R.drawable.ic_list
|
||||
1 -> if (isPagesTabEnabled) R.drawable.ic_grid else R.drawable.ic_bookmark
|
||||
2 -> R.drawable.ic_bookmark
|
||||
else -> 0
|
||||
},
|
||||
)
|
||||
// tab.setText(
|
||||
// when (position) {
|
||||
// 0 -> R.string.chapters
|
||||
// 1 -> if (isPagesTabEnabled) R.string.pages else R.string.bookmarks
|
||||
// 2 -> R.string.bookmarks
|
||||
// else -> 0
|
||||
// },
|
||||
// )
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,11 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.FragmentManager
|
||||
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.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.nav.router
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetBehavior.Companion.STATE_COLLAPSED
|
||||
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetBehavior.Companion.STATE_DRAGGING
|
||||
@@ -26,9 +27,9 @@ 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.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.DetailsViewModel
|
||||
import org.koitharu.kotatsu.details.ui.ReadButtonDelegate
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -49,11 +50,14 @@ class ChaptersPagesSheet : BaseAdaptiveSheet<SheetChaptersPagesBinding>(), Actio
|
||||
disableFitToContents()
|
||||
|
||||
val args = arguments ?: Bundle.EMPTY
|
||||
var defaultTab = args.getInt(ARG_TAB, settings.defaultDetailsTab)
|
||||
var defaultTab = args.getInt(AppRouter.KEY_TAB, settings.defaultDetailsTab)
|
||||
val adapter = ChaptersPagesAdapter(this, settings.isPagesTabEnabled)
|
||||
if (!adapter.isPagesTabEnabled) {
|
||||
defaultTab = (defaultTab - 1).coerceAtLeast(TAB_CHAPTERS)
|
||||
}
|
||||
(viewModel as? DetailsViewModel)?.let { dvm ->
|
||||
ReadButtonDelegate(binding.splitButtonRead, dvm, router).attach(viewLifecycleOwner)
|
||||
}
|
||||
binding.pager.offscreenPageLimit = adapter.itemCount
|
||||
binding.pager.recyclerView?.isNestedScrollingEnabled = false
|
||||
binding.pager.adapter = adapter
|
||||
@@ -87,7 +91,9 @@ class ChaptersPagesSheet : BaseAdaptiveSheet<SheetChaptersPagesBinding>(), Actio
|
||||
}
|
||||
val binding = viewBinding ?: return
|
||||
val isActionModeStarted = actionModeDelegate?.isActionModeStarted == true
|
||||
binding.toolbar.menuView?.isVisible = newState != STATE_COLLAPSED && !isActionModeStarted
|
||||
binding.toolbar.menuView?.isVisible = newState == STATE_EXPANDED && !isActionModeStarted
|
||||
binding.splitButtonRead.isVisible = newState != STATE_EXPANDED && !isActionModeStarted
|
||||
&& viewModel is DetailsViewModel
|
||||
}
|
||||
|
||||
override fun onActionModeStarted(mode: ActionMode) {
|
||||
@@ -138,22 +144,5 @@ class ChaptersPagesSheet : BaseAdaptiveSheet<SheetChaptersPagesBinding>(), Actio
|
||||
const val TAB_CHAPTERS = 0
|
||||
const val TAB_PAGES = 1
|
||||
const val TAB_BOOKMARKS = 2
|
||||
private const val ARG_TAB = "tag"
|
||||
private const val TAG = "ChaptersPagesSheet"
|
||||
|
||||
fun show(fm: FragmentManager) {
|
||||
ChaptersPagesSheet().showDistinct(fm, TAG)
|
||||
}
|
||||
|
||||
fun show(fm: FragmentManager, defaultTab: Int) {
|
||||
ChaptersPagesSheet().withArgs(1) {
|
||||
putInt(ARG_TAB, defaultTab)
|
||||
}.showDistinct(fm, TAG)
|
||||
}
|
||||
|
||||
fun isShown(fm: FragmentManager): Boolean {
|
||||
val sheet = fm.findFragmentByTag(TAG) as? ChaptersPagesSheet
|
||||
return sheet?.dialog?.isShowing == true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,14 +19,17 @@ import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.plus
|
||||
import okio.FileNotFoundException
|
||||
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
|
||||
import org.koitharu.kotatsu.core.model.toChipModel
|
||||
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.core.ui.util.ReversibleAction
|
||||
import org.koitharu.kotatsu.core.util.LocaleStringComparator
|
||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
import org.koitharu.kotatsu.core.util.ext.combine
|
||||
import org.koitharu.kotatsu.core.util.ext.requireValue
|
||||
import org.koitharu.kotatsu.core.util.ext.sortedWithSafe
|
||||
import org.koitharu.kotatsu.details.data.MangaDetails
|
||||
import org.koitharu.kotatsu.details.domain.DetailsInteractor
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
@@ -36,6 +39,7 @@ import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadTask
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||
import org.koitharu.kotatsu.list.domain.ListFilterOption
|
||||
import org.koitharu.kotatsu.local.domain.DeleteLocalMangaUseCase
|
||||
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
@@ -57,7 +61,6 @@ abstract class ChaptersPagesViewModel(
|
||||
val readingState = MutableStateFlow<ReaderState?>(null)
|
||||
|
||||
val onActionDone = MutableEventFlow<ReversibleAction>()
|
||||
val onSelectChapter = MutableEventFlow<Long>()
|
||||
val onDownloadStarted = MutableEventFlow<Unit>()
|
||||
val onMangaRemoved = MutableEventFlow<Manga>()
|
||||
|
||||
@@ -119,6 +122,18 @@ abstract class ChaptersPagesViewModel(
|
||||
(if (reversed) list.asReversed() else list).filterSearch(query)
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
val quickFilter = combine(
|
||||
mangaDetails,
|
||||
selectedBranch,
|
||||
) { details, branch ->
|
||||
val branches = details?.chapters?.keys?.sortedWithSafe(LocaleStringComparator()).orEmpty()
|
||||
if (branches.size > 1) {
|
||||
branches.map { ListFilterOption.Branch(it).toChipModel(it == branch) }
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
launchJob(Dispatchers.Default) {
|
||||
localStorageChanges
|
||||
|
||||
@@ -18,13 +18,15 @@ import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.bookmarks.ui.BookmarksSelectionDecoration
|
||||
import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksAdapter
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||
import org.koitharu.kotatsu.core.nav.ReaderIntent
|
||||
import org.koitharu.kotatsu.core.nav.dismissParentDialog
|
||||
import org.koitharu.kotatsu.core.nav.router
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.util.PagerNestedScrollHelper
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
||||
import org.koitharu.kotatsu.core.util.ext.dismissParentDialog
|
||||
import org.koitharu.kotatsu.core.util.ext.findAppCompatDelegate
|
||||
import org.koitharu.kotatsu.core.util.ext.findParentCallback
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
@@ -34,7 +36,6 @@ import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesViewModel
|
||||
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 javax.inject.Inject
|
||||
|
||||
@@ -124,21 +125,21 @@ class BookmarksFragment : BaseFragment<FragmentMangaBookmarksBinding>(),
|
||||
if (listener != null && listener.onBookmarkSelected(item)) {
|
||||
dismissParentDialog()
|
||||
} else {
|
||||
val intent = IntentBuilder(view.context)
|
||||
val intent = ReaderIntent.Builder(view.context)
|
||||
.manga(activityViewModel.getMangaOrNull() ?: return)
|
||||
.bookmark(item)
|
||||
.incognito(true)
|
||||
.build()
|
||||
startActivity(intent)
|
||||
router.openReader(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemLongClick(item: Bookmark, view: View): Boolean {
|
||||
return selectionController?.onItemLongClick(view, item.pageId) ?: false
|
||||
return selectionController?.onItemLongClick(view, item.pageId) == true
|
||||
}
|
||||
|
||||
override fun onItemContextClick(item: Bookmark, view: View): Boolean {
|
||||
return selectionController?.onItemContextClick(view, item.pageId) ?: false
|
||||
return selectionController?.onItemContextClick(view, item.pageId) == true
|
||||
}
|
||||
|
||||
override fun onSelectionChanged(controller: ListSelectionController, count: Int) {
|
||||
|
||||
@@ -5,38 +5,38 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.ancestors
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.google.android.material.chip.Chip
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.nav.ReaderIntent
|
||||
import org.koitharu.kotatsu.core.nav.dismissParentDialog
|
||||
import org.koitharu.kotatsu.core.nav.router
|
||||
import org.koitharu.kotatsu.core.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.core.ui.dialog.CommonAlertDialogs
|
||||
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.util.PagerNestedScrollHelper
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback
|
||||
import org.koitharu.kotatsu.core.util.ext.dismissParentDialog
|
||||
import org.koitharu.kotatsu.core.util.ext.findAppCompatDelegate
|
||||
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.adapter.ChaptersAdapter
|
||||
import org.koitharu.kotatsu.details.ui.adapter.ChaptersSelectionDecoration
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesViewModel
|
||||
import org.koitharu.kotatsu.details.ui.withVolumeHeaders
|
||||
import org.koitharu.kotatsu.list.domain.ListFilterOption
|
||||
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 javax.inject.Inject
|
||||
@@ -45,7 +45,7 @@ import kotlin.math.roundToInt
|
||||
@AndroidEntryPoint
|
||||
class ChaptersFragment :
|
||||
BaseFragment<FragmentChaptersBinding>(),
|
||||
OnListItemClickListener<ChapterListItem> {
|
||||
OnListItemClickListener<ChapterListItem>, ChipsView.OnChipClickListener {
|
||||
|
||||
private val viewModel by ChaptersPagesViewModel.ActivityVMLazy(this)
|
||||
|
||||
@@ -86,15 +86,16 @@ class ChaptersFragment :
|
||||
adapter = chaptersAdapter
|
||||
ChapterGridSpanHelper.attach(this)
|
||||
}
|
||||
binding.chipsFilter.onChipClickListener = this
|
||||
viewModel.isLoading.observe(viewLifecycleOwner, this::onLoadingStateChanged)
|
||||
viewModel.chapters
|
||||
.map { it.withVolumeHeaders(requireContext()) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
.observe(viewLifecycleOwner, this::onChaptersChanged)
|
||||
viewModel.quickFilter.observe(viewLifecycleOwner, this::onFilterChanged)
|
||||
viewModel.isChaptersEmpty.observe(viewLifecycleOwner) {
|
||||
binding.textViewHolder.isVisible = it
|
||||
}
|
||||
viewModel.onSelectChapter.observeEvent(viewLifecycleOwner, ::onSelectChapter)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
@@ -111,8 +112,8 @@ class ChaptersFragment :
|
||||
if (listener != null && listener.onChapterSelected(item.chapter)) {
|
||||
dismissParentDialog()
|
||||
} else {
|
||||
startActivity(
|
||||
IntentBuilder(view.context)
|
||||
router.openReader(
|
||||
ReaderIntent.Builder(view.context)
|
||||
.manga(viewModel.getMangaOrNull() ?: return)
|
||||
.state(ReaderState(item.chapter.id, 0, 0))
|
||||
.build(),
|
||||
@@ -121,11 +122,16 @@ class ChaptersFragment :
|
||||
}
|
||||
|
||||
override fun onItemLongClick(item: ChapterListItem, view: View): Boolean {
|
||||
return selectionController?.onItemLongClick(view, item.chapter.id) ?: false
|
||||
return selectionController?.onItemLongClick(view, item.chapter.id) == true
|
||||
}
|
||||
|
||||
override fun onItemContextClick(item: ChapterListItem, view: View): Boolean {
|
||||
return selectionController?.onItemContextClick(view, item.chapter.id) ?: false
|
||||
return selectionController?.onItemContextClick(view, item.chapter.id) == true
|
||||
}
|
||||
|
||||
override fun onChipClick(chip: Chip, data: Any?) {
|
||||
if (data !is ListFilterOption.Branch) return
|
||||
viewModel.setSelectedBranch(data.titleText)
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) = Unit
|
||||
@@ -148,22 +154,10 @@ class ChaptersFragment :
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun onSelectChapter(chapterId: Long) {
|
||||
if (!isResumed) {
|
||||
view?.ancestors?.firstNotNullOfOrNull { it as? ViewPager2 }?.setCurrentItem(0, true)
|
||||
}
|
||||
val position = withContext(Dispatchers.Default) {
|
||||
val predicate: (ListModel) -> Boolean = { x -> x is ChapterListItem && x.chapter.id == chapterId }
|
||||
val items = chaptersAdapter?.observeItems()?.firstOrNull { it.any(predicate) }
|
||||
items?.indexOfFirst(predicate) ?: -1
|
||||
}
|
||||
if (position >= 0) {
|
||||
selectionController?.startSelection(chapterId)
|
||||
val lm = (viewBinding?.recyclerViewChapters?.layoutManager as? LinearLayoutManager)
|
||||
if (lm != null) {
|
||||
val offset = resources.getDimensionPixelOffset(R.dimen.chapter_list_item_height)
|
||||
lm.scrollToPositionWithOffset(position, offset)
|
||||
}
|
||||
private fun onFilterChanged(list: List<ChipsView.ChipModel>) {
|
||||
viewBinding?.chipsFilter?.run {
|
||||
setChips(list)
|
||||
isGone = list.isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.koitharu.kotatsu.details.ui.pager.pages
|
||||
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.core.net.toUri
|
||||
import coil3.ImageLoader
|
||||
import coil3.decode.DataSource
|
||||
@@ -21,8 +20,10 @@ import okio.Path.Companion.toOkioPath
|
||||
import org.koitharu.kotatsu.core.network.MangaHttpClient
|
||||
import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.util.MimeTypes
|
||||
import org.koitharu.kotatsu.core.util.ext.fetch
|
||||
import org.koitharu.kotatsu.core.util.ext.isNetworkUri
|
||||
import org.koitharu.kotatsu.core.util.ext.toMimeTypeOrNull
|
||||
import org.koitharu.kotatsu.local.data.PagesCache
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.util.mimeType
|
||||
@@ -47,7 +48,7 @@ class MangaPageFetcher(
|
||||
pagesCache.get(pageUrl)?.let { file ->
|
||||
return SourceFetchResult(
|
||||
source = ImageSource(file.toOkioPath(), options.fileSystem),
|
||||
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(file.extension),
|
||||
mimeType = MimeTypes.getMimeTypeFromExtension(file.name)?.toString(),
|
||||
dataSource = DataSource.DISK,
|
||||
)
|
||||
}
|
||||
@@ -67,13 +68,13 @@ class MangaPageFetcher(
|
||||
if (!response.isSuccessful) {
|
||||
throw HttpException(response.toNetworkResponse())
|
||||
}
|
||||
val mimeType = response.mimeType
|
||||
val mimeType = response.mimeType?.toMimeTypeOrNull()
|
||||
val file = response.requireBody().use {
|
||||
pagesCache.put(pageUrl, it.source(), mimeType)
|
||||
}
|
||||
SourceFetchResult(
|
||||
source = ImageSource(file.toOkioPath(), FileSystem.SYSTEM),
|
||||
mimeType = mimeType,
|
||||
mimeType = mimeType?.toString(),
|
||||
dataSource = DataSource.NETWORK,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||
import org.koitharu.kotatsu.core.nav.ReaderIntent
|
||||
import org.koitharu.kotatsu.core.nav.dismissParentDialog
|
||||
import org.koitharu.kotatsu.core.nav.router
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.core.ui.list.BoundsScrollListener
|
||||
@@ -29,7 +32,6 @@ import org.koitharu.kotatsu.core.ui.list.ListSelectionController
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.util.PagerNestedScrollHelper
|
||||
import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback
|
||||
import org.koitharu.kotatsu.core.util.ext.dismissParentDialog
|
||||
import org.koitharu.kotatsu.core.util.ext.findAppCompatDelegate
|
||||
import org.koitharu.kotatsu.core.util.ext.findParentCallback
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
@@ -42,7 +44,6 @@ 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.PageSaveHelper
|
||||
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
|
||||
@@ -151,8 +152,8 @@ class PagesFragment :
|
||||
if (listener != null && listener.onPageSelected(item.page)) {
|
||||
dismissParentDialog()
|
||||
} else {
|
||||
startActivity(
|
||||
IntentBuilder(view.context)
|
||||
router.openReader(
|
||||
ReaderIntent.Builder(view.context)
|
||||
.manga(parentViewModel.getMangaOrNull() ?: return)
|
||||
.state(ReaderState(item.page.chapterId, item.page.index, 0))
|
||||
.build(),
|
||||
@@ -161,11 +162,11 @@ class PagesFragment :
|
||||
}
|
||||
|
||||
override fun onItemLongClick(item: PageThumbnail, view: View): Boolean {
|
||||
return selectionController?.onItemLongClick(view, item.page.id) ?: false
|
||||
return selectionController?.onItemLongClick(view, item.page.id) == true
|
||||
}
|
||||
|
||||
override fun onItemContextClick(item: PageThumbnail, view: View): Boolean {
|
||||
return selectionController?.onItemContextClick(view, item.page.id) ?: false
|
||||
return selectionController?.onItemContextClick(view, item.page.id) == true
|
||||
}
|
||||
|
||||
override fun onSelectionChanged(controller: ListSelectionController, count: Int) {
|
||||
|
||||
@@ -13,7 +13,7 @@ import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
@@ -37,7 +37,7 @@ class RelatedListViewModel @Inject constructor(
|
||||
downloadScheduler: DownloadWorker.Scheduler,
|
||||
) : MangaListViewModel(settings, downloadScheduler) {
|
||||
|
||||
private val seed = savedStateHandle.require<ParcelableManga>(MangaIntent.KEY_MANGA).manga
|
||||
private val seed = savedStateHandle.require<ParcelableManga>(AppRouter.KEY_MANGA).manga
|
||||
private val repository = mangaRepositoryFactory.create(seed.source)
|
||||
private val mangaList = MutableStateFlow<List<Manga>?>(null)
|
||||
private val listError = MutableStateFlow<Throwable?>(null)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package org.koitharu.kotatsu.details.ui.related
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.updatePadding
|
||||
@@ -9,12 +7,9 @@ import androidx.fragment.app.commit
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.databinding.ActivityContainerBinding
|
||||
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
|
||||
@AndroidEntryPoint
|
||||
class RelatedMangaActivity : BaseActivity<ActivityContainerBinding>(), AppBarOwner {
|
||||
@@ -41,10 +36,4 @@ class RelatedMangaActivity : BaseActivity<ActivityContainerBinding>(), AppBarOwn
|
||||
right = insets.right,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun newIntent(context: Context, seed: Manga) = Intent(context, RelatedMangaActivity::class.java)
|
||||
.putExtra(MangaIntent.KEY_MANGA, ParcelableManga(seed))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package org.koitharu.kotatsu.details.ui.scrobbling
|
||||
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import coil3.ImageLoader
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
|
||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||
@@ -15,12 +15,12 @@ import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
||||
fun scrobblingInfoAD(
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
coil: ImageLoader,
|
||||
fragmentManager: FragmentManager,
|
||||
router: AppRouter,
|
||||
) = adapterDelegateViewBinding<ScrobblingInfo, ListModel, ItemScrobblingInfoBinding>(
|
||||
{ layoutInflater, parent -> ItemScrobblingInfoBinding.inflate(layoutInflater, parent, false) },
|
||||
) {
|
||||
binding.root.setOnClickListener {
|
||||
ScrobblingInfoSheet.show(fragmentManager, bindingAdapterPosition)
|
||||
router.showScrobblingInfoSheet(bindingAdapterPosition)
|
||||
}
|
||||
|
||||
bind {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.koitharu.kotatsu.details.ui.scrobbling
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
@@ -10,13 +9,14 @@ import android.widget.AdapterView
|
||||
import android.widget.RatingBar
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.text.method.LinkMovementMethodCompat
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import coil3.ImageLoader
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.nav.router
|
||||
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
|
||||
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
|
||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||
@@ -25,14 +25,10 @@ import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.core.util.ext.sanitize
|
||||
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
|
||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||
import org.koitharu.kotatsu.databinding.SheetScrobblingBinding
|
||||
import org.koitharu.kotatsu.details.ui.DetailsViewModel
|
||||
import org.koitharu.kotatsu.image.ui.ImageActivity
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
|
||||
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
@@ -53,7 +49,7 @@ class ScrobblingInfoSheet :
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
scrobblerIndex = requireArguments().getInt(ARG_INDEX, scrobblerIndex)
|
||||
scrobblerIndex = requireArguments().getInt(AppRouter.KEY_INDEX, scrobblerIndex)
|
||||
}
|
||||
|
||||
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetScrobblingBinding {
|
||||
@@ -108,11 +104,11 @@ class ScrobblingInfoSheet :
|
||||
override fun onClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.button_menu -> menu?.show()
|
||||
R.id.imageView_cover -> {
|
||||
val coverUrl = viewModel.scrobblingInfo.value.getOrNull(scrobblerIndex)?.coverUrl ?: return
|
||||
val options = scaleUpActivityOptionsOf(v)
|
||||
startActivity(ImageActivity.newIntent(v.context, coverUrl, null), options)
|
||||
}
|
||||
R.id.imageView_cover -> router.openImage(
|
||||
url = viewModel.scrobblingInfo.value.getOrNull(scrobblerIndex)?.coverUrl ?: return,
|
||||
source = null,
|
||||
anchor = v,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,10 +135,13 @@ class ScrobblingInfoSheet :
|
||||
when (item.itemId) {
|
||||
R.id.action_browser -> {
|
||||
val url = viewModel.scrobblingInfo.value.getOrNull(scrobblerIndex)?.externalUrl ?: return false
|
||||
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||
startActivity(
|
||||
Intent.createChooser(intent, getString(R.string.open_in_browser)),
|
||||
)
|
||||
if (!router.openExternalBrowser(url, getString(R.string.open_in_browser))) {
|
||||
Snackbar.make(
|
||||
viewBinding?.textViewDescription ?: return false,
|
||||
R.string.operation_not_supported,
|
||||
Snackbar.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
R.id.action_unregister -> {
|
||||
@@ -153,20 +152,10 @@ class ScrobblingInfoSheet :
|
||||
R.id.action_edit -> {
|
||||
val manga = viewModel.manga.value ?: return false
|
||||
val scrobblerService = viewModel.scrobblingInfo.value.getOrNull(scrobblerIndex)?.scrobbler
|
||||
ScrobblingSelectorSheet.show(parentFragmentManager, manga, scrobblerService)
|
||||
router.showScrobblingSelectorSheet(manga, scrobblerService)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "ScrobblingInfoBottomSheet"
|
||||
private const val ARG_INDEX = "index"
|
||||
|
||||
fun show(fm: FragmentManager, index: Int) = ScrobblingInfoSheet().withArgs(1) {
|
||||
putInt(ARG_INDEX, index)
|
||||
}.show(fm, TAG)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
package org.koitharu.kotatsu.details.ui.scrobbling
|
||||
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import coil3.ImageLoader
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
class ScrollingInfoAdapter(
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
coil: ImageLoader,
|
||||
fragmentManager: FragmentManager,
|
||||
router: AppRouter,
|
||||
) : BaseListAdapter<ListModel>() {
|
||||
|
||||
init {
|
||||
delegatesManager.addDelegate(scrobblingInfoAD(lifecycleOwner, coil, fragmentManager))
|
||||
delegatesManager.addDelegate(scrobblingInfoAD(lifecycleOwner, coil, router))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.koitharu.kotatsu.download.ui.dialog
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
@@ -9,17 +8,16 @@ import android.view.ViewGroup
|
||||
import android.widget.Spinner
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.FragmentResultListener
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.prefs.DownloadFormat
|
||||
import org.koitharu.kotatsu.core.ui.AlertDialogFragment
|
||||
import org.koitharu.kotatsu.core.ui.dialog.CommonAlertDialogs
|
||||
@@ -27,17 +25,12 @@ import org.koitharu.kotatsu.core.ui.widgets.TwoLinesItemView
|
||||
import org.koitharu.kotatsu.core.util.ext.findActivity
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.core.util.ext.joinToStringWithLimit
|
||||
import org.koitharu.kotatsu.core.util.ext.mapToArray
|
||||
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.showDistinct
|
||||
import org.koitharu.kotatsu.core.util.ext.showOrHide
|
||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||
import org.koitharu.kotatsu.databinding.DialogDownloadBinding
|
||||
import org.koitharu.kotatsu.download.ui.list.DownloadsActivity
|
||||
import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.util.format
|
||||
import org.koitharu.kotatsu.settings.storage.DirectoryModel
|
||||
import javax.inject.Inject
|
||||
@@ -75,6 +68,7 @@ class DownloadDialogFragment : AlertDialogFragment<DialogDownloadBinding>(), Vie
|
||||
binding.buttonConfirm.setOnClickListener(this)
|
||||
binding.textViewMore.setOnClickListener(this)
|
||||
|
||||
binding.textViewTip.isVisible = viewModel.manga.size == 1
|
||||
binding.textViewSummary.text = viewModel.manga.joinToStringWithLimit(binding.root.context, 120) { it.title }
|
||||
|
||||
viewModel.isLoading.observe(viewLifecycleOwner, this::onLoadingStateChanged)
|
||||
@@ -324,7 +318,9 @@ class DownloadDialogFragment : AlertDialogFragment<DialogDownloadBinding>(), Vie
|
||||
}
|
||||
}
|
||||
|
||||
private class SnackbarResultListener(private val host: View) : FragmentResultListener {
|
||||
private class SnackbarResultListener(
|
||||
private val host: View,
|
||||
) : FragmentResultListener {
|
||||
|
||||
override fun onFragmentResult(requestKey: String, result: Bundle) {
|
||||
val isStarted = result.getBoolean(ARG_STARTED, true)
|
||||
@@ -336,8 +332,9 @@ class DownloadDialogFragment : AlertDialogFragment<DialogDownloadBinding>(), Vie
|
||||
(host.context.findActivity() as? BottomNavOwner)?.let {
|
||||
snackbar.anchorView = it.bottomNav
|
||||
}
|
||||
snackbar.setAction(R.string.details) {
|
||||
it.context.startActivity(Intent(it.context, DownloadsActivity::class.java))
|
||||
val router = AppRouter.from(host)
|
||||
if (router != null) {
|
||||
snackbar.setAction(R.string.details) { router.openDownloads() }
|
||||
}
|
||||
snackbar.show()
|
||||
}
|
||||
@@ -345,28 +342,16 @@ class DownloadDialogFragment : AlertDialogFragment<DialogDownloadBinding>(), Vie
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "DownloadDialogFragment"
|
||||
private const val RESULT_KEY = "DOWNLOAD_STARTED"
|
||||
private const val ARG_STARTED = "started"
|
||||
private const val KEY_CHECKED_OPTION = "checked_opt"
|
||||
const val ARG_MANGA = "manga"
|
||||
|
||||
fun show(fm: FragmentManager, manga: Collection<Manga>) = DownloadDialogFragment().withArgs(1) {
|
||||
putParcelableArray(ARG_MANGA, manga.mapToArray { ParcelableManga(it) })
|
||||
}.showDistinct(fm, TAG)
|
||||
fun registerCallback(
|
||||
fm: FragmentManager,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
snackbarHost: View
|
||||
) = fm.setFragmentResultListener(RESULT_KEY, lifecycleOwner, SnackbarResultListener(snackbarHost))
|
||||
|
||||
fun registerCallback(activity: FragmentActivity, snackbarHost: View) =
|
||||
activity.supportFragmentManager.setFragmentResultListener(
|
||||
RESULT_KEY,
|
||||
activity,
|
||||
SnackbarResultListener(snackbarHost),
|
||||
)
|
||||
|
||||
fun registerCallback(fragment: Fragment, snackbarHost: View) =
|
||||
fragment.childFragmentManager.setFragmentResultListener(
|
||||
RESULT_KEY,
|
||||
fragment.viewLifecycleOwner,
|
||||
SnackbarResultListener(snackbarHost),
|
||||
)
|
||||
fun unregisterCallback(fm: FragmentManager) = fm.clearFragmentResultListener(RESULT_KEY)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.getPreferredBranch
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.DownloadFormat
|
||||
@@ -45,7 +46,7 @@ class DownloadDialogViewModel @Inject constructor(
|
||||
private val settings: AppSettings,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val manga = savedStateHandle.require<Array<ParcelableManga>>(DownloadDialogFragment.ARG_MANGA).map {
|
||||
val manga = savedStateHandle.require<Array<ParcelableManga>>(AppRouter.KEY_MANGA).map {
|
||||
it.manga
|
||||
}
|
||||
private val mangaDetails = suspendLazy {
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.core.view.updatePadding
|
||||
import coil3.ImageLoader
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.nav.router
|
||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
|
||||
import org.koitharu.kotatsu.core.ui.list.RecyclerScrollKeeper
|
||||
@@ -20,7 +21,6 @@ import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.databinding.ActivityDownloadsBinding
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
|
||||
import javax.inject.Inject
|
||||
@@ -84,7 +84,7 @@ class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>(),
|
||||
if (selectionController.onItemClick(item.id.mostSignificantBits)) {
|
||||
return
|
||||
}
|
||||
startActivity(DetailsActivity.newIntent(view.context, item.manga ?: return))
|
||||
router.openDetails(item.manga ?: return)
|
||||
}
|
||||
|
||||
override fun onItemLongClick(item: DownloadItemModel, view: View): Boolean {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
package org.koitharu.kotatsu.download.ui.list
|
||||
|
||||
import android.content.Context
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.nav.router
|
||||
import org.koitharu.kotatsu.core.ui.dialog.buildAlertDialog
|
||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
|
||||
class DownloadsMenuProvider(
|
||||
private val context: Context,
|
||||
private val activity: FragmentActivity,
|
||||
private val viewModel: DownloadsViewModel,
|
||||
) : MenuProvider {
|
||||
|
||||
@@ -24,10 +24,7 @@ class DownloadsMenuProvider(
|
||||
R.id.action_resume -> viewModel.resumeAll()
|
||||
R.id.action_cancel_all -> confirmCancelAll()
|
||||
R.id.action_remove_completed -> confirmRemoveCompleted()
|
||||
R.id.action_settings -> {
|
||||
context.startActivity(SettingsActivity.newDownloadsSettingsIntent(context))
|
||||
}
|
||||
|
||||
R.id.action_settings -> activity.router.openDownloadsSetting()
|
||||
else -> return false
|
||||
}
|
||||
return true
|
||||
@@ -41,7 +38,7 @@ class DownloadsMenuProvider(
|
||||
}
|
||||
|
||||
private fun confirmCancelAll() {
|
||||
buildAlertDialog(context, isCentered = true) {
|
||||
buildAlertDialog(activity, isCentered = true) {
|
||||
setTitle(R.string.cancel_all)
|
||||
setMessage(R.string.cancel_all_downloads_confirm)
|
||||
setIcon(R.drawable.ic_cancel_multiple)
|
||||
@@ -51,7 +48,7 @@ class DownloadsMenuProvider(
|
||||
}
|
||||
|
||||
private fun confirmRemoveCompleted() {
|
||||
buildAlertDialog(context, isCentered = true) {
|
||||
buildAlertDialog(activity, isCentered = true) {
|
||||
setTitle(R.string.remove_completed)
|
||||
setMessage(R.string.remove_completed_downloads_confirm)
|
||||
setIcon(R.drawable.ic_clear_all)
|
||||
|
||||
@@ -289,7 +289,7 @@ class DownloadsViewModel @Inject constructor(
|
||||
}
|
||||
return cacheMutex.withLock {
|
||||
mangaCache.getOrElse(mangaId) {
|
||||
mangaDataRepository.findMangaById(mangaId)?.also {
|
||||
mangaDataRepository.findMangaById(mangaId, withChapters = true)?.also {
|
||||
mangaCache[mangaId] = it
|
||||
} ?: return null
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user