Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28dede0d3e | ||
|
|
d66e61f845 | ||
|
|
b246575486 | ||
|
|
18dd205051 | ||
|
|
0e10fdaf36 | ||
|
|
7c82b4effb | ||
|
|
82684601b7 | ||
|
|
77ad21bd7a | ||
|
|
e6c8591bf8 | ||
|
|
e330be5d13 | ||
|
|
6a4cd9643a | ||
|
|
d98cb9a577 | ||
|
|
ac455527ef | ||
|
|
7e37345dea | ||
|
|
6e810179a7 | ||
|
|
7715aff953 | ||
|
|
63e6b9f026 | ||
|
|
b6f136fb71 | ||
|
|
de0327a00a | ||
|
|
e5f09ae4c9 | ||
|
|
f10d9b54d8 | ||
|
|
619d672e49 | ||
|
|
db519701bc | ||
|
|
e42aeb857f | ||
|
|
4f82495cfc | ||
|
|
311c36b7c0 | ||
|
|
002ce25d7e | ||
|
|
d9cf13d3fb | ||
|
|
ed5b1306b8 | ||
|
|
227fe86cf9 | ||
|
|
1905482b06 | ||
|
|
46ded4af0d | ||
|
|
6676ab82b4 | ||
|
|
1a60df6d98 | ||
|
|
5ef1b4ac9c | ||
|
|
17828ae755 | ||
|
|
d8ac4d6738 | ||
|
|
0a10cb509c | ||
|
|
7a3fd20dfa | ||
|
|
ab20e50dc1 | ||
|
|
f783ffef11 | ||
|
|
e01c485949 | ||
|
|
3672c84e8f | ||
|
|
55c5a07c8b | ||
|
|
a3cf32aefb | ||
|
|
c21bf30e91 | ||
|
|
1719547ce0 | ||
|
|
22186825a0 | ||
|
|
b9c83ad5cc | ||
|
|
1359689b23 | ||
|
|
7bad6ad077 | ||
|
|
b9097fa077 | ||
|
|
0b03806ccd | ||
|
|
db9c1279ac |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -24,3 +24,4 @@
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
/.idea/deviceManager.xml
|
||||
|
||||
@@ -16,8 +16,8 @@ android {
|
||||
applicationId 'org.koitharu.kotatsu'
|
||||
minSdk = 21
|
||||
targetSdk = 34
|
||||
versionCode = 636
|
||||
versionName = '7.0-b2'
|
||||
versionCode = 641
|
||||
versionName = '7.0'
|
||||
generatedDensities = []
|
||||
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
|
||||
ksp {
|
||||
@@ -82,18 +82,18 @@ afterEvaluate {
|
||||
}
|
||||
dependencies {
|
||||
//noinspection GradleDependency
|
||||
implementation('com.github.KotatsuApp:kotatsu-parsers:a245574dee') {
|
||||
implementation('com.github.KotatsuApp:kotatsu-parsers:3e32a6280a') {
|
||||
exclude group: 'org.json', module: 'json'
|
||||
}
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
|
||||
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.23'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0'
|
||||
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.24'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1'
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.core:core-ktx:1.13.0'
|
||||
implementation 'androidx.core:core-ktx:1.13.1'
|
||||
implementation 'androidx.activity:activity-ktx:1.9.0'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.6.2'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.7.0'
|
||||
implementation 'androidx.collection:collection-ktx:1.4.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-service:2.7.0'
|
||||
@@ -101,12 +101,12 @@ dependencies {
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-rc01'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
|
||||
implementation 'com.google.android.material:material:1.12.0-rc01'
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.7.0'
|
||||
implementation 'androidx.webkit:webkit:1.10.0'
|
||||
implementation 'androidx.webkit:webkit:1.11.0'
|
||||
|
||||
implementation 'androidx.work:work-runtime:2.9.0'
|
||||
//noinspection GradleDependency
|
||||
@@ -121,6 +121,7 @@ dependencies {
|
||||
ksp 'androidx.room:room-compiler:2.6.1'
|
||||
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp-tls:4.12.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.12.0'
|
||||
implementation 'com.squareup.okio:okio:3.9.0'
|
||||
|
||||
@@ -146,17 +147,18 @@ dependencies {
|
||||
implementation 'org.conscrypt:conscrypt-android:2.5.2'
|
||||
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.14'
|
||||
debugImplementation 'com.github.Koitharu:WorkInspector:5778dd1747'
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.json:json:20240303'
|
||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0'
|
||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1'
|
||||
|
||||
androidTestImplementation 'androidx.test:runner:1.5.2'
|
||||
androidTestImplementation 'androidx.test:rules:1.5.0'
|
||||
androidTestImplementation 'androidx.test:core-ktx:1.5.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5'
|
||||
|
||||
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0'
|
||||
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1'
|
||||
|
||||
androidTestImplementation 'androidx.room:room-testing:2.6.1'
|
||||
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.15.1'
|
||||
|
||||
@@ -11,7 +11,7 @@ import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||
|
||||
class KotatsuApp : BaseApp() {
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(base)
|
||||
enableStrictMode()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.koitharu.kotatsu.core.util.ext
|
||||
|
||||
import android.os.Looper
|
||||
|
||||
fun Throwable.printStackTraceDebug() = printStackTrace()
|
||||
|
||||
fun assertNotInMainThread() = check(Looper.myLooper() != Looper.getMainLooper()) {
|
||||
"Calling this from the main thread is prohibited"
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.util.ext
|
||||
|
||||
fun Throwable.printStackTraceDebug() = printStackTrace()
|
||||
@@ -13,4 +13,9 @@
|
||||
android:title="@string/check_for_new_chapters"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@id/action_works"
|
||||
android:title="Works"
|
||||
app:showAsAction="never" />
|
||||
|
||||
</menu>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<bool name="leak_canary_add_launcher_icon" tools:node="replace">false</bool>
|
||||
<bool name="wi_launcher_icon_enabled" tools:node="replace">false</bool>
|
||||
</resources>
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
android:name="org.koitharu.kotatsu.favourites.ui.FavouritesActivity"
|
||||
android:label="@string/favourites" />
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.bookmarks.ui.BookmarksActivity"
|
||||
android:name="org.koitharu.kotatsu.bookmarks.ui.AllBookmarksActivity"
|
||||
android:label="@string/bookmarks" />
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity"
|
||||
|
||||
31
app/src/main/assets/isrgrootx1.pem
Normal file
31
app/src/main/assets/isrgrootx1.pem
Normal file
@@ -0,0 +1,31 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
|
||||
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
||||
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
|
||||
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
|
||||
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
|
||||
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
|
||||
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
|
||||
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
|
||||
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
|
||||
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
|
||||
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
|
||||
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
|
||||
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
|
||||
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
|
||||
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
|
||||
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
|
||||
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
|
||||
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
|
||||
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
|
||||
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
|
||||
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
|
||||
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
|
||||
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
|
||||
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
|
||||
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
|
||||
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
|
||||
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
|
||||
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
|
||||
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -12,6 +12,7 @@ import org.koitharu.kotatsu.history.data.toMangaHistory
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.tracker.data.TrackEntity
|
||||
import javax.inject.Inject
|
||||
|
||||
class MigrateUseCase @Inject constructor(
|
||||
@@ -56,6 +57,22 @@ class MigrateUseCase @Inject constructor(
|
||||
historyDao.delete(oldDetails.id)
|
||||
historyDao.upsert(newHistory)
|
||||
}
|
||||
// track
|
||||
val tracksDao = database.getTracksDao()
|
||||
val oldTrack = tracksDao.find(oldDetails.id)
|
||||
if (oldTrack != null) {
|
||||
val lastChapter = newDetails.chapters?.lastOrNull()
|
||||
val newTrack = TrackEntity(
|
||||
mangaId = newDetails.id,
|
||||
lastChapterId = lastChapter?.id ?: 0L,
|
||||
newChapters = 0,
|
||||
lastCheckTime = System.currentTimeMillis(),
|
||||
lastChapterDate = lastChapter?.uploadDate ?: 0L,
|
||||
lastResult = TrackEntity.RESULT_EXTERNAL_MODIFICATION,
|
||||
)
|
||||
tracksDao.delete(oldDetails.id)
|
||||
tracksDao.upsert(newTrack)
|
||||
}
|
||||
}
|
||||
progressUpdateUseCase(newManga)
|
||||
}
|
||||
|
||||
@@ -12,9 +12,6 @@ import org.koitharu.kotatsu.core.db.entity.MangaWithTags
|
||||
@Dao
|
||||
abstract class BookmarksDao {
|
||||
|
||||
@Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId AND page_id = :pageId")
|
||||
abstract suspend fun find(mangaId: Long, pageId: Long): BookmarkEntity?
|
||||
|
||||
@Query("SELECT * FROM bookmarks WHERE page_id = :pageId")
|
||||
abstract suspend fun find(pageId: Long): BookmarkEntity?
|
||||
|
||||
@@ -42,9 +39,6 @@ abstract class BookmarksDao {
|
||||
@Delete
|
||||
abstract suspend fun delete(entity: BookmarkEntity)
|
||||
|
||||
@Query("DELETE FROM bookmarks WHERE manga_id = :mangaId AND page_id = :pageId")
|
||||
abstract suspend fun delete(mangaId: Long, pageId: Long): Int
|
||||
|
||||
@Query("DELETE FROM bookmarks WHERE page_id = :pageId")
|
||||
abstract suspend fun delete(pageId: Long): Int
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||
import org.koitharu.kotatsu.main.ui.owners.SnackbarOwner
|
||||
|
||||
@AndroidEntryPoint
|
||||
class BookmarksActivity :
|
||||
class AllBookmarksActivity :
|
||||
BaseActivity<ActivityContainerBinding>(),
|
||||
AppBarOwner,
|
||||
SnackbarOwner {
|
||||
@@ -35,7 +35,7 @@ class BookmarksActivity :
|
||||
if (fm.findFragmentById(R.id.container) == null) {
|
||||
fm.commit {
|
||||
setReorderingAllowed(true)
|
||||
replace(R.id.container, BookmarksFragment::class.java, null)
|
||||
replace(R.id.container, AllBookmarksFragment::class.java, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,6 @@ class BookmarksActivity :
|
||||
|
||||
companion object {
|
||||
|
||||
fun newIntent(context: Context) = Intent(context, BookmarksActivity::class.java)
|
||||
fun newIntent(context: Context) = Intent(context, AllBookmarksActivity::class.java)
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import coil.ImageLoader
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.bookmarks.ui.sheet.BookmarksAdapter
|
||||
import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksAdapter
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BaseFragment
|
||||
@@ -42,7 +42,7 @@ import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class BookmarksFragment :
|
||||
class AllBookmarksFragment :
|
||||
BaseFragment<FragmentListSimpleBinding>(),
|
||||
ListStateHolderListener,
|
||||
OnListItemClickListener<Bookmark>,
|
||||
@@ -55,7 +55,7 @@ class BookmarksFragment :
|
||||
@Inject
|
||||
lateinit var settings: AppSettings
|
||||
|
||||
private val viewModel by viewModels<BookmarksViewModel>()
|
||||
private val viewModel by viewModels<AllBookmarksViewModel>()
|
||||
private var bookmarksAdapter: BookmarksAdapter? = null
|
||||
private var selectionController: ListSelectionController? = null
|
||||
|
||||
@@ -213,6 +213,6 @@ class BookmarksFragment :
|
||||
"org.koitharu.kotatsu.bookmarks.ui.BookmarksFragment",
|
||||
),
|
||||
)
|
||||
fun newInstance() = BookmarksFragment()
|
||||
fun newInstance() = AllBookmarksFragment()
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class BookmarksViewModel @Inject constructor(
|
||||
class AllBookmarksViewModel @Inject constructor(
|
||||
private val repository: BookmarksRepository,
|
||||
) : BaseViewModel() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.bookmarks.ui.sheet
|
||||
package org.koitharu.kotatsu.bookmarks.ui.adapter
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import coil.ImageLoader
|
||||
@@ -1,19 +1,36 @@
|
||||
package org.koitharu.kotatsu.bookmarks.ui.adapter
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import coil.ImageLoader
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListHeaderClickListener
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
|
||||
import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD
|
||||
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
|
||||
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
class BookmarksAdapter(
|
||||
coil: ImageLoader,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
clickListener: OnListItemClickListener<Bookmark>,
|
||||
) : BaseListAdapter<Bookmark>() {
|
||||
headerClickListener: ListHeaderClickListener?,
|
||||
) : BaseListAdapter<ListModel>(), FastScroller.SectionIndexer {
|
||||
|
||||
init {
|
||||
addDelegate(ListItemType.PAGE_THUMB, bookmarkListAD(coil, lifecycleOwner, clickListener))
|
||||
addDelegate(ListItemType.PAGE_THUMB, bookmarkLargeAD(coil, lifecycleOwner, clickListener))
|
||||
addDelegate(ListItemType.HEADER, listHeaderAD(headerClickListener))
|
||||
addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD())
|
||||
addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
|
||||
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, null))
|
||||
}
|
||||
|
||||
override fun getSectionText(context: Context, position: Int): CharSequence? {
|
||||
return findHeader(position)?.getText(context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
package org.koitharu.kotatsu.bookmarks.ui.sheet
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import coil.ImageLoader
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListHeaderClickListener
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
|
||||
import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD
|
||||
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
|
||||
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
class BookmarksAdapter(
|
||||
coil: ImageLoader,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
clickListener: OnListItemClickListener<Bookmark>,
|
||||
headerClickListener: ListHeaderClickListener?,
|
||||
) : BaseListAdapter<ListModel>(), FastScroller.SectionIndexer {
|
||||
|
||||
init {
|
||||
addDelegate(ListItemType.PAGE_THUMB, bookmarkLargeAD(coil, lifecycleOwner, clickListener))
|
||||
addDelegate(ListItemType.HEADER, listHeaderAD(headerClickListener))
|
||||
addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD())
|
||||
addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
|
||||
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, null))
|
||||
}
|
||||
|
||||
override fun getSectionText(context: Context, position: Int): CharSequence? {
|
||||
return findHeader(position)?.getText(context)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package org.koitharu.kotatsu.browser.cloudflare
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import androidx.core.app.NotificationChannelCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
@@ -33,7 +36,7 @@ class CaptchaNotifier(
|
||||
.build()
|
||||
manager.createNotificationChannel(channel)
|
||||
|
||||
val intent = CloudFlareActivity.newIntent(context, exception.url, exception.headers)
|
||||
val intent = CloudFlareActivity.newIntent(context, exception)
|
||||
.setData(exception.url.toUri())
|
||||
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setContentTitle(channel.name)
|
||||
@@ -56,8 +59,21 @@ class CaptchaNotifier(
|
||||
),
|
||||
)
|
||||
.setContentIntent(PendingIntentCompat.getActivity(context, 0, intent, 0, false))
|
||||
.build()
|
||||
manager.notify(TAG, exception.source.hashCode(), notification)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val actionIntent = PendingIntentCompat.getActivity(
|
||||
context, SETTINGS_ACTION_CODE,
|
||||
Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
|
||||
.putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
|
||||
.putExtra(Settings.EXTRA_CHANNEL_ID, CHANNEL_ID),
|
||||
0, false,
|
||||
)
|
||||
notification.addAction(
|
||||
R.drawable.ic_settings,
|
||||
context.getString(R.string.notifications_settings),
|
||||
actionIntent,
|
||||
)
|
||||
}
|
||||
manager.notify(TAG, exception.source.hashCode(), notification.build())
|
||||
}
|
||||
|
||||
fun dismiss(source: MangaSource) {
|
||||
@@ -84,5 +100,6 @@ class CaptchaNotifier(
|
||||
private const val CHANNEL_ID = "captcha"
|
||||
private const val TAG = CHANNEL_ID
|
||||
private const val GROUP_CAPTCHA = "org.koitharu.kotatsu.CAPTCHA"
|
||||
private const val SETTINGS_ACTION_CODE = 3
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,12 +23,15 @@ 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.network.cookies.MutableCookieJar
|
||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.core.util.TaggedActivityResult
|
||||
import org.koitharu.kotatsu.core.util.ext.configureForParser
|
||||
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
|
||||
|
||||
@@ -137,6 +140,10 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
|
||||
|
||||
override fun onCheckPassed() {
|
||||
pendingResult = RESULT_OK
|
||||
val source = intent?.getStringExtra(ARG_SOURCE)
|
||||
if (source != null) {
|
||||
CaptchaNotifier(this).dismiss(MangaSource(source))
|
||||
}
|
||||
finishAfterTransition()
|
||||
}
|
||||
|
||||
@@ -174,9 +181,9 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
|
||||
}
|
||||
}
|
||||
|
||||
class Contract : ActivityResultContract<Pair<String, Headers?>, TaggedActivityResult>() {
|
||||
override fun createIntent(context: Context, input: Pair<String, Headers?>): Intent {
|
||||
return newIntent(context, input.first, input.second)
|
||||
class Contract : ActivityResultContract<CloudFlareProtectedException, TaggedActivityResult>() {
|
||||
override fun createIntent(context: Context, input: CloudFlareProtectedException): Intent {
|
||||
return newIntent(context, input)
|
||||
}
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?): TaggedActivityResult {
|
||||
@@ -188,13 +195,23 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
|
||||
|
||||
const val TAG = "CloudFlareActivity"
|
||||
private const val ARG_UA = "ua"
|
||||
private const val ARG_SOURCE = "_source"
|
||||
|
||||
fun newIntent(
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -26,9 +26,6 @@ import kotlinx.coroutines.flow.asSharedFlow
|
||||
import okhttp3.OkHttpClient
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier
|
||||
import org.koitharu.kotatsu.core.cache.ContentCache
|
||||
import org.koitharu.kotatsu.core.cache.MemoryContentCache
|
||||
import org.koitharu.kotatsu.core.cache.StubContentCache
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.network.ImageProxyInterceptor
|
||||
import org.koitharu.kotatsu.core.network.MangaHttpClient
|
||||
@@ -43,6 +40,7 @@ import org.koitharu.kotatsu.core.util.AcraScreenLogger
|
||||
import org.koitharu.kotatsu.core.util.ext.connectivityManager
|
||||
import org.koitharu.kotatsu.core.util.ext.isLowRamDevice
|
||||
import org.koitharu.kotatsu.details.ui.pager.pages.MangaPageFetcher
|
||||
import org.koitharu.kotatsu.details.ui.pager.pages.MangaPageKeyer
|
||||
import org.koitharu.kotatsu.local.data.CacheDir
|
||||
import org.koitharu.kotatsu.local.data.CbzFetcher
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||
@@ -54,6 +52,7 @@ import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
|
||||
import org.koitharu.kotatsu.settings.backup.BackupObserver
|
||||
import org.koitharu.kotatsu.sync.domain.SyncController
|
||||
import org.koitharu.kotatsu.widget.WidgetUpdater
|
||||
import javax.inject.Provider
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@@ -86,7 +85,7 @@ interface AppModule {
|
||||
@Singleton
|
||||
fun provideCoil(
|
||||
@ApplicationContext context: Context,
|
||||
@MangaHttpClient okHttpClient: OkHttpClient,
|
||||
@MangaHttpClient okHttpClientProvider: Provider<OkHttpClient>,
|
||||
mangaRepositoryFactory: MangaRepository.Factory,
|
||||
imageProxyInterceptor: ImageProxyInterceptor,
|
||||
pageFetcherFactory: MangaPageFetcher.Factory,
|
||||
@@ -98,11 +97,14 @@ interface AppModule {
|
||||
.directory(rootDir.resolve(CacheDir.THUMBS.dir))
|
||||
.build()
|
||||
}
|
||||
val okHttpClientLazy = lazy {
|
||||
okHttpClientProvider.get().newBuilder().cache(null).build()
|
||||
}
|
||||
return ImageLoader.Builder(context)
|
||||
.okHttpClient(okHttpClient.newBuilder().cache(null).build())
|
||||
.okHttpClient { okHttpClientLazy.value }
|
||||
.interceptorDispatcher(Dispatchers.Default)
|
||||
.fetcherDispatcher(Dispatchers.IO)
|
||||
.decoderDispatcher(Dispatchers.Default)
|
||||
.fetcherDispatcher(Dispatchers.Default)
|
||||
.decoderDispatcher(Dispatchers.IO)
|
||||
.transformationDispatcher(Dispatchers.Default)
|
||||
.diskCache(diskCacheFactory)
|
||||
.logger(if (BuildConfig.DEBUG) DebugLogger() else null)
|
||||
@@ -112,7 +114,8 @@ interface AppModule {
|
||||
ComponentRegistry.Builder()
|
||||
.add(SvgDecoder.Factory())
|
||||
.add(CbzFetcher.Factory())
|
||||
.add(FaviconFetcher.Factory(context, okHttpClient, mangaRepositoryFactory))
|
||||
.add(FaviconFetcher.Factory(context, okHttpClientLazy, mangaRepositoryFactory))
|
||||
.add(MangaPageKeyer())
|
||||
.add(pageFetcherFactory)
|
||||
.add(imageProxyInterceptor)
|
||||
.add(coverRestoreInterceptor)
|
||||
@@ -153,18 +156,6 @@ interface AppModule {
|
||||
acraScreenLogger,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideContentCache(
|
||||
application: Application,
|
||||
): ContentCache {
|
||||
return if (application.isLowRamDevice()) {
|
||||
StubContentCache()
|
||||
} else {
|
||||
MemoryContentCache(application)
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@LocalStorageChanges
|
||||
|
||||
@@ -37,7 +37,7 @@ import javax.inject.Provider
|
||||
open class BaseApp : Application(), Configuration.Provider {
|
||||
|
||||
@Inject
|
||||
lateinit var databaseObservers: Set<@JvmSuppressWildcards InvalidationTracker.Observer>
|
||||
lateinit var databaseObserversProvider: Provider<Set<@JvmSuppressWildcards InvalidationTracker.Observer>>
|
||||
|
||||
@Inject
|
||||
lateinit var activityLifecycleCallbacks: Set<@JvmSuppressWildcards ActivityLifecycleCallbacks>
|
||||
@@ -87,7 +87,7 @@ open class BaseApp : Application(), Configuration.Provider {
|
||||
WorkServiceStopHelper(workManagerProvider).setup()
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(base)
|
||||
initAcra {
|
||||
buildConfigClass = BuildConfig::class.java
|
||||
@@ -123,7 +123,7 @@ open class BaseApp : Application(), Configuration.Provider {
|
||||
@WorkerThread
|
||||
private fun setupDatabaseObservers() {
|
||||
val tracker = database.get().invalidationTracker
|
||||
databaseObservers.forEach {
|
||||
databaseObserversProvider.get().forEach {
|
||||
tracker.addObserver(it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.cache
|
||||
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
|
||||
interface ContentCache {
|
||||
|
||||
val isCachingEnabled: Boolean
|
||||
|
||||
suspend fun getDetails(source: MangaSource, url: String): Manga?
|
||||
|
||||
fun putDetails(source: MangaSource, url: String, details: SafeDeferred<Manga>)
|
||||
|
||||
suspend fun getPages(source: MangaSource, url: String): List<MangaPage>?
|
||||
|
||||
fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>)
|
||||
|
||||
suspend fun getRelatedManga(source: MangaSource, url: String): List<Manga>?
|
||||
|
||||
fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred<List<Manga>>)
|
||||
|
||||
fun clear(source: MangaSource)
|
||||
|
||||
data class Key(
|
||||
val source: MangaSource,
|
||||
val url: String,
|
||||
)
|
||||
}
|
||||
@@ -2,18 +2,19 @@ package org.koitharu.kotatsu.core.cache
|
||||
|
||||
import androidx.collection.LruCache
|
||||
import java.util.concurrent.TimeUnit
|
||||
import org.koitharu.kotatsu.core.cache.MemoryContentCache.Key as CacheKey
|
||||
|
||||
class ExpiringLruCache<T>(
|
||||
val maxSize: Int,
|
||||
private val lifetime: Long,
|
||||
private val timeUnit: TimeUnit,
|
||||
) : Iterable<ContentCache.Key> {
|
||||
) : Iterable<CacheKey> {
|
||||
|
||||
private val cache = LruCache<ContentCache.Key, ExpiringValue<T>>(maxSize)
|
||||
private val cache = LruCache<CacheKey, ExpiringValue<T>>(maxSize)
|
||||
|
||||
override fun iterator(): Iterator<ContentCache.Key> = cache.snapshot().keys.iterator()
|
||||
override fun iterator(): Iterator<CacheKey> = cache.snapshot().keys.iterator()
|
||||
|
||||
operator fun get(key: ContentCache.Key): T? {
|
||||
operator fun get(key: CacheKey): T? {
|
||||
val value = cache[key] ?: return null
|
||||
if (value.isExpired) {
|
||||
cache.remove(key)
|
||||
@@ -21,7 +22,7 @@ class ExpiringLruCache<T>(
|
||||
return value.get()
|
||||
}
|
||||
|
||||
operator fun set(key: ContentCache.Key, value: T) {
|
||||
operator fun set(key: CacheKey, value: T) {
|
||||
cache.put(key, ExpiringValue(value, lifetime, timeUnit))
|
||||
}
|
||||
|
||||
@@ -33,7 +34,7 @@ class ExpiringLruCache<T>(
|
||||
cache.trimToSize(size)
|
||||
}
|
||||
|
||||
fun remove(key: ContentCache.Key) {
|
||||
fun remove(key: CacheKey) {
|
||||
cache.remove(key)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,48 +3,54 @@ package org.koitharu.kotatsu.core.cache
|
||||
import android.app.Application
|
||||
import android.content.ComponentCallbacks2
|
||||
import android.content.res.Configuration
|
||||
import org.koitharu.kotatsu.core.util.ext.isLowRamDevice
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
class MemoryContentCache(application: Application) : ContentCache, ComponentCallbacks2 {
|
||||
@Singleton
|
||||
class MemoryContentCache @Inject constructor(application: Application) : ComponentCallbacks2 {
|
||||
|
||||
private val isLowRam = application.isLowRamDevice()
|
||||
|
||||
init {
|
||||
application.registerComponentCallbacks(this)
|
||||
}
|
||||
|
||||
private val detailsCache = ExpiringLruCache<SafeDeferred<Manga>>(4, 5, TimeUnit.MINUTES)
|
||||
private val pagesCache = ExpiringLruCache<SafeDeferred<List<MangaPage>>>(4, 10, TimeUnit.MINUTES)
|
||||
private val relatedMangaCache = ExpiringLruCache<SafeDeferred<List<Manga>>>(4, 10, TimeUnit.MINUTES)
|
||||
private val detailsCache = ExpiringLruCache<SafeDeferred<Manga>>(if (isLowRam) 1 else 4, 5, TimeUnit.MINUTES)
|
||||
private val pagesCache =
|
||||
ExpiringLruCache<SafeDeferred<List<MangaPage>>>(if (isLowRam) 1 else 4, 10, TimeUnit.MINUTES)
|
||||
private val relatedMangaCache =
|
||||
ExpiringLruCache<SafeDeferred<List<Manga>>>(if (isLowRam) 1 else 3, 10, TimeUnit.MINUTES)
|
||||
|
||||
override val isCachingEnabled: Boolean = true
|
||||
|
||||
override suspend fun getDetails(source: MangaSource, url: String): Manga? {
|
||||
return detailsCache[ContentCache.Key(source, url)]?.awaitOrNull()
|
||||
suspend fun getDetails(source: MangaSource, url: String): Manga? {
|
||||
return detailsCache[Key(source, url)]?.awaitOrNull()
|
||||
}
|
||||
|
||||
override fun putDetails(source: MangaSource, url: String, details: SafeDeferred<Manga>) {
|
||||
detailsCache[ContentCache.Key(source, url)] = details
|
||||
fun putDetails(source: MangaSource, url: String, details: SafeDeferred<Manga>) {
|
||||
detailsCache[Key(source, url)] = details
|
||||
}
|
||||
|
||||
override suspend fun getPages(source: MangaSource, url: String): List<MangaPage>? {
|
||||
return pagesCache[ContentCache.Key(source, url)]?.awaitOrNull()
|
||||
suspend fun getPages(source: MangaSource, url: String): List<MangaPage>? {
|
||||
return pagesCache[Key(source, url)]?.awaitOrNull()
|
||||
}
|
||||
|
||||
override fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>) {
|
||||
pagesCache[ContentCache.Key(source, url)] = pages
|
||||
fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>) {
|
||||
pagesCache[Key(source, url)] = pages
|
||||
}
|
||||
|
||||
override suspend fun getRelatedManga(source: MangaSource, url: String): List<Manga>? {
|
||||
return relatedMangaCache[ContentCache.Key(source, url)]?.awaitOrNull()
|
||||
suspend fun getRelatedManga(source: MangaSource, url: String): List<Manga>? {
|
||||
return relatedMangaCache[Key(source, url)]?.awaitOrNull()
|
||||
}
|
||||
|
||||
override fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred<List<Manga>>) {
|
||||
relatedMangaCache[ContentCache.Key(source, url)] = related
|
||||
fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred<List<Manga>>) {
|
||||
relatedMangaCache[Key(source, url)] = related
|
||||
}
|
||||
|
||||
override fun clear(source: MangaSource) {
|
||||
fun clear(source: MangaSource) {
|
||||
clearCache(detailsCache, source)
|
||||
clearCache(pagesCache, source)
|
||||
clearCache(relatedMangaCache, source)
|
||||
@@ -81,4 +87,9 @@ class MemoryContentCache(application: Application) : ContentCache, ComponentCall
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class Key(
|
||||
val source: MangaSource,
|
||||
val url: String,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.cache
|
||||
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
|
||||
class StubContentCache : ContentCache {
|
||||
|
||||
override val isCachingEnabled: Boolean = false
|
||||
|
||||
override suspend fun getDetails(source: MangaSource, url: String): Manga? = null
|
||||
|
||||
override fun putDetails(source: MangaSource, url: String, details: SafeDeferred<Manga>) = Unit
|
||||
|
||||
override suspend fun getPages(source: MangaSource, url: String): List<MangaPage>? = null
|
||||
|
||||
override fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>) = Unit
|
||||
|
||||
override suspend fun getRelatedManga(source: MangaSource, url: String): List<Manga>? = null
|
||||
|
||||
override fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred<List<Manga>>) = Unit
|
||||
|
||||
override fun clear(source: MangaSource) = Unit
|
||||
}
|
||||
@@ -40,7 +40,7 @@ abstract class MangaDao {
|
||||
abstract suspend fun searchByTitle(query: String, source: String, limit: Int): List<MangaWithTags>
|
||||
|
||||
@Upsert
|
||||
abstract suspend fun upsert(manga: MangaEntity)
|
||||
protected abstract suspend fun upsert(manga: MangaEntity)
|
||||
|
||||
@Update(onConflict = OnConflictStrategy.IGNORE)
|
||||
abstract suspend fun update(manga: MangaEntity): Int
|
||||
|
||||
@@ -20,11 +20,8 @@ abstract class MangaSourcesDao {
|
||||
@Query("SELECT * FROM sources ORDER BY sort_key")
|
||||
abstract suspend fun findAll(): List<MangaSourceEntity>
|
||||
|
||||
@Query("SELECT * FROM sources WHERE enabled = 0 ORDER BY sort_key")
|
||||
abstract suspend fun findAllDisabled(): List<MangaSourceEntity>
|
||||
|
||||
@Query("SELECT * FROM sources WHERE enabled = 0")
|
||||
abstract fun observeDisabled(): Flow<List<MangaSourceEntity>>
|
||||
@Query("SELECT source FROM sources WHERE enabled = 1")
|
||||
abstract suspend fun findAllEnabledNames(): List<String>
|
||||
|
||||
@Query("SELECT * FROM sources ORDER BY sort_key")
|
||||
abstract fun observeAll(): Flow<List<MangaSourceEntity>>
|
||||
|
||||
@@ -28,9 +28,6 @@ interface TrackLogsDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(entity: TrackLogEntity): Long
|
||||
|
||||
@Query("DELETE FROM track_logs WHERE manga_id = :mangaId")
|
||||
suspend fun removeAll(mangaId: Long)
|
||||
|
||||
@Query("DELETE FROM track_logs WHERE manga_id NOT IN (SELECT manga_id FROM tracks)")
|
||||
suspend fun gc()
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import androidx.annotation.StringRes
|
||||
import androidx.collection.ArrayMap
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import okhttp3.Headers
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.alternatives.ui.AlternativesActivity
|
||||
import org.koitharu.kotatsu.browser.BrowserActivity
|
||||
@@ -30,7 +29,7 @@ class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
|
||||
private val activity: FragmentActivity?
|
||||
private val fragment: Fragment?
|
||||
private val sourceAuthContract: ActivityResultLauncher<MangaSource>
|
||||
private val cloudflareContract: ActivityResultLauncher<Pair<String, Headers?>>
|
||||
private val cloudflareContract: ActivityResultLauncher<CloudFlareProtectedException>
|
||||
|
||||
constructor(activity: FragmentActivity) {
|
||||
this.activity = activity
|
||||
@@ -55,7 +54,7 @@ class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
|
||||
}
|
||||
|
||||
suspend fun resolve(e: Throwable): Boolean = when (e) {
|
||||
is CloudFlareProtectedException -> resolveCF(e.url, e.headers)
|
||||
is CloudFlareProtectedException -> resolveCF(e)
|
||||
is AuthRequiredException -> resolveAuthException(e.source)
|
||||
is NotFoundException -> {
|
||||
openInBrowser(e.url)
|
||||
@@ -70,9 +69,9 @@ class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
|
||||
else -> false
|
||||
}
|
||||
|
||||
private suspend fun resolveCF(url: String, headers: Headers): Boolean = suspendCoroutine { cont ->
|
||||
private suspend fun resolveCF(e: CloudFlareProtectedException): Boolean = suspendCoroutine { cont ->
|
||||
continuations[CloudFlareActivity.TAG] = cont
|
||||
cloudflareContract.launch(url to headers)
|
||||
cloudflareContract.launch(e)
|
||||
}
|
||||
|
||||
private suspend fun resolveAuthException(source: MangaSource): Boolean = suspendCoroutine { cont ->
|
||||
|
||||
@@ -16,8 +16,10 @@ import org.koitharu.kotatsu.core.network.cookies.AndroidCookieJar
|
||||
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||
import org.koitharu.kotatsu.core.network.cookies.PreferencesCookieJar
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.ext.assertNotInMainThread
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Provider
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@@ -50,10 +52,12 @@ interface NetworkModule {
|
||||
@Singleton
|
||||
@BaseHttpClient
|
||||
fun provideBaseHttpClient(
|
||||
@ApplicationContext contextProvider: Provider<Context>,
|
||||
cache: Cache,
|
||||
cookieJar: CookieJar,
|
||||
settings: AppSettings,
|
||||
): OkHttpClient = OkHttpClient.Builder().apply {
|
||||
assertNotInMainThread()
|
||||
connectTimeout(20, TimeUnit.SECONDS)
|
||||
readTimeout(60, TimeUnit.SECONDS)
|
||||
writeTimeout(20, TimeUnit.SECONDS)
|
||||
@@ -62,7 +66,9 @@ interface NetworkModule {
|
||||
proxyAuthenticator(ProxyAuthenticator(settings))
|
||||
dns(DoHManager(cache, settings))
|
||||
if (settings.isSSLBypassEnabled) {
|
||||
bypassSSLErrors()
|
||||
disableCertificateVerification()
|
||||
} else {
|
||||
installExtraCertsificates(contextProvider.get())
|
||||
}
|
||||
cache(cache)
|
||||
addInterceptor(GZipInterceptor())
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.network
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import okhttp3.OkHttpClient
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import java.security.SecureRandom
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.SSLSocketFactory
|
||||
import javax.net.ssl.X509TrustManager
|
||||
|
||||
@SuppressLint("CustomX509TrustManager")
|
||||
fun OkHttpClient.Builder.bypassSSLErrors() = also { builder ->
|
||||
runCatching {
|
||||
val trustAllCerts = object : X509TrustManager {
|
||||
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) = Unit
|
||||
|
||||
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) = Unit
|
||||
|
||||
override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
|
||||
}
|
||||
val sslContext = SSLContext.getInstance("SSL")
|
||||
sslContext.init(null, arrayOf(trustAllCerts), SecureRandom())
|
||||
val sslSocketFactory: SSLSocketFactory = sslContext.socketFactory
|
||||
builder.sslSocketFactory(sslSocketFactory, trustAllCerts)
|
||||
builder.hostnameVerifier { _, _ -> true }
|
||||
}.onFailure {
|
||||
it.printStackTraceDebug()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package org.koitharu.kotatsu.core.network
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.res.AssetManager
|
||||
import android.util.Log
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.tls.HandshakeCertificates
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import java.security.SecureRandom
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.SSLSocketFactory
|
||||
import javax.net.ssl.X509TrustManager
|
||||
|
||||
@SuppressLint("CustomX509TrustManager")
|
||||
fun OkHttpClient.Builder.disableCertificateVerification() = also { builder ->
|
||||
runCatching {
|
||||
val trustAllCerts = object : X509TrustManager {
|
||||
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) = Unit
|
||||
|
||||
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) = Unit
|
||||
|
||||
override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
|
||||
}
|
||||
val sslContext = SSLContext.getInstance("SSL")
|
||||
sslContext.init(null, arrayOf(trustAllCerts), SecureRandom())
|
||||
val sslSocketFactory: SSLSocketFactory = sslContext.socketFactory
|
||||
builder.sslSocketFactory(sslSocketFactory, trustAllCerts)
|
||||
builder.hostnameVerifier { _, _ -> true }
|
||||
}.onFailure {
|
||||
it.printStackTraceDebug()
|
||||
}
|
||||
}
|
||||
|
||||
fun OkHttpClient.Builder.installExtraCertsificates(context: Context) = also { builder ->
|
||||
val certificatesBuilder = HandshakeCertificates.Builder()
|
||||
.addPlatformTrustedCertificates()
|
||||
val assets = context.assets.list("").orEmpty()
|
||||
for (path in assets) {
|
||||
if (path.endsWith(".pem")) {
|
||||
val cert = loadCert(context, path) ?: continue
|
||||
certificatesBuilder.addTrustedCertificate(cert)
|
||||
}
|
||||
}
|
||||
val certificates = certificatesBuilder.build()
|
||||
builder.sslSocketFactory(certificates.sslSocketFactory(), certificates.trustManager)
|
||||
}
|
||||
|
||||
private fun loadCert(context: Context, path: String): X509Certificate? = runCatching {
|
||||
val cf = CertificateFactory.getInstance("X.509")
|
||||
context.assets.open(path, AssetManager.ACCESS_STREAMING).use {
|
||||
cf.generateCertificate(it)
|
||||
} as X509Certificate
|
||||
}.onFailure { e ->
|
||||
e.printStackTraceDebug()
|
||||
}.onSuccess {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.i("ExtraCerts", "Loaded cert $path")
|
||||
}
|
||||
}.getOrNull()
|
||||
@@ -18,6 +18,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.db.TABLE_HISTORY
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
@@ -31,6 +32,7 @@ import org.koitharu.kotatsu.core.util.ext.source
|
||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||
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
|
||||
@@ -90,6 +92,14 @@ class AppShortcutManager @Inject constructor(
|
||||
false
|
||||
}
|
||||
|
||||
fun getMangaShortcuts(): Set<Long> {
|
||||
val shortcuts = ShortcutManagerCompat.getShortcuts(
|
||||
context,
|
||||
ShortcutManagerCompat.FLAG_MATCH_CACHED or ShortcutManagerCompat.FLAG_MATCH_PINNED or ShortcutManagerCompat.FLAG_MATCH_DYNAMIC,
|
||||
)
|
||||
return shortcuts.mapNotNullToSet { it.id.toLongOrNull() }
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
suspend fun await(): Boolean {
|
||||
return shortcutsUpdateJob?.join() != null
|
||||
@@ -150,7 +160,7 @@ class AppShortcutManager @Inject constructor(
|
||||
.build()
|
||||
}
|
||||
|
||||
private suspend fun buildShortcutInfo(source: MangaSource): ShortcutInfoCompat {
|
||||
private suspend fun buildShortcutInfo(source: MangaSource): ShortcutInfoCompat = withContext(Dispatchers.Default) {
|
||||
val icon = runCatchingCancellable {
|
||||
coil.execute(
|
||||
ImageRequest.Builder(context)
|
||||
@@ -163,7 +173,7 @@ class AppShortcutManager @Inject constructor(
|
||||
onSuccess = { IconCompat.createWithAdaptiveBitmap(it) },
|
||||
onFailure = { IconCompat.createWithResource(context, R.drawable.ic_shortcut_default) },
|
||||
)
|
||||
return ShortcutInfoCompat.Builder(context, source.name)
|
||||
ShortcutInfoCompat.Builder(context, source.name)
|
||||
.setShortLabel(source.title)
|
||||
.setLongLabel(source.title)
|
||||
.setIcon(icon)
|
||||
|
||||
@@ -22,11 +22,11 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.network.UserAgents
|
||||
import org.koitharu.kotatsu.parsers.util.SuspendLazy
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
@@ -38,15 +38,10 @@ class MangaLoaderContextImpl @Inject constructor(
|
||||
) : MangaLoaderContext() {
|
||||
|
||||
private var webViewCached: WeakReference<WebView>? = null
|
||||
|
||||
private val userAgentLazy = SuspendLazy {
|
||||
withContext(Dispatchers.Main) {
|
||||
obtainWebView().settings.userAgentString
|
||||
}.sanitizeHeaderValue()
|
||||
}
|
||||
private val webViewUserAgent by lazy { obtainWebViewUserAgent() }
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override suspend fun evaluateJs(script: String): String? = withContext(Dispatchers.Main) {
|
||||
override suspend fun evaluateJs(script: String): String? = withContext(Dispatchers.Main.immediate) {
|
||||
val webView = obtainWebView()
|
||||
suspendCoroutine { cont ->
|
||||
webView.evaluateJavascript(script) { result ->
|
||||
@@ -55,13 +50,7 @@ class MangaLoaderContextImpl @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDefaultUserAgent(): String = runCatching {
|
||||
runBlocking {
|
||||
userAgentLazy.get()
|
||||
}
|
||||
}.onFailure { e ->
|
||||
e.printStackTraceDebug()
|
||||
}.getOrDefault(UserAgents.FIREFOX_MOBILE)
|
||||
override fun getDefaultUserAgent(): String = webViewUserAgent
|
||||
|
||||
override fun getConfig(source: MangaSource): MangaSourceConfig {
|
||||
return SourceSettings(androidContext, source)
|
||||
@@ -86,4 +75,22 @@ class MangaLoaderContextImpl @Inject constructor(
|
||||
webViewCached = WeakReference(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun obtainWebViewUserAgent(): String {
|
||||
val mainDispatcher = Dispatchers.Main.immediate
|
||||
return if (!mainDispatcher.isDispatchNeeded(EmptyCoroutineContext)) {
|
||||
obtainWebViewUserAgentImpl()
|
||||
} else {
|
||||
runBlocking(mainDispatcher) {
|
||||
obtainWebViewUserAgentImpl()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private fun obtainWebViewUserAgentImpl() = runCatching {
|
||||
obtainWebView().settings.userAgentString.sanitizeHeaderValue()
|
||||
}.onFailure { e ->
|
||||
e.printStackTraceDebug()
|
||||
}.getOrDefault(UserAgents.FIREFOX_MOBILE)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.koitharu.kotatsu.core.parser
|
||||
|
||||
import androidx.annotation.AnyThread
|
||||
import org.koitharu.kotatsu.core.cache.ContentCache
|
||||
import org.koitharu.kotatsu.core.cache.MemoryContentCache
|
||||
import org.koitharu.kotatsu.core.network.MirrorSwitchInterceptor
|
||||
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
@@ -57,7 +57,7 @@ interface MangaRepository {
|
||||
class Factory @Inject constructor(
|
||||
private val localMangaRepository: LocalMangaRepository,
|
||||
private val loaderContext: MangaLoaderContext,
|
||||
private val contentCache: ContentCache,
|
||||
private val contentCache: MemoryContentCache,
|
||||
private val mirrorSwitchInterceptor: MirrorSwitchInterceptor,
|
||||
) {
|
||||
|
||||
|
||||
@@ -13,15 +13,15 @@ import okhttp3.Headers
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.cache.ContentCache
|
||||
import org.koitharu.kotatsu.core.cache.MemoryContentCache
|
||||
import org.koitharu.kotatsu.core.cache.SafeDeferred
|
||||
import org.koitharu.kotatsu.core.network.MirrorSwitchInterceptor
|
||||
import org.koitharu.kotatsu.core.prefs.SourceSettings
|
||||
import org.koitharu.kotatsu.core.util.MultiMutex
|
||||
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
|
||||
import org.koitharu.kotatsu.parsers.MangaParser
|
||||
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
|
||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||
import org.koitharu.kotatsu.parsers.exception.ParseException
|
||||
import org.koitharu.kotatsu.parsers.model.ContentRating
|
||||
import org.koitharu.kotatsu.parsers.model.Favicons
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
@@ -38,10 +38,14 @@ import java.util.Locale
|
||||
|
||||
class RemoteMangaRepository(
|
||||
private val parser: MangaParser,
|
||||
private val cache: ContentCache,
|
||||
private val cache: MemoryContentCache,
|
||||
private val mirrorSwitchInterceptor: MirrorSwitchInterceptor,
|
||||
) : MangaRepository, Interceptor {
|
||||
|
||||
private val detailsMutex = MultiMutex<Long>()
|
||||
private val relatedMangaMutex = MultiMutex<Long>()
|
||||
private val pagesMutex = MultiMutex<Long>()
|
||||
|
||||
override val source: MangaSource
|
||||
get() = parser.source
|
||||
|
||||
@@ -97,7 +101,7 @@ class RemoteMangaRepository(
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga = getDetails(manga, CachePolicy.ENABLED)
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> = pagesMutex.withLock(chapter.id) {
|
||||
cache.getPages(source, chapter.url)?.let { return it }
|
||||
val pages = asyncSafe {
|
||||
mirrorSwitchInterceptor.withMirrorSwitching {
|
||||
@@ -105,8 +109,8 @@ class RemoteMangaRepository(
|
||||
}
|
||||
}
|
||||
cache.putPages(source, chapter.url, pages)
|
||||
return pages.await()
|
||||
}
|
||||
pages
|
||||
}.await()
|
||||
|
||||
override suspend fun getPageUrl(page: MangaPage): String = mirrorSwitchInterceptor.withMirrorSwitching {
|
||||
parser.getPageUrl(page)
|
||||
@@ -124,16 +128,16 @@ class RemoteMangaRepository(
|
||||
parser.getFavicons()
|
||||
}
|
||||
|
||||
override suspend fun getRelated(seed: Manga): List<Manga> {
|
||||
override suspend fun getRelated(seed: Manga): List<Manga> = relatedMangaMutex.withLock(seed.id) {
|
||||
cache.getRelatedManga(source, seed.url)?.let { return it }
|
||||
val related = asyncSafe {
|
||||
parser.getRelatedManga(seed).filterNot { it.id == seed.id }
|
||||
}
|
||||
cache.putRelatedManga(source, seed.url, related)
|
||||
return related.await()
|
||||
}
|
||||
related
|
||||
}.await()
|
||||
|
||||
suspend fun getDetails(manga: Manga, cachePolicy: CachePolicy): Manga {
|
||||
suspend fun getDetails(manga: Manga, cachePolicy: CachePolicy): Manga = detailsMutex.withLock(manga.id) {
|
||||
if (cachePolicy.readEnabled) {
|
||||
cache.getDetails(source, manga.url)?.let { return it }
|
||||
}
|
||||
@@ -145,8 +149,8 @@ class RemoteMangaRepository(
|
||||
if (cachePolicy.writeEnabled) {
|
||||
cache.putDetails(source, manga.url, details)
|
||||
}
|
||||
return details.await()
|
||||
}
|
||||
details
|
||||
}.await()
|
||||
|
||||
suspend fun peekDetails(manga: Manga): Manga? {
|
||||
return cache.getDetails(source, manga.url)
|
||||
|
||||
@@ -170,10 +170,11 @@ class FaviconFetcher(
|
||||
|
||||
class Factory(
|
||||
context: Context,
|
||||
private val okHttpClient: OkHttpClient,
|
||||
okHttpClientLazy: Lazy<OkHttpClient>,
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
) : Fetcher.Factory<Uri> {
|
||||
|
||||
private val okHttpClient by okHttpClientLazy
|
||||
private val diskCache = lazy {
|
||||
val rootDir = context.externalCacheDir ?: context.cacheDir
|
||||
DiskCache.Builder()
|
||||
|
||||
@@ -252,7 +252,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
|
||||
val defaultDetailsTab: Int
|
||||
get() = if (isPagesTabEnabled) {
|
||||
val raw = prefs.getString(KEY_DETAILS_TAB, null)?.toIntOrNull() ?: 0
|
||||
val raw = prefs.getString(KEY_DETAILS_TAB, null)?.toIntOrNull() ?: -1
|
||||
if (raw == -1) {
|
||||
lastDetailsTab
|
||||
} else {
|
||||
@@ -281,7 +281,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
set(value) = prefs.edit { putEnumValue(KEY_SOURCES_ORDER, value) }
|
||||
|
||||
var isSourcesGridMode: Boolean
|
||||
get() = prefs.getBoolean(KEY_SOURCES_GRID, false)
|
||||
get() = prefs.getBoolean(KEY_SOURCES_GRID, true)
|
||||
set(value) = prefs.edit { putBoolean(KEY_SOURCES_GRID, value) }
|
||||
|
||||
val isNewSourcesTipEnabled: Boolean
|
||||
|
||||
@@ -3,33 +3,27 @@ package org.koitharu.kotatsu.core.ui
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.appcompat.widget.ActionBarContextView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
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.ActionModeDelegate
|
||||
import org.koitharu.kotatsu.core.ui.util.BaseActivityEntryPoint
|
||||
import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||
import org.koitharu.kotatsu.core.util.ext.isWebViewUnavailable
|
||||
|
||||
@Suppress("LeakingThis")
|
||||
@@ -98,6 +92,10 @@ abstract class BaseActivity<B : ViewBinding> :
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
if (supportFragmentManager.backStackEntryCount > 0) {
|
||||
supportFragmentManager.popBackStack()
|
||||
return false
|
||||
}
|
||||
dispatchNavigateUp()
|
||||
return true
|
||||
}
|
||||
@@ -123,32 +121,13 @@ abstract class BaseActivity<B : ViewBinding> :
|
||||
@CallSuper
|
||||
override fun onSupportActionModeStarted(mode: ActionMode) {
|
||||
super.onSupportActionModeStarted(mode)
|
||||
actionModeDelegate.onSupportActionModeStarted(mode)
|
||||
val actionModeColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
ColorUtils.compositeColors(
|
||||
ContextCompat.getColor(this, com.google.android.material.R.color.m3_appbar_overlay_color),
|
||||
getThemeColor(com.google.android.material.R.attr.colorSurface),
|
||||
)
|
||||
} else {
|
||||
ContextCompat.getColor(this, R.color.kotatsu_background)
|
||||
}
|
||||
defaultStatusBarColor = window.statusBarColor
|
||||
window.statusBarColor = actionModeColor
|
||||
val insets = ViewCompat.getRootWindowInsets(viewBinding.root)
|
||||
?.getInsets(WindowInsetsCompat.Type.systemBars()) ?: return
|
||||
findViewById<ActionBarContextView?>(androidx.appcompat.R.id.action_mode_bar).apply {
|
||||
setBackgroundColor(actionModeColor)
|
||||
updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = insets.top
|
||||
}
|
||||
}
|
||||
actionModeDelegate.onSupportActionModeStarted(mode, window)
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onSupportActionModeFinished(mode: ActionMode) {
|
||||
super.onSupportActionModeFinished(mode)
|
||||
actionModeDelegate.onSupportActionModeFinished(mode)
|
||||
window.statusBarColor = defaultStatusBarColor
|
||||
actionModeDelegate.onSupportActionModeFinished(mode, window)
|
||||
}
|
||||
|
||||
protected open fun dispatchNavigateUp() {
|
||||
@@ -181,6 +160,12 @@ abstract class BaseActivity<B : ViewBinding> :
|
||||
}
|
||||
}
|
||||
|
||||
@EntryPoint
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface BaseActivityEntryPoint {
|
||||
val settings: AppSettings
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val EXTRA_DATA = "data"
|
||||
|
||||
@@ -19,7 +19,7 @@ abstract class BaseFullscreenActivity<B : ViewBinding> :
|
||||
with(window) {
|
||||
systemUiController = SystemUiController(this)
|
||||
statusBarColor = Color.TRANSPARENT
|
||||
navigationBarColor = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
navigationBarColor = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) {
|
||||
ContextCompat.getColor(this@BaseFullscreenActivity, R.color.dim)
|
||||
} else {
|
||||
Color.TRANSPARENT
|
||||
|
||||
@@ -7,23 +7,21 @@ import android.graphics.ColorFilter
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||
import com.google.android.material.animation.ArgbEvaluatorCompat
|
||||
import org.koitharu.kotatsu.core.util.ext.animatorDurationScale
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||
import kotlin.math.abs
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
class AnimatedPlaceholderDrawable(context: Context) : Drawable(), Animatable, TimeAnimator.TimeListener {
|
||||
|
||||
private val colorLow = context.getThemeColor(materialR.attr.colorBackgroundFloating)
|
||||
private val colorHigh = context.getThemeColor(materialR.attr.colorSurfaceContainer)
|
||||
private val colorLow = context.getThemeColor(materialR.attr.colorSurfaceContainerLowest)
|
||||
private val colorHigh = context.getThemeColor(materialR.attr.colorSurfaceContainerHighest)
|
||||
private var currentColor: Int = colorLow
|
||||
private var alpha: Int = 255
|
||||
private val interpolator = FastOutSlowInInterpolator()
|
||||
private val period = 2000 * context.animatorDurationScale
|
||||
private val period = context.getAnimationDuration(R.integer.config_longAnimTime) * 2
|
||||
private val timeAnimator = TimeAnimator()
|
||||
|
||||
init {
|
||||
@@ -32,7 +30,7 @@ class AnimatedPlaceholderDrawable(context: Context) : Drawable(), Animatable, Ti
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
if (!isRunning) {
|
||||
if (!isRunning && period > 0) {
|
||||
updateColor()
|
||||
start()
|
||||
}
|
||||
@@ -40,23 +38,22 @@ class AnimatedPlaceholderDrawable(context: Context) : Drawable(), Animatable, Ti
|
||||
}
|
||||
|
||||
override fun setAlpha(alpha: Int) {
|
||||
this.alpha = alpha
|
||||
// this.alpha = alpha FIXME coil's crossfade
|
||||
}
|
||||
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) {
|
||||
throw UnsupportedOperationException("ColorFilter is not supported by PlaceholderDrawable")
|
||||
}
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) = Unit
|
||||
|
||||
@Suppress("DeprecatedCallableAddReplaceWith")
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun getOpacity(): Int = PixelFormat.OPAQUE
|
||||
|
||||
override fun getAlpha(): Int = alpha
|
||||
override fun getAlpha(): Int = 255
|
||||
|
||||
override fun onTimeUpdate(animation: TimeAnimator?, totalTime: Long, deltaTime: Long) {
|
||||
if (callback != null) {
|
||||
callback?.also {
|
||||
updateColor()
|
||||
invalidateSelf()
|
||||
}
|
||||
it.invalidateDrawable(this)
|
||||
} ?: stop()
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
@@ -64,19 +61,18 @@ class AnimatedPlaceholderDrawable(context: Context) : Drawable(), Animatable, Ti
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
timeAnimator.cancel()
|
||||
timeAnimator.end()
|
||||
}
|
||||
|
||||
override fun isRunning(): Boolean = timeAnimator.isStarted
|
||||
|
||||
private fun updateColor() {
|
||||
if (period <= 0f) {
|
||||
return
|
||||
}
|
||||
val ph = period / 2
|
||||
val fraction = abs((System.currentTimeMillis() % period) - ph) / ph.toFloat()
|
||||
var color = ArgbEvaluatorCompat.getInstance()
|
||||
currentColor = ArgbEvaluatorCompat.getInstance()
|
||||
.evaluate(interpolator.getInterpolation(fraction), colorLow, colorHigh)
|
||||
if (alpha != 255) {
|
||||
color = ColorUtils.setAlphaComponent(color, alpha)
|
||||
}
|
||||
currentColor = color
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.ui.image
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.ColorFilter
|
||||
import android.graphics.Outline
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Path
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.LayerDrawable
|
||||
import android.os.Build
|
||||
import androidx.annotation.ReturnThis
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColorStateList
|
||||
import org.koitharu.kotatsu.core.util.ext.resolveDp
|
||||
import org.koitharu.kotatsu.parsers.util.toIntUp
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
class CardDrawable(
|
||||
context: Context,
|
||||
private var corners: Int,
|
||||
) : Drawable() {
|
||||
|
||||
private val cornerSize = context.resources.resolveDp(12f)
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val cornersF = FloatArray(8)
|
||||
private val boundsF = RectF()
|
||||
private val color: ColorStateList
|
||||
private val path = Path()
|
||||
private var alpha = 255
|
||||
private var state: IntArray? = null
|
||||
private var horizontalInset: Int = 0
|
||||
|
||||
init {
|
||||
paint.style = Paint.Style.FILL
|
||||
color = context.getThemeColorStateList(materialR.attr.colorSurfaceContainerHighest)
|
||||
?: ColorStateList.valueOf(Color.TRANSPARENT)
|
||||
setCorners(corners)
|
||||
updateColor()
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
canvas.drawPath(path, paint)
|
||||
}
|
||||
|
||||
override fun setAlpha(alpha: Int) {
|
||||
this.alpha = alpha
|
||||
updateColor()
|
||||
}
|
||||
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) {
|
||||
paint.colorFilter = colorFilter
|
||||
}
|
||||
|
||||
override fun getColorFilter(): ColorFilter? = paint.colorFilter
|
||||
|
||||
override fun getOutline(outline: Outline) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
outline.setPath(path)
|
||||
} else if (path.isConvex) {
|
||||
outline.setConvexPath(path)
|
||||
}
|
||||
outline.alpha = 1f
|
||||
}
|
||||
|
||||
override fun getPadding(padding: Rect): Boolean {
|
||||
padding.set(
|
||||
horizontalInset,
|
||||
0,
|
||||
horizontalInset,
|
||||
0,
|
||||
)
|
||||
if (corners or TOP != 0) {
|
||||
padding.top += cornerSize.toIntUp()
|
||||
}
|
||||
if (corners or BOTTOM != 0) {
|
||||
padding.bottom += cornerSize.toIntUp()
|
||||
}
|
||||
return horizontalInset != 0
|
||||
}
|
||||
|
||||
override fun onStateChange(state: IntArray): Boolean {
|
||||
this.state = state
|
||||
if (color.isStateful) {
|
||||
updateColor()
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun getOpacity(): Int = PixelFormat.TRANSPARENT
|
||||
|
||||
override fun onBoundsChange(bounds: Rect) {
|
||||
super.onBoundsChange(bounds)
|
||||
boundsF.set(bounds)
|
||||
boundsF.inset(horizontalInset.toFloat(), 0f)
|
||||
path.reset()
|
||||
path.addRoundRect(boundsF, cornersF, Path.Direction.CW)
|
||||
path.close()
|
||||
}
|
||||
|
||||
@ReturnThis
|
||||
fun setCorners(corners: Int): CardDrawable {
|
||||
this.corners = corners
|
||||
val topLeft = if (corners and TOP_LEFT == TOP_LEFT) cornerSize else 0f
|
||||
val topRight = if (corners and TOP_RIGHT == TOP_RIGHT) cornerSize else 0f
|
||||
val bottomRight = if (corners and BOTTOM_RIGHT == BOTTOM_RIGHT) cornerSize else 0f
|
||||
val bottomLeft = if (corners and BOTTOM_LEFT == BOTTOM_LEFT) cornerSize else 0f
|
||||
cornersF[0] = topLeft
|
||||
cornersF[1] = topLeft
|
||||
cornersF[2] = topRight
|
||||
cornersF[3] = topRight
|
||||
cornersF[4] = bottomRight
|
||||
cornersF[5] = bottomRight
|
||||
cornersF[6] = bottomLeft
|
||||
cornersF[7] = bottomLeft
|
||||
invalidateSelf()
|
||||
return this
|
||||
}
|
||||
|
||||
fun setHorizontalInset(inset: Int) {
|
||||
horizontalInset = inset
|
||||
invalidateSelf()
|
||||
}
|
||||
|
||||
private fun updateColor() {
|
||||
paint.color = color.getColorForState(state, color.defaultColor)
|
||||
paint.alpha = alpha
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val TOP_LEFT = 1
|
||||
const val TOP_RIGHT = 2
|
||||
const val BOTTOM_LEFT = 4
|
||||
const val BOTTOM_RIGHT = 8
|
||||
|
||||
const val LEFT = TOP_LEFT or BOTTOM_LEFT
|
||||
const val TOP = TOP_LEFT or TOP_RIGHT
|
||||
const val RIGHT = TOP_RIGHT or BOTTOM_RIGHT
|
||||
const val BOTTOM = BOTTOM_LEFT or BOTTOM_RIGHT
|
||||
|
||||
const val NONE = 0
|
||||
const val ALL = TOP_LEFT or TOP_RIGHT or BOTTOM_RIGHT or BOTTOM_LEFT
|
||||
|
||||
fun from(d: Drawable?): CardDrawable? = when (d) {
|
||||
null -> null
|
||||
is CardDrawable -> d
|
||||
is LayerDrawable -> (0 until d.numberOfLayers).firstNotNullOfOrNull { i ->
|
||||
from(d.getDrawable(i))
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import coil.size.Dimension
|
||||
import coil.size.Size
|
||||
import coil.size.SizeResolver
|
||||
import coil.size.ViewSizeResolver
|
||||
import kotlinx.coroutines.CancellableContinuation
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlin.coroutines.resume
|
||||
@@ -16,24 +16,24 @@ private const val ASPECT_RATIO_HEIGHT = 18f
|
||||
private const val ASPECT_RATIO_WIDTH = 13f
|
||||
|
||||
class CoverSizeResolver(
|
||||
private val imageView: ImageView,
|
||||
) : SizeResolver {
|
||||
override val view: ImageView,
|
||||
) : ViewSizeResolver<ImageView> {
|
||||
|
||||
override suspend fun size(): Size {
|
||||
getSize()?.let { return it }
|
||||
return suspendCancellableCoroutine { cont ->
|
||||
val layoutListener = LayoutListener(cont)
|
||||
imageView.addOnLayoutChangeListener(layoutListener)
|
||||
view.addOnLayoutChangeListener(layoutListener)
|
||||
cont.invokeOnCancellation {
|
||||
imageView.removeOnLayoutChangeListener(layoutListener)
|
||||
view.removeOnLayoutChangeListener(layoutListener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSize(): Size? {
|
||||
val lp = imageView.layoutParams
|
||||
var width = getDimension(lp.width, imageView.width, imageView.paddingLeft + imageView.paddingRight)
|
||||
var height = getDimension(lp.height, imageView.height, imageView.paddingTop + imageView.paddingBottom)
|
||||
val lp = view.layoutParams
|
||||
var width = getDimension(lp.width, view.width, view.paddingLeft + view.paddingRight)
|
||||
var height = getDimension(lp.height, view.height, view.paddingTop + view.paddingBottom)
|
||||
if (width == null && height == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ class FastScroller @JvmOverloads constructor(
|
||||
private var hideScrollbar = true
|
||||
private var showBubble = true
|
||||
private var showBubbleAlways = false
|
||||
private var bubbleSize = BubbleSize.NORMAL
|
||||
private var bubbleSize = BubbleSize.SMALL
|
||||
private var bubbleImage: Drawable? = null
|
||||
private var handleImage: Drawable? = null
|
||||
private var trackImage: Drawable? = null
|
||||
@@ -91,7 +91,7 @@ class FastScroller @JvmOverloads constructor(
|
||||
|
||||
if (showBubbleAlways) {
|
||||
val targetPos = getRecyclerViewTargetPosition(y)
|
||||
sectionIndexer?.let { binding.bubble.text = it.getSectionText(recyclerView.context, targetPos) }
|
||||
sectionIndexer?.let { bindBubble(it.getSectionText(recyclerView.context, targetPos)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,7 +145,7 @@ class FastScroller @JvmOverloads constructor(
|
||||
showBubble = getBoolean(R.styleable.FastScrollRecyclerView_showBubble, showBubble)
|
||||
showBubbleAlways = getBoolean(R.styleable.FastScrollRecyclerView_showBubbleAlways, showBubbleAlways)
|
||||
showTrack = getBoolean(R.styleable.FastScrollRecyclerView_showTrack, showTrack)
|
||||
bubbleSize = getBubbleSize(R.styleable.FastScrollRecyclerView_bubbleSize, BubbleSize.NORMAL)
|
||||
bubbleSize = getBubbleSize(R.styleable.FastScrollRecyclerView_bubbleSize, bubbleSize)
|
||||
val textSize = getDimension(R.styleable.FastScrollRecyclerView_bubbleTextSize, bubbleSize.textSize)
|
||||
binding.bubble.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
|
||||
offset = getDimensionPixelOffset(R.styleable.FastScrollRecyclerView_scrollerOffset, offset)
|
||||
@@ -473,7 +473,7 @@ class FastScroller @JvmOverloads constructor(
|
||||
val layoutManager = recyclerView?.layoutManager ?: return
|
||||
val targetPos = getRecyclerViewTargetPosition(y)
|
||||
layoutManager.scrollToPosition(targetPos)
|
||||
if (showBubble) sectionIndexer?.let { binding.bubble.text = it.getSectionText(context, targetPos) }
|
||||
if (showBubble) sectionIndexer?.let { bindBubble(it.getSectionText(context, targetPos)) }
|
||||
}
|
||||
|
||||
private fun setViewPositions(y: Float) {
|
||||
@@ -535,6 +535,11 @@ class FastScroller @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindBubble(text: CharSequence?) {
|
||||
binding.bubble.text = text
|
||||
binding.bubble.alpha = if (text.isNullOrEmpty()) 0f else 1f
|
||||
}
|
||||
|
||||
private val BubbleSize.textSize
|
||||
@Px get() = resources.getDimension(textSizeId)
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@ package org.koitharu.kotatsu.core.ui.sheet
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@@ -16,15 +14,8 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDialog
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.appcompat.widget.ActionBarContextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
@@ -33,14 +24,12 @@ import com.google.android.material.sidesheet.SideSheetDialog
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
|
||||
|
||||
private var waitingForDismissAllowingStateLoss = false
|
||||
private var isFitToContentsDisabled = false
|
||||
private var defaultStatusBarColor = Color.TRANSPARENT
|
||||
|
||||
var viewBinding: B? = null
|
||||
private set
|
||||
@@ -105,40 +94,18 @@ abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
|
||||
|
||||
@CallSuper
|
||||
protected open fun dispatchSupportActionModeStarted(mode: ActionMode) {
|
||||
actionModeDelegate?.onSupportActionModeStarted(mode)
|
||||
val ctx = requireContext()
|
||||
val actionModeColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
ColorUtils.compositeColors(
|
||||
ContextCompat.getColor(ctx, com.google.android.material.R.color.m3_appbar_overlay_color),
|
||||
ctx.getThemeColor(com.google.android.material.R.attr.colorSurface),
|
||||
)
|
||||
} else {
|
||||
ContextCompat.getColor(ctx, R.color.kotatsu_surface)
|
||||
}
|
||||
dialog?.window?.let {
|
||||
defaultStatusBarColor = it.statusBarColor
|
||||
it.statusBarColor = actionModeColor
|
||||
}
|
||||
val insets = ViewCompat.getRootWindowInsets(requireView())
|
||||
?.getInsets(WindowInsetsCompat.Type.systemBars()) ?: return
|
||||
dialog?.window?.decorView?.findViewById<ActionBarContextView?>(androidx.appcompat.R.id.action_mode_bar)?.apply {
|
||||
setBackgroundColor(actionModeColor)
|
||||
updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = insets.top
|
||||
}
|
||||
}
|
||||
actionModeDelegate?.onSupportActionModeStarted(mode, dialog?.window)
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
protected open fun dispatchSupportActionModeFinished(mode: ActionMode) {
|
||||
actionModeDelegate?.onSupportActionModeFinished(mode)
|
||||
dialog?.window?.statusBarColor = defaultStatusBarColor
|
||||
actionModeDelegate?.onSupportActionModeFinished(mode, dialog?.window)
|
||||
}
|
||||
|
||||
fun addSheetCallback(callback: AdaptiveSheetCallback, lifecycleOwner: LifecycleOwner): Boolean {
|
||||
val b = behavior ?: return false
|
||||
b.addCallback(callback)
|
||||
val rootView = dialog?.findViewById<View>(materialR.id.design_bottom_sheet)
|
||||
val rootView = dialog?.findViewById(materialR.id.design_bottom_sheet)
|
||||
?: dialog?.findViewById(materialR.id.coordinator)
|
||||
?: view
|
||||
if (rootView != null) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.core.ui.util
|
||||
package org.koitharu.kotatsu.core.ui.sheet
|
||||
|
||||
import android.view.View
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
@@ -6,9 +6,8 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HALF_EXPANDED
|
||||
import org.koitharu.kotatsu.core.util.ext.doOnExpansionsChanged
|
||||
|
||||
class BottomSheetClollapseCallback(
|
||||
class BottomSheetCollapseCallback(
|
||||
private val behavior: BottomSheetBehavior<*>,
|
||||
) : OnBackPressedCallback(behavior.state == STATE_EXPANDED) {
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
package org.koitharu.kotatsu.core.ui.util
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.appcompat.widget.ActionBarContextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
class ActionModeDelegate : OnBackPressedCallback(false) {
|
||||
|
||||
private var activeActionMode: ActionMode? = null
|
||||
private var listeners: MutableList<ActionModeListener>? = null
|
||||
private var defaultStatusBarColor = Color.TRANSPARENT
|
||||
|
||||
val isActionModeStarted: Boolean
|
||||
get() = activeActionMode != null
|
||||
@@ -17,16 +31,40 @@ class ActionModeDelegate : OnBackPressedCallback(false) {
|
||||
finishActionMode()
|
||||
}
|
||||
|
||||
fun onSupportActionModeStarted(mode: ActionMode) {
|
||||
fun onSupportActionModeStarted(mode: ActionMode, window: Window?) {
|
||||
activeActionMode = mode
|
||||
isEnabled = true
|
||||
listeners?.forEach { it.onActionModeStarted(mode) }
|
||||
if (window != null) {
|
||||
val ctx = window.context
|
||||
val actionModeColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
ColorUtils.compositeColors(
|
||||
ContextCompat.getColor(ctx, materialR.color.m3_appbar_overlay_color),
|
||||
ctx.getThemeColor(materialR.attr.colorSurface),
|
||||
)
|
||||
} else {
|
||||
ContextCompat.getColor(ctx, R.color.kotatsu_surface)
|
||||
}
|
||||
defaultStatusBarColor = window.statusBarColor
|
||||
window.statusBarColor = actionModeColor
|
||||
val insets = ViewCompat.getRootWindowInsets(window.decorView)
|
||||
?.getInsets(WindowInsetsCompat.Type.systemBars()) ?: return
|
||||
window.decorView.findViewById<ActionBarContextView?>(androidx.appcompat.R.id.action_mode_bar)?.apply {
|
||||
setBackgroundColor(actionModeColor)
|
||||
updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = insets.top
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onSupportActionModeFinished(mode: ActionMode) {
|
||||
fun onSupportActionModeFinished(mode: ActionMode, window: Window?) {
|
||||
activeActionMode = null
|
||||
isEnabled = false
|
||||
listeners?.forEach { it.onActionModeFinished(mode) }
|
||||
if (window != null) {
|
||||
window.statusBarColor = defaultStatusBarColor
|
||||
}
|
||||
}
|
||||
|
||||
fun addListener(listener: ActionModeListener) {
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.ui.util
|
||||
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
|
||||
@EntryPoint
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface BaseActivityEntryPoint {
|
||||
val settings: AppSettings
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.koitharu.kotatsu.core.ui.util
|
||||
|
||||
import android.view.View
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.view.ancestors
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.Lifecycle.State.RESUMED
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
|
||||
class PagerNestedScrollHelper(
|
||||
private val recyclerView: RecyclerView,
|
||||
) : DefaultLifecycleObserver {
|
||||
|
||||
fun bind(lifecycleOwner: LifecycleOwner) {
|
||||
lifecycleOwner.lifecycle.addObserver(this)
|
||||
recyclerView.isNestedScrollingEnabled = lifecycleOwner.lifecycle.currentState.isAtLeast(RESUMED)
|
||||
}
|
||||
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
recyclerView.isNestedScrollingEnabled = false
|
||||
invalidateBottomSheetScrollTarget()
|
||||
}
|
||||
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
recyclerView.isNestedScrollingEnabled = true
|
||||
}
|
||||
|
||||
override fun onDestroy(owner: LifecycleOwner) {
|
||||
owner.lifecycle.removeObserver(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Here we need to invalidate the `nestedScrollingChildRef` of the [BottomSheetBehavior]
|
||||
*/
|
||||
private fun invalidateBottomSheetScrollTarget() {
|
||||
var handleCoordinator = false
|
||||
for (parent in recyclerView.ancestors) {
|
||||
if (handleCoordinator && parent is CoordinatorLayout) {
|
||||
parent.requestLayout()
|
||||
break
|
||||
}
|
||||
val lp = (parent as? View)?.layoutParams ?: continue
|
||||
if (lp is CoordinatorLayout.LayoutParams && lp.behavior is BottomSheetBehavior<*>) {
|
||||
handleCoordinator = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.ui.util
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.shape.MaterialShapeDrawable
|
||||
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
class StatusBarDimHelper : AppBarLayout.OnOffsetChangedListener {
|
||||
|
||||
private var animator: ValueAnimator? = null
|
||||
private val interpolator = AccelerateDecelerateInterpolator()
|
||||
|
||||
override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) {
|
||||
val foreground = appBarLayout.statusBarForeground ?: return
|
||||
val start = foreground.alpha
|
||||
val collapsed = verticalOffset != 0
|
||||
val end = if (collapsed) 255 else 0
|
||||
animator?.cancel()
|
||||
if (start == end) {
|
||||
animator = null
|
||||
return
|
||||
}
|
||||
animator = ValueAnimator.ofInt(start, end).apply {
|
||||
duration = appBarLayout.context.getAnimationDuration(materialR.integer.app_bar_elevation_anim_duration)
|
||||
interpolator = this@StatusBarDimHelper.interpolator
|
||||
addUpdateListener {
|
||||
foreground.alpha = it.animatedValue as Int
|
||||
}
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
fun attachToAppBar(appBarLayout: AppBarLayout) {
|
||||
appBarLayout.addOnOffsetChangedListener(this)
|
||||
appBarLayout.statusBarForeground =
|
||||
MaterialShapeDrawable.createWithElevationOverlay(appBarLayout.context).apply {
|
||||
alpha = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,23 +33,30 @@ sealed class SystemUiController(
|
||||
private class LegacyImpl(window: Window) : SystemUiController(window) {
|
||||
|
||||
override fun setSystemUiVisible(value: Boolean) {
|
||||
val flags = window.decorView.systemUiVisibility
|
||||
window.decorView.systemUiVisibility = if (value) {
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
(flags and LEGACY_FLAGS_HIDDEN.inv()) or LEGACY_FLAGS_VISIBLE
|
||||
} else {
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN or
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
(flags and LEGACY_FLAGS_VISIBLE.inv()) or LEGACY_FLAGS_HIDDEN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private const val LEGACY_FLAGS_VISIBLE = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private const val LEGACY_FLAGS_HIDDEN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN or
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
|
||||
operator fun invoke(window: Window): SystemUiController =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
Api30Impl(window)
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.ui.widgets
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import androidx.viewpager.widget.ViewPager
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
class EnhancedViewPager @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
) : ViewPager(context, attrs) {
|
||||
|
||||
var isUserInputEnabled: Boolean = true
|
||||
set(value) {
|
||||
field = value
|
||||
if (!value) {
|
||||
cancelPendingInputEvents()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
return isUserInputEnabled && super.onTouchEvent(event)
|
||||
}
|
||||
|
||||
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
|
||||
return try {
|
||||
isUserInputEnabled && super.onInterceptTouchEvent(event)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
e.printStackTraceDebug()
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.koitharu.kotatsu.core.ui.widgets
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.annotation.AttrRes
|
||||
import com.google.android.material.textview.MaterialTextView
|
||||
|
||||
class MultilineEllipsizeTextView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
@AttrRes defStyleAttr: Int = android.R.attr.textViewStyle,
|
||||
) : MaterialTextView(context, attrs, defStyleAttr) {
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
val lh = lineHeight
|
||||
maxLines = if (lh > 0) h / lh else 1
|
||||
}
|
||||
}
|
||||
@@ -16,11 +16,13 @@ import android.widget.TextView
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.widget.LinearLayoutCompat
|
||||
import androidx.core.content.withStyledAttributes
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.view.children
|
||||
import androidx.core.widget.TextViewCompat
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColorStateList
|
||||
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
|
||||
import org.koitharu.kotatsu.core.util.ext.resolveDp
|
||||
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
|
||||
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
||||
@@ -37,10 +39,14 @@ class ProgressButton @JvmOverloads constructor(
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
|
||||
private var progress = 0f
|
||||
private var targetProgress = 0f
|
||||
private var colorBase: ColorStateList = ColorStateList.valueOf(Color.TRANSPARENT)
|
||||
private var colorProgress: ColorStateList = ColorStateList.valueOf(Color.TRANSPARENT)
|
||||
private var progressAnimator: ValueAnimator? = null
|
||||
|
||||
private var colorBaseCurrent = colorProgress.defaultColor
|
||||
private var colorProgressCurrent = colorProgress.defaultColor
|
||||
|
||||
var title: CharSequence?
|
||||
get() = textViewTitle.textAndVisible
|
||||
set(value) {
|
||||
@@ -97,10 +103,19 @@ class ProgressButton @JvmOverloads constructor(
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
canvas.drawColor(colorBase.getColorForState(drawableState, colorBase.defaultColor))
|
||||
paint.color = colorProgress.getColorForState(drawableState, colorProgress.defaultColor)
|
||||
paint.alpha = 84 // 255 * 0.33F
|
||||
canvas.drawRect(0f, 0f, width * progress, height.toFloat(), paint)
|
||||
canvas.drawColor(colorBaseCurrent)
|
||||
if (progress > 0f) {
|
||||
canvas.drawRect(0f, 0f, width * progress, height.toFloat(), paint)
|
||||
}
|
||||
}
|
||||
|
||||
override fun drawableStateChanged() {
|
||||
super.drawableStateChanged()
|
||||
val state = drawableState
|
||||
colorBaseCurrent = colorBase.getColorForState(state, colorBase.defaultColor)
|
||||
colorProgressCurrent = colorProgress.getColorForState(state, colorProgress.defaultColor)
|
||||
colorProgressCurrent = ColorUtils.setAlphaComponent(colorProgressCurrent, 84 /* 255 * 0.33F */)
|
||||
paint.color = colorProgressCurrent
|
||||
}
|
||||
|
||||
override fun setGravity(gravity: Int) {
|
||||
@@ -116,8 +131,10 @@ class ProgressButton @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
override fun onAnimationUpdate(animation: ValueAnimator) {
|
||||
progress = animation.animatedValue as Float
|
||||
invalidate()
|
||||
if (animation === progressAnimator) {
|
||||
progress = animation.animatedValue as Float
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
fun setTitle(@StringRes titleResId: Int) {
|
||||
@@ -129,19 +146,25 @@ class ProgressButton @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
fun setProgress(value: Float, animate: Boolean) {
|
||||
progressAnimator?.cancel()
|
||||
if (animate) {
|
||||
val prevAnimator = progressAnimator
|
||||
if (animate && context.isAnimationsEnabled) {
|
||||
if (value == targetProgress) {
|
||||
return
|
||||
}
|
||||
targetProgress = value
|
||||
progressAnimator = ValueAnimator.ofFloat(progress, value).apply {
|
||||
duration = context.getAnimationDuration(android.R.integer.config_shortAnimTime)
|
||||
duration = context.getAnimationDuration(android.R.integer.config_mediumAnimTime)
|
||||
interpolator = AccelerateDecelerateInterpolator()
|
||||
addUpdateListener(this@ProgressButton)
|
||||
start()
|
||||
}
|
||||
progressAnimator?.start()
|
||||
} else {
|
||||
progressAnimator = null
|
||||
progress = value
|
||||
targetProgress = value
|
||||
invalidate()
|
||||
}
|
||||
prevAnimator?.cancel()
|
||||
}
|
||||
|
||||
private fun applyGravity() {
|
||||
|
||||
@@ -11,6 +11,7 @@ import android.view.ViewPropertyAnimator
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.customview.view.AbsSavedState
|
||||
import androidx.interpolator.view.animation.FastOutLinearInInterpolator
|
||||
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
|
||||
@@ -47,6 +48,9 @@ class SlidingBottomNavigationView @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
val isShownOrShowing: Boolean
|
||||
get() = isVisible && currentState == STATE_UP
|
||||
|
||||
override fun getBehavior(): CoordinatorLayout.Behavior<*> {
|
||||
return behavior
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package org.koitharu.kotatsu.core.util
|
||||
|
||||
import androidx.collection.ArrayMap
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
|
||||
class MultiMutex<T : Any> : Set<T> {
|
||||
|
||||
@@ -10,12 +12,12 @@ class MultiMutex<T : Any> : Set<T> {
|
||||
override val size: Int
|
||||
get() = delegates.size
|
||||
|
||||
override fun contains(element: T): Boolean {
|
||||
return delegates.containsKey(element)
|
||||
override fun contains(element: T): Boolean = synchronized(delegates) {
|
||||
delegates.containsKey(element)
|
||||
}
|
||||
|
||||
override fun containsAll(elements: Collection<T>): Boolean {
|
||||
return elements.all { x -> delegates.containsKey(x) }
|
||||
override fun containsAll(elements: Collection<T>): Boolean = synchronized(delegates) {
|
||||
elements.all { x -> delegates.containsKey(x) }
|
||||
}
|
||||
|
||||
override fun isEmpty(): Boolean {
|
||||
@@ -40,4 +42,16 @@ class MultiMutex<T : Any> : Set<T> {
|
||||
delegates.remove(element)?.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
suspend inline fun <R> withLock(element: T, block: () -> R): R {
|
||||
contract {
|
||||
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
return try {
|
||||
lock(element)
|
||||
block()
|
||||
} finally {
|
||||
unlock(element)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,11 +37,14 @@ import androidx.appcompat.app.AppCompatDialog
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import androidx.webkit.WebViewCompat
|
||||
import androidx.webkit.WebViewFeature
|
||||
import androidx.work.CoroutineWorker
|
||||
import com.google.android.material.elevation.ElevationOverlayProvider
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -59,7 +62,6 @@ import okio.use
|
||||
import org.json.JSONException
|
||||
import org.jsoup.internal.StringUtil.StringJoiner
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import org.xmlpull.v1.XmlPullParserException
|
||||
@@ -139,6 +141,9 @@ fun Window.setNavigationBarTransparentCompat(context: Context, elevation: Float,
|
||||
!context.getSystemBoolean("config_navBarNeedsScrim", true)
|
||||
) {
|
||||
Color.TRANSPARENT
|
||||
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) {
|
||||
val baseColor = context.getThemeColor(android.R.attr.navigationBarColor)
|
||||
ColorUtils.setAlphaComponent(baseColor, (Color.alpha(baseColor) * alphaFactor).toInt())
|
||||
} else {
|
||||
// Set navbar scrim 70% of navigationBarColor
|
||||
ElevationOverlayProvider(context).compositeOverlayIfNeeded(
|
||||
@@ -263,6 +268,9 @@ fun WebView.configureForParser(userAgentOverride: String?) = with(settings) {
|
||||
javaScriptEnabled = true
|
||||
domStorageEnabled = true
|
||||
mediaPlaybackRequiresUserGesture = false
|
||||
if (WebViewFeature.isFeatureSupported(WebViewFeature.MUTE_AUDIO)) {
|
||||
WebViewCompat.setAudioMuted(this@configureForParser, true)
|
||||
}
|
||||
databaseEnabled = true
|
||||
if (userAgentOverride != null) {
|
||||
userAgentString = userAgentOverride
|
||||
|
||||
@@ -47,15 +47,6 @@ fun ImageResult.getDrawableOrThrow() = when (this) {
|
||||
is ErrorResult -> throw throwable
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
"",
|
||||
ReplaceWith(
|
||||
"getDrawableOrThrow().toBitmap()",
|
||||
"androidx.core.graphics.drawable.toBitmap",
|
||||
),
|
||||
)
|
||||
fun ImageResult.requireBitmap() = getDrawableOrThrow().toBitmap()
|
||||
|
||||
fun ImageResult.toBitmapOrNull() = when (this) {
|
||||
is SuccessResult -> try {
|
||||
drawable.toBitmap()
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.util.ext
|
||||
|
||||
import android.app.Activity
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.Display
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val Activity.displayCompat: Display
|
||||
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
display ?: windowManager.defaultDisplay
|
||||
} else {
|
||||
windowManager.defaultDisplay
|
||||
}
|
||||
|
||||
fun Activity.getDisplaySize(): Rect {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
windowManager.currentWindowMetrics.bounds
|
||||
} else {
|
||||
val dm = DisplayMetrics()
|
||||
displayCompat.getRealMetrics(dm)
|
||||
Rect(0, 0, dm.widthPixels, dm.heightPixels)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.koitharu.kotatsu.core.util.ext
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
@@ -28,11 +27,16 @@ fun <T> Flow<T>.observe(owner: LifecycleOwner, minState: Lifecycle.State, collec
|
||||
}
|
||||
|
||||
fun <T> Flow<Event<T>?>.observeEvent(owner: LifecycleOwner, collector: FlowCollector<T>) {
|
||||
observeEvent(owner, Lifecycle.State.STARTED, collector)
|
||||
}
|
||||
|
||||
fun <T> Flow<Event<T>?>.observeEvent(owner: LifecycleOwner, minState: Lifecycle.State, collector: FlowCollector<T>) {
|
||||
owner.lifecycleScope.launch {
|
||||
owner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
owner.repeatOnLifecycle(minState) {
|
||||
collect {
|
||||
it?.consume(collector)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ class DetailsLoadUseCase @Inject constructor(
|
||||
send(MangaDetails(manga, null, null, false))
|
||||
try {
|
||||
val details = getDetails(manga)
|
||||
launch { updateTracker(manga) }
|
||||
launch { updateTracker(details) }
|
||||
send(MangaDetails(details, local?.peek(), details.description?.parseAsHtml(withImages = false), false))
|
||||
send(MangaDetails(details, local?.await(), details.description?.parseAsHtml(withImages = true), true))
|
||||
} catch (e: IOException) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
import org.koitharu.kotatsu.core.cache.ContentCache
|
||||
import org.koitharu.kotatsu.core.cache.MemoryContentCache
|
||||
import org.koitharu.kotatsu.core.model.findById
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableChapter
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
@@ -27,7 +27,7 @@ class MangaPrefetchService : CoroutineIntentService() {
|
||||
lateinit var mangaRepositoryFactory: MangaRepository.Factory
|
||||
|
||||
@Inject
|
||||
lateinit var cache: ContentCache
|
||||
lateinit var cache: MemoryContentCache
|
||||
|
||||
@Inject
|
||||
lateinit var historyRepository: HistoryRepository
|
||||
@@ -110,17 +110,14 @@ class MangaPrefetchService : CoroutineIntentService() {
|
||||
}
|
||||
|
||||
private fun isPrefetchAvailable(context: Context, source: MangaSource?): Boolean {
|
||||
if (source == MangaSource.LOCAL) {
|
||||
return false
|
||||
}
|
||||
if (context.isPowerSaveMode()) {
|
||||
if (source == MangaSource.LOCAL || context.isPowerSaveMode()) {
|
||||
return false
|
||||
}
|
||||
val entryPoint = EntryPointAccessors.fromApplication(
|
||||
context,
|
||||
PrefetchCompanionEntryPoint::class.java,
|
||||
)
|
||||
return entryPoint.contentCache.isCachingEnabled && entryPoint.settings.isContentPrefetchEnabled
|
||||
return entryPoint.settings.isContentPrefetchEnabled
|
||||
}
|
||||
|
||||
private fun tryStart(context: Context, intent: Intent) {
|
||||
|
||||
@@ -3,12 +3,10 @@ package org.koitharu.kotatsu.details.service
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import org.koitharu.kotatsu.core.cache.ContentCache
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
|
||||
@EntryPoint
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface PrefetchCompanionEntryPoint {
|
||||
val settings: AppSettings
|
||||
val contentCache: ContentCache
|
||||
}
|
||||
|
||||
@@ -72,8 +72,7 @@ fun MangaDetails.mapChapters(
|
||||
fun List<ChapterListItem>.withVolumeHeaders(context: Context): List<ListModel> {
|
||||
var prevVolume = 0
|
||||
val result = ArrayList<ListModel>((size * 1.4).toInt())
|
||||
var groupPos: Byte = 0
|
||||
for ((index, item) in this.withIndex()) {
|
||||
for (item in this) {
|
||||
val chapter = item.chapter
|
||||
if (chapter.volume != prevVolume) {
|
||||
val text = if (chapter.volume == 0) {
|
||||
@@ -83,19 +82,8 @@ fun List<ChapterListItem>.withVolumeHeaders(context: Context): List<ListModel> {
|
||||
}
|
||||
result.add(ListHeader(text))
|
||||
prevVolume = chapter.volume
|
||||
groupPos = ChapterListItem.GROUP_START
|
||||
} else if (groupPos == ChapterListItem.GROUP_START) {
|
||||
groupPos = ChapterListItem.GROUP_MIDDLE
|
||||
}
|
||||
if (groupPos != 0.toByte()) {
|
||||
val next = this.getOrNull(index + 1)
|
||||
if (next == null || next.chapter.volume != prevVolume) {
|
||||
groupPos = ChapterListItem.GROUP_END
|
||||
}
|
||||
result.add(item.copy(groupPosition = groupPos))
|
||||
} else {
|
||||
result.add(item)
|
||||
}
|
||||
result.add(item)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -53,12 +53,11 @@ import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||
import org.koitharu.kotatsu.core.ui.image.ChipIconTarget
|
||||
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.util.BottomSheetClollapseCallback
|
||||
import org.koitharu.kotatsu.core.ui.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.ViewBadge
|
||||
import org.koitharu.kotatsu.core.util.ext.crossfade
|
||||
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
|
||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||
@@ -124,7 +123,6 @@ class DetailsActivity :
|
||||
|
||||
private val viewModel: DetailsViewModel by viewModels()
|
||||
|
||||
private lateinit var chaptersBadge: ViewBadge
|
||||
private lateinit var menuProvider: DetailsMenuProvider
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -156,13 +154,11 @@ class DetailsActivity :
|
||||
viewBinding.chipsTags.onChipClickListener = this
|
||||
TitleScrollCoordinator(viewBinding.textViewTitle).attach(viewBinding.scrollView)
|
||||
viewBinding.containerBottomSheet?.let { BottomSheetBehavior.from(it) }?.let { behavior ->
|
||||
onBackPressedDispatcher.addCallback(BottomSheetClollapseCallback(behavior))
|
||||
onBackPressedDispatcher.addCallback(BottomSheetCollapseCallback(behavior))
|
||||
}
|
||||
chaptersBadge = ViewBadge(viewBinding.buttonRead, this)
|
||||
|
||||
viewModel.details.filterNotNull().observe(this, ::onMangaUpdated)
|
||||
viewModel.onMangaRemoved.observeEvent(this, ::onMangaRemoved)
|
||||
viewModel.newChaptersCount.observe(this, ::onNewChaptersChanged)
|
||||
viewModel.onError
|
||||
.filterNot { ChaptersPagesSheet.isShown(supportFragmentManager) }
|
||||
.observeEvent(this, DetailsErrorObserver(this, viewModel, exceptionResolver))
|
||||
@@ -185,7 +181,8 @@ class DetailsActivity :
|
||||
viewModel.isStatsAvailable.observe(this, menuInvalidator)
|
||||
viewModel.remoteManga.observe(this, menuInvalidator)
|
||||
viewModel.branches.observe(this) {
|
||||
viewBinding.infoLayout.chipBranch.isVisible = it.size > 1
|
||||
viewBinding.infoLayout.chipBranch.isVisible = it.size > 1 || !it.firstOrNull()?.name.isNullOrEmpty()
|
||||
viewBinding.infoLayout.chipBranch.isCloseIconVisible = it.size > 1
|
||||
}
|
||||
viewModel.chapters.observe(this, PrefetchObserver(this))
|
||||
viewModel.onDownloadStarted
|
||||
@@ -379,15 +376,6 @@ class DetailsActivity :
|
||||
chip.textAndVisible = time?.formatShort(chip.resources)
|
||||
}
|
||||
|
||||
private fun onDescriptionChanged(description: CharSequence?) {
|
||||
val tv = viewBinding.textViewDescription
|
||||
if (description.isNullOrBlank()) {
|
||||
tv.setText(R.string.no_description)
|
||||
} else {
|
||||
tv.text = description
|
||||
}
|
||||
}
|
||||
|
||||
private fun onLocalSizeChanged(size: Long) {
|
||||
val chip = viewBinding.infoLayout.chipSize
|
||||
if (size == 0L) {
|
||||
@@ -455,7 +443,7 @@ class DetailsActivity :
|
||||
loadCover(manga)
|
||||
textViewTitle.text = manga.title
|
||||
textViewSubtitle.textAndVisible = manga.altTitle
|
||||
infoLayout.chipAuthor.textAndVisible = manga.author
|
||||
infoLayout.chipAuthor.textAndVisible = manga.author?.ellipsize(AUTHOR_LABEL_LIMIT)
|
||||
if (manga.hasRating) {
|
||||
ratingBar.rating = manga.rating * ratingBar.numStars
|
||||
ratingBar.isVisible = true
|
||||
@@ -545,18 +533,19 @@ class DetailsActivity :
|
||||
info.totalChapters == -1 -> getString(R.string.error_occurred)
|
||||
else -> resources.getQuantityString(R.plurals.chapters, info.totalChapters, info.totalChapters)
|
||||
}
|
||||
buttonRead.setProgress(info.history?.percent?.coerceIn(0f, 1f) ?: 0f, true)
|
||||
val isFirstCall = buttonRead.tag == null
|
||||
buttonRead.tag = Unit
|
||||
buttonRead.setProgress(info.history?.percent?.coerceIn(0f, 1f) ?: 0f, !isFirstCall)
|
||||
buttonDownload?.isEnabled = info.isValid && info.canDownload
|
||||
buttonRead.isEnabled = info.isValid
|
||||
}
|
||||
|
||||
private fun onNewChaptersChanged(count: Int) {
|
||||
chaptersBadge.counter = count
|
||||
}
|
||||
|
||||
private fun showBranchPopupMenu(v: View) {
|
||||
val menu = PopupMenu(v.context, v)
|
||||
val branches = viewModel.branches.value
|
||||
if (branches.size <= 1) {
|
||||
return
|
||||
}
|
||||
val menu = PopupMenu(v.context, v)
|
||||
for ((i, branch) in branches.withIndex()) {
|
||||
val title = buildSpannedString {
|
||||
if (branch.isCurrent) {
|
||||
@@ -600,8 +589,7 @@ class DetailsActivity :
|
||||
|
||||
private fun openReader(isIncognitoMode: Boolean) {
|
||||
val manga = viewModel.manga.value ?: return
|
||||
val chapterId = viewModel.historyInfo.value.history?.chapterId
|
||||
if (chapterId != null && manga.chapters?.none { x -> x.id == chapterId } == true) {
|
||||
if (viewModel.historyInfo.value.isChapterMissing) {
|
||||
Snackbar.make(viewBinding.scrollView, R.string.chapter_is_missing, Snackbar.LENGTH_SHORT)
|
||||
.show()
|
||||
} else {
|
||||
@@ -680,6 +668,7 @@ class DetailsActivity :
|
||||
companion object {
|
||||
|
||||
private const val FAV_LABEL_LIMIT = 10
|
||||
private const val AUTHOR_LABEL_LIMIT = 16
|
||||
|
||||
fun newIntent(context: Context, manga: Manga): Intent {
|
||||
return Intent(context, DetailsActivity::class.java)
|
||||
|
||||
@@ -19,7 +19,6 @@ import org.koitharu.kotatsu.browser.BrowserActivity
|
||||
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.util.ShareHelper
|
||||
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesSheet
|
||||
import org.koitharu.kotatsu.download.ui.dialog.DownloadOption
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet
|
||||
@@ -135,7 +134,6 @@ class DetailsMenuProvider(
|
||||
is DownloadOption.WholeManga -> null
|
||||
is DownloadOption.SelectionHint -> {
|
||||
viewModel.startChaptersSelection()
|
||||
ChaptersPagesSheet.show(activity.supportFragmentManager, ChaptersPagesSheet.TAB_CHAPTERS)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +88,6 @@ class DetailsViewModel @Inject constructor(
|
||||
val mangaId = intent.mangaId
|
||||
|
||||
val onActionDone = MutableEventFlow<ReversibleAction>()
|
||||
val onShowTip = MutableEventFlow<Unit>()
|
||||
val onSelectChapter = MutableEventFlow<Long>()
|
||||
val onDownloadStarted = MutableEventFlow<Unit>()
|
||||
|
||||
@@ -161,11 +160,6 @@ class DetailsViewModel @Inject constructor(
|
||||
}
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(5000), 0L)
|
||||
|
||||
@Deprecated("")
|
||||
val description = details
|
||||
.map { it?.description }
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, null)
|
||||
|
||||
val onMangaRemoved = MutableEventFlow<Manga>()
|
||||
val isScrobblingAvailable: Boolean
|
||||
get() = scrobblers.any { it.isAvailable }
|
||||
@@ -179,7 +173,7 @@ class DetailsViewModel @Inject constructor(
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, emptyList())
|
||||
|
||||
val branches: StateFlow<List<MangaBranch>> = combine(
|
||||
details,
|
||||
@@ -226,7 +220,7 @@ class DetailsViewModel @Inject constructor(
|
||||
chaptersQuery,
|
||||
) { list, reversed, query ->
|
||||
(if (reversed) list.asReversed() else list).filterSearch(query)
|
||||
}.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
val readingTime = combine(
|
||||
details,
|
||||
@@ -234,7 +228,7 @@ class DetailsViewModel @Inject constructor(
|
||||
history,
|
||||
) { m, b, h ->
|
||||
readingTimeUseCase.invoke(m, b, h)
|
||||
}.stateIn(viewModelScope, SharingStarted.Lazily, null)
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, null)
|
||||
|
||||
val selectedBranchValue: String?
|
||||
get() = selectedBranch.value
|
||||
|
||||
@@ -26,18 +26,9 @@ fun chapterListItemAD(
|
||||
itemView.setOnClickListener(eventListener)
|
||||
itemView.setOnLongClickListener(eventListener)
|
||||
|
||||
bind { payloads ->
|
||||
bind {
|
||||
binding.textViewTitle.text = item.chapter.name
|
||||
binding.textViewDescription.textAndVisible = item.description
|
||||
itemView.setBackgroundResource(
|
||||
when {
|
||||
item.isGroupStart && item.isGroupEnd -> R.drawable.bg_card_full
|
||||
item.isGroupStart -> R.drawable.bg_card_top
|
||||
item.isGroupMiddle -> R.drawable.bg_card_none
|
||||
item.isGroupEnd -> R.drawable.bg_card_bottom
|
||||
else -> R.drawable.list_selector
|
||||
},
|
||||
)
|
||||
when {
|
||||
item.isCurrent -> {
|
||||
binding.textViewTitle.drawableStart = ContextCompat.getDrawable(context, R.drawable.ic_current_chapter)
|
||||
|
||||
@@ -1,25 +1,39 @@
|
||||
package org.koitharu.kotatsu.details.ui.adapter
|
||||
|
||||
import android.content.Context
|
||||
import org.koitharu.kotatsu.core.model.formatNumber
|
||||
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||
import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD
|
||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
class ChaptersAdapter(
|
||||
private val onItemClickListener: OnListItemClickListener<ChapterListItem>,
|
||||
) : BaseListAdapter<ListModel>(), FastScroller.SectionIndexer {
|
||||
|
||||
private var hasVolumes = false
|
||||
|
||||
init {
|
||||
addDelegate(ListItemType.HEADER, listHeaderAD(null))
|
||||
addDelegate(ListItemType.CHAPTER_LIST, chapterListItemAD(onItemClickListener))
|
||||
addDelegate(ListItemType.CHAPTER_GRID, chapterGridItemAD(onItemClickListener))
|
||||
}
|
||||
|
||||
override suspend fun emit(value: List<ListModel>?) {
|
||||
super.emit(value)
|
||||
hasVolumes = value != null && value.any { it is ListHeader }
|
||||
}
|
||||
|
||||
override fun getSectionText(context: Context, position: Int): CharSequence? {
|
||||
return findHeader(position)?.getText(context)
|
||||
return if (hasVolumes) {
|
||||
findHeader(position)?.getText(context)
|
||||
} else {
|
||||
val chapter = (items.getOrNull(position) as? ChapterListItem)?.chapter ?: return null
|
||||
if (chapter.number > 0) chapter.formatNumber() else null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
import android.view.View
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.koitharu.kotatsu.R
|
||||
@@ -19,7 +20,10 @@ import com.google.android.material.R as materialR
|
||||
class ChaptersSelectionDecoration(context: Context) : AbstractSelectionItemDecoration() {
|
||||
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val defaultRadius = context.resources.getDimension(materialR.dimen.abc_control_corner_material)
|
||||
private val radius = context.resources.getDimension(materialR.dimen.abc_control_corner_material)
|
||||
private val checkIcon = ContextCompat.getDrawable(context, materialR.drawable.ic_mtrl_checked_circle)
|
||||
private val iconOffset = context.resources.getDimensionPixelOffset(R.dimen.chapter_check_offset)
|
||||
private val iconSize = context.resources.getDimensionPixelOffset(R.dimen.chapter_check_size)
|
||||
private val strokeColor = context.getThemeColor(materialR.attr.colorPrimary, Color.RED)
|
||||
private val fillColor = ColorUtils.setAlphaComponent(
|
||||
ColorUtils.blendARGB(strokeColor, context.getThemeColor(materialR.attr.colorSurface), 0.8f),
|
||||
@@ -32,11 +36,12 @@ class ChaptersSelectionDecoration(context: Context) : AbstractSelectionItemDecor
|
||||
98,
|
||||
)
|
||||
paint.style = Paint.Style.FILL
|
||||
hasBackground = false
|
||||
hasBackground = true
|
||||
hasForeground = true
|
||||
isIncludeDecorAndMargins = false
|
||||
|
||||
paint.strokeWidth = context.resources.getDimension(R.dimen.selection_stroke_width)
|
||||
checkIcon?.setTint(strokeColor)
|
||||
}
|
||||
|
||||
override fun getItemId(parent: RecyclerView, child: View): Long {
|
||||
@@ -45,6 +50,19 @@ class ChaptersSelectionDecoration(context: Context) : AbstractSelectionItemDecor
|
||||
return item.chapter.id
|
||||
}
|
||||
|
||||
override fun onDrawBackground(
|
||||
canvas: Canvas,
|
||||
parent: RecyclerView,
|
||||
child: View,
|
||||
bounds: RectF,
|
||||
state: RecyclerView.State,
|
||||
) {
|
||||
if (child is CardView) {
|
||||
return
|
||||
}
|
||||
canvas.drawRoundRect(bounds, radius, radius, paint)
|
||||
}
|
||||
|
||||
override fun onDrawForeground(
|
||||
canvas: Canvas,
|
||||
parent: RecyclerView,
|
||||
@@ -52,16 +70,24 @@ class ChaptersSelectionDecoration(context: Context) : AbstractSelectionItemDecor
|
||||
bounds: RectF,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
val radius = if (child is CardView) {
|
||||
child.radius
|
||||
} else {
|
||||
defaultRadius
|
||||
if (child !is CardView) {
|
||||
return
|
||||
}
|
||||
val radius = child.radius
|
||||
paint.color = fillColor
|
||||
paint.style = Paint.Style.FILL
|
||||
canvas.drawRoundRect(bounds, radius, radius, paint)
|
||||
paint.color = strokeColor
|
||||
paint.style = Paint.Style.STROKE
|
||||
canvas.drawRoundRect(bounds, radius, radius, paint)
|
||||
checkIcon?.run {
|
||||
setBounds(
|
||||
(bounds.right - iconSize - iconOffset).toInt(),
|
||||
(bounds.top + iconOffset).toInt(),
|
||||
(bounds.right - iconOffset).toInt(),
|
||||
(bounds.top + iconOffset + iconSize).toInt(),
|
||||
)
|
||||
draw(canvas)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,6 @@ import kotlin.experimental.and
|
||||
data class ChapterListItem(
|
||||
val chapter: MangaChapter,
|
||||
val flags: Byte,
|
||||
private val uploadDateMs: Long,
|
||||
private val groupPosition: Byte,
|
||||
) : ListModel {
|
||||
|
||||
var description: String? = null
|
||||
@@ -26,9 +24,9 @@ data class ChapterListItem(
|
||||
private set
|
||||
get() {
|
||||
if (field != null) return field
|
||||
if (uploadDateMs == 0L) return null
|
||||
if (chapter.uploadDate == 0L) return null
|
||||
field = DateUtils.getRelativeTimeSpanString(
|
||||
uploadDateMs,
|
||||
chapter.uploadDate,
|
||||
System.currentTimeMillis(),
|
||||
DateUtils.DAY_IN_MILLIS,
|
||||
)
|
||||
@@ -53,15 +51,6 @@ data class ChapterListItem(
|
||||
val isGrid: Boolean
|
||||
get() = hasFlag(FLAG_GRID)
|
||||
|
||||
val isGroupStart: Boolean
|
||||
get() = (groupPosition and GROUP_START) == GROUP_START
|
||||
|
||||
val isGroupMiddle: Boolean
|
||||
get() = (groupPosition and GROUP_MIDDLE) == GROUP_MIDDLE
|
||||
|
||||
val isGroupEnd: Boolean
|
||||
get() = (groupPosition and GROUP_END) == GROUP_END
|
||||
|
||||
private fun buildDescription(): String {
|
||||
val joiner = StringJoiner(" • ")
|
||||
chapter.formatNumber()?.let {
|
||||
@@ -105,9 +94,5 @@ data class ChapterListItem(
|
||||
const val FLAG_BOOKMARKED: Byte = 16
|
||||
const val FLAG_DOWNLOADED: Byte = 32
|
||||
const val FLAG_GRID: Byte = 64
|
||||
|
||||
const val GROUP_START: Byte = 2
|
||||
const val GROUP_MIDDLE: Byte = 4
|
||||
const val GROUP_END: Byte = 8
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package org.koitharu.kotatsu.details.ui.model
|
||||
|
||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.model.isLocal
|
||||
import org.koitharu.kotatsu.details.data.MangaDetails
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
|
||||
data class HistoryInfo(
|
||||
val totalChapters: Int,
|
||||
@@ -17,8 +14,8 @@ data class HistoryInfo(
|
||||
val isValid: Boolean
|
||||
get() = totalChapters >= 0
|
||||
|
||||
val canContinue: Boolean
|
||||
get() = history != null && !isChapterMissing
|
||||
val canContinue
|
||||
get() = currentChapter >= 0
|
||||
}
|
||||
|
||||
fun HistoryInfo(
|
||||
@@ -38,7 +35,7 @@ fun HistoryInfo(
|
||||
currentChapter = currentChapter,
|
||||
history = history,
|
||||
isIncognitoMode = isIncognitoMode,
|
||||
isChapterMissing = currentChapter == -1,
|
||||
isChapterMissing = history != null && manga?.isLoaded == true && manga.allChapters.none { it.id == history.chapterId },
|
||||
canDownload = manga?.isLocal == false,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -27,7 +27,5 @@ fun MangaChapter.toListItem(
|
||||
return ChapterListItem(
|
||||
chapter = this,
|
||||
flags = flags,
|
||||
uploadDateMs = uploadDate,
|
||||
groupPosition = 0,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.details.ui.pager.bookmarks.MangaBookmarksFragment
|
||||
import org.koitharu.kotatsu.details.ui.pager.bookmarks.BookmarksFragment
|
||||
import org.koitharu.kotatsu.details.ui.pager.chapters.ChaptersFragment
|
||||
import org.koitharu.kotatsu.details.ui.pager.pages.PagesFragment
|
||||
|
||||
@@ -19,8 +19,8 @@ class ChaptersPagesAdapter(
|
||||
|
||||
override fun createFragment(position: Int): Fragment = when (position) {
|
||||
0 -> ChaptersFragment()
|
||||
1 -> if (isPagesTabEnabled) PagesFragment() else MangaBookmarksFragment()
|
||||
2 -> MangaBookmarksFragment()
|
||||
1 -> if (isPagesTabEnabled) PagesFragment() else BookmarksFragment()
|
||||
2 -> BookmarksFragment()
|
||||
else -> throw IllegalArgumentException("Invalid position $position")
|
||||
}
|
||||
|
||||
|
||||
@@ -8,12 +8,10 @@ import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetBehavior
|
||||
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetBehavior.Companion.STATE_COLLAPSED
|
||||
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetBehavior.Companion.STATE_DRAGGING
|
||||
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetBehavior.Companion.STATE_EXPANDED
|
||||
@@ -21,11 +19,13 @@ import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetBehavior.Companion.STATE_
|
||||
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetCallback
|
||||
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
|
||||
import org.koitharu.kotatsu.core.ui.util.ActionModeListener
|
||||
import org.koitharu.kotatsu.core.ui.util.MenuInvalidator
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
||||
import org.koitharu.kotatsu.core.util.ext.doOnPageChanged
|
||||
import org.koitharu.kotatsu.core.util.ext.menuView
|
||||
import org.koitharu.kotatsu.core.util.ext.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
|
||||
@@ -51,9 +51,13 @@ class ChaptersPagesSheet : BaseAdaptiveSheet<SheetChaptersPagesBinding>(), Actio
|
||||
disableFitToContents()
|
||||
|
||||
val args = arguments ?: Bundle.EMPTY
|
||||
val defaultTab = args.getInt(ARG_TAB, settings.defaultDetailsTab)
|
||||
val adapter = ChaptersPagesAdapter(this, settings.isPagesTabEnabled || defaultTab == TAB_PAGES)
|
||||
var defaultTab = args.getInt(ARG_TAB, settings.defaultDetailsTab)
|
||||
val adapter = ChaptersPagesAdapter(this, settings.isPagesTabEnabled)
|
||||
if (!adapter.isPagesTabEnabled) {
|
||||
defaultTab = (defaultTab - 1).coerceAtLeast(TAB_CHAPTERS)
|
||||
}
|
||||
binding.pager.offscreenPageLimit = adapter.itemCount
|
||||
binding.pager.recyclerView?.isNestedScrollingEnabled = false
|
||||
binding.pager.adapter = adapter
|
||||
binding.pager.doOnPageChanged(::onPageChanged)
|
||||
TabLayoutMediator(binding.tabs, binding.pager, adapter).attach()
|
||||
@@ -64,21 +68,28 @@ class ChaptersPagesSheet : BaseAdaptiveSheet<SheetChaptersPagesBinding>(), Actio
|
||||
onBackPressedDispatcher.addCallback(viewLifecycleOwner, menuProvider)
|
||||
binding.toolbar.addMenuProvider(menuProvider)
|
||||
|
||||
val menuInvalidator = MenuInvalidator(binding.toolbar)
|
||||
viewModel.isChaptersReversed.observe(viewLifecycleOwner, menuInvalidator)
|
||||
viewModel.isChaptersInGridView.observe(viewLifecycleOwner, menuInvalidator)
|
||||
|
||||
actionModeDelegate?.addListener(this, viewLifecycleOwner)
|
||||
addSheetCallback(this, viewLifecycleOwner)
|
||||
|
||||
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.pager, this))
|
||||
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(binding.pager, null))
|
||||
viewModel.onDownloadStarted.observeEvent(viewLifecycleOwner, DownloadStartedObserver(binding.pager))
|
||||
viewModel.newChaptersCount.observe(viewLifecycleOwner, ::onNewChaptersChanged)
|
||||
if (dialog != null) {
|
||||
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.pager, this))
|
||||
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(binding.pager, null))
|
||||
viewModel.onDownloadStarted.observeEvent(viewLifecycleOwner, DownloadStartedObserver(binding.pager))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStateChanged(sheet: View, newState: Int) {
|
||||
if (newState == STATE_DRAGGING || newState == STATE_SETTLING) {
|
||||
return
|
||||
}
|
||||
val binding = viewBinding ?: return
|
||||
val isActionModeStarted = actionModeDelegate?.isActionModeStarted == true
|
||||
viewBinding?.toolbar?.menuView?.isVisible = newState != STATE_COLLAPSED && !isActionModeStarted
|
||||
binding.toolbar.menuView?.isVisible = newState != STATE_COLLAPSED && !isActionModeStarted
|
||||
}
|
||||
|
||||
override fun onActionModeStarted(mode: ActionMode) {
|
||||
@@ -130,9 +141,6 @@ class ChaptersPagesSheet : BaseAdaptiveSheet<SheetChaptersPagesBinding>(), Actio
|
||||
const val TAB_PAGES = 1
|
||||
const val TAB_BOOKMARKS = 2
|
||||
private const val ARG_TAB = "tag"
|
||||
|
||||
@Deprecated("")
|
||||
private const val ARG_SHOW_PAGES = "pages"
|
||||
private const val TAG = "ChaptersPagesSheet"
|
||||
|
||||
fun show(fm: FragmentManager) {
|
||||
@@ -147,7 +155,7 @@ class ChaptersPagesSheet : BaseAdaptiveSheet<SheetChaptersPagesBinding>(), Actio
|
||||
|
||||
fun isShown(fm: FragmentManager): Boolean {
|
||||
val sheet = fm.findFragmentByTag(TAG) as? ChaptersPagesSheet
|
||||
return sheet != null && sheet.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
|
||||
return sheet?.dialog?.isShowing == true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,22 +2,33 @@ package org.koitharu.kotatsu.details.ui.pager.bookmarks
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import coil.ImageLoader
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.bookmarks.ui.sheet.BookmarksAdapter
|
||||
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.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
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.databinding.FragmentMangaBookmarksBinding
|
||||
import org.koitharu.kotatsu.details.ui.DetailsViewModel
|
||||
import org.koitharu.kotatsu.list.ui.GridSpanResolver
|
||||
@@ -28,11 +39,11 @@ import org.koitharu.kotatsu.reader.ui.ReaderNavigationCallback
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MangaBookmarksFragment : BaseFragment<FragmentMangaBookmarksBinding>(),
|
||||
OnListItemClickListener<Bookmark> {
|
||||
class BookmarksFragment : BaseFragment<FragmentMangaBookmarksBinding>(),
|
||||
OnListItemClickListener<Bookmark>, ListSelectionController.Callback2 {
|
||||
|
||||
private val activityViewModel by activityViewModels<DetailsViewModel>()
|
||||
private val viewModel by viewModels<MangaBookmarksViewModel>()
|
||||
private val viewModel by viewModels<BookmarksViewModel>()
|
||||
|
||||
@Inject
|
||||
lateinit var coil: ImageLoader
|
||||
@@ -42,6 +53,7 @@ class MangaBookmarksFragment : BaseFragment<FragmentMangaBookmarksBinding>(),
|
||||
|
||||
private var bookmarksAdapter: BookmarksAdapter? = null
|
||||
private var spanResolver: GridSpanResolver? = null
|
||||
private var selectionController: ListSelectionController? = null
|
||||
|
||||
private val spanSizeLookup = SpanSizeLookup()
|
||||
private val listCommitCallback = Runnable {
|
||||
@@ -60,46 +72,54 @@ class MangaBookmarksFragment : BaseFragment<FragmentMangaBookmarksBinding>(),
|
||||
override fun onViewBindingCreated(binding: FragmentMangaBookmarksBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
spanResolver = GridSpanResolver(binding.root.resources)
|
||||
selectionController = ListSelectionController(
|
||||
appCompatDelegate = checkNotNull(findAppCompatDelegate()),
|
||||
decoration = BookmarksSelectionDecoration(binding.root.context),
|
||||
registryOwner = this,
|
||||
callback = this,
|
||||
)
|
||||
bookmarksAdapter = BookmarksAdapter(
|
||||
coil = coil,
|
||||
lifecycleOwner = viewLifecycleOwner,
|
||||
clickListener = this@MangaBookmarksFragment,
|
||||
clickListener = this@BookmarksFragment,
|
||||
headerClickListener = null,
|
||||
)
|
||||
viewModel.gridScale.observe(viewLifecycleOwner, ::onGridScaleChanged) // before rv initialization
|
||||
with(binding.recyclerView) {
|
||||
addItemDecoration(TypedListSpacingDecoration(context, false))
|
||||
setHasFixedSize(true)
|
||||
PagerNestedScrollHelper(this).bind(viewLifecycleOwner)
|
||||
adapter = bookmarksAdapter
|
||||
addOnLayoutChangeListener(spanResolver)
|
||||
(layoutManager as GridLayoutManager).let {
|
||||
it.spanSizeLookup = spanSizeLookup
|
||||
it.spanCount = checkNotNull(spanResolver).spanCount
|
||||
}
|
||||
selectionController?.attachToRecyclerView(this)
|
||||
}
|
||||
viewModel.content.observe(viewLifecycleOwner, checkNotNull(bookmarksAdapter))
|
||||
viewModel.content.observe(viewLifecycleOwner) { bookmarksAdapter?.setItems(it, listCommitCallback) }
|
||||
|
||||
viewModel.onError.observeEvent(
|
||||
viewLifecycleOwner,
|
||||
SnackbarErrorObserver(binding.recyclerView, this),
|
||||
)
|
||||
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView))
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
spanResolver = null
|
||||
bookmarksAdapter = null
|
||||
selectionController = null
|
||||
spanSizeLookup.invalidateCache()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
// required for BottomSheetBehavior
|
||||
requireViewBinding().recyclerView.isNestedScrollingEnabled = false
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
requireViewBinding().recyclerView.isNestedScrollingEnabled = true
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) = Unit
|
||||
|
||||
override fun onItemClick(item: Bookmark, view: View) {
|
||||
if (selectionController?.onItemClick(item.pageId) == true) {
|
||||
return
|
||||
}
|
||||
val listener = findParentCallback(ReaderNavigationCallback::class.java)
|
||||
if (listener != null && listener.onBookmarkSelected(item)) {
|
||||
dismissParentDialog()
|
||||
@@ -113,6 +133,40 @@ class MangaBookmarksFragment : BaseFragment<FragmentMangaBookmarksBinding>(),
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemLongClick(item: Bookmark, view: View): Boolean {
|
||||
return selectionController?.onItemLongClick(item.pageId) ?: false
|
||||
}
|
||||
|
||||
override fun onSelectionChanged(controller: ListSelectionController, count: Int) {
|
||||
requireViewBinding().recyclerView.invalidateItemDecorations()
|
||||
}
|
||||
|
||||
override fun onCreateActionMode(
|
||||
controller: ListSelectionController,
|
||||
mode: ActionMode,
|
||||
menu: Menu,
|
||||
): Boolean {
|
||||
mode.menuInflater.inflate(R.menu.mode_bookmarks, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onActionItemClicked(
|
||||
controller: ListSelectionController,
|
||||
mode: ActionMode,
|
||||
item: MenuItem,
|
||||
): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.action_remove -> {
|
||||
val ids = selectionController?.snapshot() ?: return false
|
||||
viewModel.removeBookmarks(ids)
|
||||
mode.finish()
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun onGridScaleChanged(scale: Float) {
|
||||
spanSizeLookup.invalidateCache()
|
||||
spanResolver?.setGridSize(scale, requireViewBinding().recyclerView)
|
||||
@@ -18,6 +18,9 @@ import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
@@ -26,12 +29,13 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class MangaBookmarksViewModel @Inject constructor(
|
||||
bookmarksRepository: BookmarksRepository,
|
||||
class BookmarksViewModel @Inject constructor(
|
||||
private val bookmarksRepository: BookmarksRepository,
|
||||
settings: AppSettings,
|
||||
) : BaseViewModel(), FlowCollector<Manga?> {
|
||||
|
||||
private val manga = MutableStateFlow<Manga?>(null)
|
||||
val onActionDone = MutableEventFlow<ReversibleAction>()
|
||||
|
||||
val gridScale = settings.observeAsStateFlow(
|
||||
scope = viewModelScope + Dispatchers.Default,
|
||||
@@ -50,7 +54,14 @@ class MangaBookmarksViewModel @Inject constructor(
|
||||
manga.value = value
|
||||
}
|
||||
|
||||
private suspend fun mapList(manga: Manga, bookmarks: List<Bookmark>): List<ListModel>? {
|
||||
fun removeBookmarks(ids: Set<Long>) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
val handle = bookmarksRepository.removeBookmarks(ids)
|
||||
onActionDone.call(ReversibleAction(R.string.bookmarks_removed, handle))
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapList(manga: Manga, bookmarks: List<Bookmark>): List<ListModel>? {
|
||||
val chapters = manga.chapters ?: return null
|
||||
val bookmarksMap = bookmarks.groupBy { it.chapterId }
|
||||
val result = ArrayList<ListModel>(bookmarks.size + bookmarksMap.size)
|
||||
@@ -8,19 +8,23 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.ancestors
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.R
|
||||
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.util.RecyclerViewScrollCallback
|
||||
import org.koitharu.kotatsu.core.util.ext.dismissParentDialog
|
||||
import org.koitharu.kotatsu.core.util.ext.findAppCompatDelegate
|
||||
@@ -79,7 +83,7 @@ class ChaptersFragment :
|
||||
addItemDecoration(TypedListSpacingDecoration(context, true))
|
||||
checkNotNull(selectionController).attachToRecyclerView(this)
|
||||
setHasFixedSize(true)
|
||||
isNestedScrollingEnabled = false
|
||||
PagerNestedScrollHelper(this).bind(viewLifecycleOwner)
|
||||
adapter = chaptersAdapter
|
||||
ChapterGridSpanHelper.attach(this)
|
||||
}
|
||||
@@ -91,12 +95,7 @@ class ChaptersFragment :
|
||||
viewModel.isChaptersEmpty.observe(viewLifecycleOwner) {
|
||||
binding.textViewHolder.isVisible = it
|
||||
}
|
||||
viewModel.onSelectChapter.observeEvent(viewLifecycleOwner) { chapterId ->
|
||||
chaptersAdapter?.observeItems()?.firstOrNull { items ->
|
||||
items.any { x -> x is ChapterListItem && x.chapter.id == chapterId }
|
||||
}
|
||||
selectionController?.onItemLongClick(chapterId)
|
||||
}
|
||||
viewModel.onSelectChapter.observeEvent(viewLifecycleOwner, ::onSelectChapter)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
@@ -105,17 +104,6 @@ class ChaptersFragment :
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
// required for BottomSheetBehavior
|
||||
requireViewBinding().recyclerViewChapters.isNestedScrollingEnabled = false
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
requireViewBinding().recyclerViewChapters.isNestedScrollingEnabled = true
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
override fun onItemClick(item: ChapterListItem, view: View) {
|
||||
if (selectionController?.onItemClick(item.chapter.id) == true) {
|
||||
return
|
||||
@@ -273,6 +261,25 @@ 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?.onItemLongClick(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 onLoadingStateChanged(isLoading: Boolean) {
|
||||
requireViewBinding().progressBar.isVisible = isLoading
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import coil.decode.ImageSource
|
||||
import coil.fetch.FetchResult
|
||||
import coil.fetch.Fetcher
|
||||
import coil.fetch.SourceResult
|
||||
import coil.network.HttpException
|
||||
import coil.request.Options
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -44,15 +45,17 @@ class MangaPageFetcher(
|
||||
override suspend fun fetch(): FetchResult {
|
||||
val repo = mangaRepositoryFactory.create(page.source)
|
||||
val pageUrl = repo.getPageUrl(page)
|
||||
pagesCache.get(pageUrl)?.let { file ->
|
||||
return SourceResult(
|
||||
source = ImageSource(
|
||||
file = file.toOkioPath(),
|
||||
metadata = MangaPageMetadata(page),
|
||||
),
|
||||
mimeType = null,
|
||||
dataSource = DataSource.DISK,
|
||||
)
|
||||
if (options.diskCachePolicy.readEnabled) {
|
||||
pagesCache.get(pageUrl)?.let { file ->
|
||||
return SourceResult(
|
||||
source = ImageSource(
|
||||
file = file.toOkioPath(),
|
||||
metadata = MangaPageMetadata(page),
|
||||
),
|
||||
mimeType = null,
|
||||
dataSource = DataSource.DISK,
|
||||
)
|
||||
}
|
||||
}
|
||||
return loadPage(pageUrl)
|
||||
}
|
||||
@@ -91,8 +94,8 @@ class MangaPageFetcher(
|
||||
else -> {
|
||||
val request = PageLoader.createPageRequest(page, pageUrl)
|
||||
imageProxyInterceptor.interceptPageRequest(request, okHttpClient).use { response ->
|
||||
check(response.isSuccessful) {
|
||||
"Invalid response: ${response.code} ${response.message} at $pageUrl"
|
||||
if (!response.isSuccessful) {
|
||||
throw HttpException(response)
|
||||
}
|
||||
val body = checkNotNull(response.body) {
|
||||
"Null response"
|
||||
@@ -122,17 +125,15 @@ class MangaPageFetcher(
|
||||
private val imageProxyInterceptor: ImageProxyInterceptor,
|
||||
) : Fetcher.Factory<MangaPage> {
|
||||
|
||||
override fun create(data: MangaPage, options: Options, imageLoader: ImageLoader): Fetcher {
|
||||
return MangaPageFetcher(
|
||||
okHttpClient = okHttpClient,
|
||||
pagesCache = pagesCache,
|
||||
options = options,
|
||||
page = data,
|
||||
context = context,
|
||||
mangaRepositoryFactory = mangaRepositoryFactory,
|
||||
imageProxyInterceptor = imageProxyInterceptor,
|
||||
)
|
||||
}
|
||||
override fun create(data: MangaPage, options: Options, imageLoader: ImageLoader) = MangaPageFetcher(
|
||||
okHttpClient = okHttpClient,
|
||||
pagesCache = pagesCache,
|
||||
options = options,
|
||||
page = data,
|
||||
context = context,
|
||||
mangaRepositoryFactory = mangaRepositoryFactory,
|
||||
imageProxyInterceptor = imageProxyInterceptor,
|
||||
)
|
||||
}
|
||||
|
||||
class MangaPageMetadata(val page: MangaPage) : ImageSource.Metadata()
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.koitharu.kotatsu.details.ui.pager.pages
|
||||
|
||||
import coil.key.Keyer
|
||||
import coil.request.Options
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
|
||||
class MangaPageKeyer : Keyer<MangaPage> {
|
||||
|
||||
override fun key(data: MangaPage, options: Options) = data.url
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.core.ui.list.BoundsScrollListener
|
||||
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.findParentCallback
|
||||
@@ -93,7 +94,7 @@ class PagesFragment :
|
||||
addItemDecoration(TypedListSpacingDecoration(context, false))
|
||||
adapter = thumbnailsAdapter
|
||||
setHasFixedSize(true)
|
||||
isNestedScrollingEnabled = false
|
||||
PagerNestedScrollHelper(this).bind(viewLifecycleOwner)
|
||||
addOnLayoutChangeListener(spanResolver)
|
||||
addOnScrollListener(ScrollListener().also { scrollListener = it })
|
||||
(layoutManager as GridLayoutManager).let {
|
||||
@@ -117,17 +118,6 @@ class PagesFragment :
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
// required for BottomSheetBehavior
|
||||
requireViewBinding().recyclerView.isNestedScrollingEnabled = false
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
requireViewBinding().recyclerView.isNestedScrollingEnabled = true
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) = Unit
|
||||
|
||||
override fun onItemClick(item: PageThumbnail, view: View) {
|
||||
|
||||
@@ -5,9 +5,7 @@ import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.work.WorkInfo
|
||||
import coil.ImageLoader
|
||||
import coil.request.SuccessResult
|
||||
@@ -62,7 +60,6 @@ fun downloadItemAD(
|
||||
val chaptersAdapter = BaseListAdapter<DownloadChapter>()
|
||||
.addDelegate(ListItemType.CHAPTER_LIST, downloadChapterAD())
|
||||
|
||||
binding.recyclerViewChapters.addItemDecoration(DividerItemDecoration(context, RecyclerView.VERTICAL))
|
||||
binding.recyclerViewChapters.adapter = chaptersAdapter
|
||||
binding.buttonCancel.setOnClickListener(clickListener)
|
||||
binding.buttonPause.setOnClickListener(clickListener)
|
||||
|
||||
@@ -322,7 +322,7 @@ class DownloadsViewModel @Inject constructor(
|
||||
emit(mapChapters())
|
||||
}
|
||||
}
|
||||
}.stateIn(viewModelScope, SharingStarted.Eagerly, null)
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
|
||||
|
||||
private suspend fun tryLoad(manga: Manga) = runCatchingCancellable {
|
||||
(mangaRepositoryFactory.create(manga.source) as RemoteMangaRepository).getDetails(manga)
|
||||
|
||||
@@ -17,7 +17,6 @@ import org.koitharu.kotatsu.core.model.isNsfw
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
import java.util.Collections
|
||||
@@ -48,8 +47,17 @@ class MangaSourcesRepository @Inject constructor(
|
||||
return dao.findAllEnabled(order).toSources(settings.isNsfwContentDisabled, order)
|
||||
}
|
||||
|
||||
suspend fun getDisabledSources(): List<MangaSource> {
|
||||
return dao.findAllDisabled().toSources(settings.isNsfwContentDisabled, null)
|
||||
suspend fun getDisabledSources(): Set<MangaSource> {
|
||||
val result = EnumSet.copyOf(remoteSources)
|
||||
val enabled = dao.findAllEnabledNames()
|
||||
for (name in enabled) {
|
||||
val source = MangaSource(name)
|
||||
result.remove(source)
|
||||
}
|
||||
if (settings.isNsfwContentDisabled) {
|
||||
result.removeAll { it.isNsfw() }
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun observeIsEnabled(source: MangaSource): Flow<Boolean> {
|
||||
@@ -97,10 +105,10 @@ class MangaSourcesRepository @Inject constructor(
|
||||
result
|
||||
}
|
||||
|
||||
suspend fun setSourceEnabled(source: MangaSource, isEnabled: Boolean): ReversibleHandle {
|
||||
dao.setEnabled(source.name, isEnabled)
|
||||
suspend fun setSourcesEnabled(sources: Collection<MangaSource>, isEnabled: Boolean): ReversibleHandle {
|
||||
setSourcesEnabledImpl(sources, isEnabled)
|
||||
return ReversibleHandle {
|
||||
dao.setEnabled(source.name, !isEnabled)
|
||||
setSourcesEnabledImpl(sources, !isEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,6 +151,7 @@ class MangaSourcesRepository @Inject constructor(
|
||||
result
|
||||
}.distinctUntilChanged()
|
||||
} else {
|
||||
assimilateNewSources()
|
||||
flowOf(emptySet())
|
||||
}
|
||||
}
|
||||
@@ -171,6 +180,18 @@ class MangaSourcesRepository @Inject constructor(
|
||||
return dao.findAll().isEmpty()
|
||||
}
|
||||
|
||||
private suspend fun setSourcesEnabledImpl(sources: Collection<MangaSource>, isEnabled: Boolean) {
|
||||
if (sources.size == 1) { // fast path
|
||||
dao.setEnabled(sources.first().name, isEnabled)
|
||||
return
|
||||
}
|
||||
db.withTransaction {
|
||||
for (source in sources) {
|
||||
dao.setEnabled(source.name, isEnabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getNewSources(): MutableSet<MangaSource> {
|
||||
val entities = dao.findAll()
|
||||
val result = EnumSet.copyOf(remoteSources)
|
||||
@@ -187,7 +208,7 @@ class MangaSourcesRepository @Inject constructor(
|
||||
val result = ArrayList<MangaSource>(size)
|
||||
for (entity in this) {
|
||||
val source = MangaSource(entity.source)
|
||||
if (skipNsfwSources && source.contentType == ContentType.HENTAI) {
|
||||
if (skipNsfwSources && source.isNsfw()) {
|
||||
continue
|
||||
}
|
||||
if (source in remoteSources) {
|
||||
|
||||
@@ -4,11 +4,11 @@ import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.viewModels
|
||||
@@ -17,22 +17,21 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.ImageLoader
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.bookmarks.ui.BookmarksActivity
|
||||
import org.koitharu.kotatsu.bookmarks.ui.AllBookmarksActivity
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
||||
import org.koitharu.kotatsu.core.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.core.ui.dialog.TwoButtonsAlertDialog
|
||||
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
||||
import org.koitharu.kotatsu.core.ui.util.SpanSizeResolver
|
||||
import org.koitharu.kotatsu.core.ui.widgets.TipView
|
||||
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
|
||||
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.core.util.ext.viewLifecycleScope
|
||||
import org.koitharu.kotatsu.databinding.FragmentExploreBinding
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.download.ui.list.DownloadsActivity
|
||||
@@ -44,6 +43,7 @@ import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||
import org.koitharu.kotatsu.list.ui.model.TipModel
|
||||
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.search.ui.MangaListActivity
|
||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
import org.koitharu.kotatsu.settings.newsources.NewSourcesDialogFragment
|
||||
@@ -56,16 +56,14 @@ class ExploreFragment :
|
||||
BaseFragment<FragmentExploreBinding>(),
|
||||
RecyclerViewOwner,
|
||||
ExploreListEventListener,
|
||||
OnListItemClickListener<MangaSourceItem>, TipView.OnButtonClickListener {
|
||||
OnListItemClickListener<MangaSourceItem>, TipView.OnButtonClickListener, ListSelectionController.Callback2 {
|
||||
|
||||
@Inject
|
||||
lateinit var coil: ImageLoader
|
||||
|
||||
@Inject
|
||||
lateinit var shortcutManager: AppShortcutManager
|
||||
|
||||
private val viewModel by viewModels<ExploreViewModel>()
|
||||
private var exploreAdapter: ExploreAdapter? = null
|
||||
private var sourceSelectionController: ListSelectionController? = null
|
||||
|
||||
override val recyclerView: RecyclerView
|
||||
get() = requireViewBinding().recyclerView
|
||||
@@ -79,11 +77,18 @@ class ExploreFragment :
|
||||
exploreAdapter = ExploreAdapter(coil, viewLifecycleOwner, this, this, this) { manga, view ->
|
||||
startActivity(DetailsActivity.newIntent(view.context, manga))
|
||||
}
|
||||
sourceSelectionController = ListSelectionController(
|
||||
appCompatDelegate = checkNotNull(findAppCompatDelegate()),
|
||||
decoration = SourceSelectionDecoration(binding.root.context),
|
||||
registryOwner = this,
|
||||
callback = this,
|
||||
)
|
||||
with(binding.recyclerView) {
|
||||
adapter = exploreAdapter
|
||||
setHasFixedSize(true)
|
||||
SpanSizeResolver(this, resources.getDimensionPixelSize(R.dimen.explore_grid_width)).attach()
|
||||
addItemDecoration(TypedListSpacingDecoration(context, false))
|
||||
checkNotNull(sourceSelectionController).attachToRecyclerView(this)
|
||||
}
|
||||
addMenuProvider(ExploreMenuProvider(binding.root.context))
|
||||
viewModel.content.observe(viewLifecycleOwner) {
|
||||
@@ -100,6 +105,7 @@ class ExploreFragment :
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
sourceSelectionController = null
|
||||
exploreAdapter = null
|
||||
}
|
||||
|
||||
@@ -133,7 +139,7 @@ class ExploreFragment :
|
||||
override fun onClick(v: View) {
|
||||
val intent = when (v.id) {
|
||||
R.id.button_local -> MangaListActivity.newIntent(v.context, MangaSource.LOCAL)
|
||||
R.id.button_bookmarks -> BookmarksActivity.newIntent(v.context)
|
||||
R.id.button_bookmarks -> AllBookmarksActivity.newIntent(v.context)
|
||||
R.id.button_more -> SuggestionsActivity.newIntent(v.context)
|
||||
R.id.button_downloads -> DownloadsActivity.newIntent(v.context)
|
||||
R.id.button_random -> {
|
||||
@@ -147,18 +153,15 @@ class ExploreFragment :
|
||||
}
|
||||
|
||||
override fun onItemClick(item: MangaSourceItem, view: View) {
|
||||
if (sourceSelectionController?.onItemClick(item.id) == true) {
|
||||
return
|
||||
}
|
||||
val intent = MangaListActivity.newIntent(view.context, item.source)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
override fun onItemLongClick(item: MangaSourceItem, view: View): Boolean {
|
||||
val menu = PopupMenu(view.context, view)
|
||||
menu.inflate(R.menu.popup_source)
|
||||
menu.menu.findItem(R.id.action_shortcut)
|
||||
?.isVisible = ShortcutManagerCompat.isRequestPinShortcutSupported(view.context)
|
||||
menu.setOnMenuItemClickListener(SourceMenuListener(item))
|
||||
menu.show()
|
||||
return true
|
||||
return sourceSelectionController?.onItemLongClick(item.id) ?: false
|
||||
}
|
||||
|
||||
override fun onRetryClick(error: Throwable) = Unit
|
||||
@@ -167,6 +170,52 @@ class ExploreFragment :
|
||||
startActivity(Intent(context ?: return, SourcesCatalogActivity::class.java))
|
||||
}
|
||||
|
||||
override fun onSelectionChanged(controller: ListSelectionController, count: Int) {
|
||||
viewBinding?.recyclerView?.invalidateItemDecorations()
|
||||
}
|
||||
|
||||
override fun onCreateActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
|
||||
mode.menuInflater.inflate(R.menu.mode_source, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
|
||||
val isSingleSelection = controller.count == 1
|
||||
menu.findItem(R.id.action_settings).isVisible = isSingleSelection
|
||||
menu.findItem(R.id.action_shortcut).isVisible = isSingleSelection
|
||||
return super.onPrepareActionMode(controller, mode, menu)
|
||||
}
|
||||
|
||||
override fun onActionItemClicked(controller: ListSelectionController, mode: ActionMode, item: MenuItem): Boolean {
|
||||
val selectedSources = controller.peekCheckedIds().mapNotNullToSet { id ->
|
||||
MangaSource.entries.getOrNull(id.toInt())
|
||||
}
|
||||
if (selectedSources.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
when (item.itemId) {
|
||||
R.id.action_settings -> {
|
||||
val source = selectedSources.singleOrNull() ?: return false
|
||||
startActivity(SettingsActivity.newSourceSettingsIntent(requireContext(), source))
|
||||
mode.finish()
|
||||
}
|
||||
|
||||
R.id.action_disable -> {
|
||||
viewModel.disableSources(selectedSources)
|
||||
mode.finish()
|
||||
}
|
||||
|
||||
R.id.action_shortcut -> {
|
||||
val source = selectedSources.singleOrNull() ?: return false
|
||||
viewModel.requestPinShortcut(source)
|
||||
mode.finish()
|
||||
}
|
||||
|
||||
else -> return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun onOpenManga(manga: Manga) {
|
||||
val intent = DetailsActivity.newIntent(context ?: return, manga)
|
||||
startActivity(intent)
|
||||
@@ -194,30 +243,4 @@ class ExploreFragment :
|
||||
.create()
|
||||
.show()
|
||||
}
|
||||
|
||||
private inner class SourceMenuListener(
|
||||
private val sourceItem: MangaSourceItem,
|
||||
) : PopupMenu.OnMenuItemClickListener {
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_settings -> {
|
||||
startActivity(SettingsActivity.newSourceSettingsIntent(requireContext(), sourceItem.source))
|
||||
}
|
||||
|
||||
R.id.action_hide -> {
|
||||
viewModel.hideSource(sourceItem.source)
|
||||
}
|
||||
|
||||
R.id.action_shortcut -> {
|
||||
viewLifecycleScope.launch {
|
||||
shortcutManager.requestPinShortcut(sourceItem.source)
|
||||
}
|
||||
}
|
||||
|
||||
else -> return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
|
||||
@@ -43,6 +44,7 @@ class ExploreViewModel @Inject constructor(
|
||||
private val suggestionRepository: SuggestionRepository,
|
||||
private val exploreRepository: ExploreRepository,
|
||||
private val sourcesRepository: MangaSourcesRepository,
|
||||
private val shortcutManager: AppShortcutManager,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val isGrid = settings.observeAsStateFlow(
|
||||
@@ -92,10 +94,11 @@ class ExploreViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun hideSource(source: MangaSource) {
|
||||
fun disableSources(sources: Collection<MangaSource>) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
val rollback = sourcesRepository.setSourceEnabled(source, isEnabled = false)
|
||||
onActionDone.call(ReversibleAction(R.string.source_disabled, rollback))
|
||||
val rollback = sourcesRepository.setSourcesEnabled(sources, isEnabled = false)
|
||||
val message = if (sources.size == 1) R.string.source_disabled else R.string.sources_disabled
|
||||
onActionDone.call(ReversibleAction(message, rollback))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +108,12 @@ class ExploreViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun requestPinShortcut(source: MangaSource) {
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
shortcutManager.requestPinShortcut(source)
|
||||
}
|
||||
}
|
||||
|
||||
fun respondSuggestionTip(isAccepted: Boolean) {
|
||||
settings.isSuggestionsEnabled = isAccepted
|
||||
settings.closeTip(TIP_SUGGESTIONS)
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package org.koitharu.kotatsu.explore.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
import android.view.View
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.NO_ID
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.list.decor.AbstractSelectionItemDecoration
|
||||
import org.koitharu.kotatsu.core.util.ext.getItem
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||
import org.koitharu.kotatsu.explore.ui.model.MangaSourceItem
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
class SourceSelectionDecoration(context: Context) : AbstractSelectionItemDecoration() {
|
||||
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val strokeColor = context.getThemeColor(materialR.attr.colorPrimary, Color.RED)
|
||||
private val fillColor = ColorUtils.setAlphaComponent(
|
||||
ColorUtils.blendARGB(strokeColor, context.getThemeColor(materialR.attr.colorSurface), 0.8f),
|
||||
0x74,
|
||||
)
|
||||
private val defaultRadius = context.resources.getDimension(R.dimen.list_selector_corner)
|
||||
|
||||
init {
|
||||
hasBackground = false
|
||||
hasForeground = true
|
||||
isIncludeDecorAndMargins = false
|
||||
paint.strokeWidth = context.resources.getDimension(R.dimen.selection_stroke_width)
|
||||
}
|
||||
|
||||
override fun getItemId(parent: RecyclerView, child: View): Long {
|
||||
val holder = parent.getChildViewHolder(child) ?: return NO_ID
|
||||
val item = holder.getItem(MangaSourceItem::class.java) ?: return NO_ID
|
||||
return item.id
|
||||
}
|
||||
|
||||
override fun onDrawForeground(
|
||||
canvas: Canvas,
|
||||
parent: RecyclerView,
|
||||
child: View,
|
||||
bounds: RectF,
|
||||
state: RecyclerView.State,
|
||||
) {
|
||||
paint.color = fillColor
|
||||
paint.style = Paint.Style.FILL
|
||||
canvas.drawRoundRect(bounds, defaultRadius, defaultRadius, paint)
|
||||
paint.color = strokeColor
|
||||
paint.style = Paint.Style.STROKE
|
||||
canvas.drawRoundRect(bounds, defaultRadius, defaultRadius, paint)
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,6 @@ fun recommendationMangaItemAD(
|
||||
binding.root.setOnClickListener { v ->
|
||||
itemClickListener.onItemClick(item.manga, v)
|
||||
}
|
||||
|
||||
bind {
|
||||
binding.textViewTitle.text = item.manga.title
|
||||
binding.textViewSubtitle.textAndVisible = item.subtitle
|
||||
|
||||
@@ -8,6 +8,9 @@ data class MangaSourceItem(
|
||||
val isGrid: Boolean,
|
||||
) : ListModel {
|
||||
|
||||
val id: Long
|
||||
get() = source.ordinal.toLong()
|
||||
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
return other is MangaSourceItem && other.source == source
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ abstract class FavouriteCategoriesDao {
|
||||
abstract fun observeAll(): Flow<List<FavouriteCategoryEntity>>
|
||||
|
||||
@Query("SELECT * FROM favourite_categories WHERE deleted_at = 0 AND show_in_lib = 1 ORDER BY sort_key")
|
||||
abstract fun observeAllForLibrary(): Flow<List<FavouriteCategoryEntity>>
|
||||
abstract fun observeAllVisible(): Flow<List<FavouriteCategoryEntity>>
|
||||
|
||||
@Query("SELECT * FROM favourite_categories WHERE category_id = :id AND deleted_at = 0")
|
||||
abstract fun observe(id: Long): Flow<FavouriteCategoryEntity?>
|
||||
@@ -40,7 +40,7 @@ abstract class FavouriteCategoriesDao {
|
||||
abstract suspend fun updateTracking(id: Long, isEnabled: Boolean)
|
||||
|
||||
@Query("UPDATE favourite_categories SET `show_in_lib` = :isEnabled WHERE category_id = :id")
|
||||
abstract suspend fun updateLibVisibility(id: Long, isEnabled: Boolean)
|
||||
abstract suspend fun updateVisibility(id: Long, isEnabled: Boolean)
|
||||
|
||||
@Query("UPDATE favourite_categories SET sort_key = :sortKey WHERE category_id = :id")
|
||||
abstract suspend fun updateSortKey(id: Long, sortKey: Int)
|
||||
|
||||
@@ -11,7 +11,6 @@ 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.core.db.entity.MangaEntity
|
||||
import org.koitharu.kotatsu.favourites.domain.model.Cover
|
||||
import org.koitharu.kotatsu.list.domain.ListSortOrder
|
||||
|
||||
@@ -39,13 +38,6 @@ abstract class FavouritesDao {
|
||||
return observeAllImpl(query)
|
||||
}
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"SELECT * FROM favourites WHERE deleted_at = 0 " +
|
||||
"GROUP BY manga_id ORDER BY created_at DESC LIMIT :limit OFFSET :offset",
|
||||
)
|
||||
abstract suspend fun findAll(offset: Int, limit: Int): List<FavouriteManga>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM favourites WHERE deleted_at = 0 ORDER BY created_at DESC LIMIT :limit OFFSET :offset")
|
||||
abstract suspend fun findAllRaw(offset: Int, limit: Int): List<FavouriteManga>
|
||||
@@ -72,19 +64,6 @@ abstract class FavouritesDao {
|
||||
return observeAllImpl(query)
|
||||
}
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"SELECT * FROM favourites WHERE category_id = :categoryId AND deleted_at = 0 " +
|
||||
"GROUP BY manga_id ORDER BY created_at DESC LIMIT :limit OFFSET :offset",
|
||||
)
|
||||
abstract suspend fun findAll(categoryId: Long, offset: Int, limit: Int): List<FavouriteManga>
|
||||
|
||||
@Query(
|
||||
"SELECT * FROM manga WHERE manga_id IN " +
|
||||
"(SELECT manga_id FROM favourites WHERE category_id = :categoryId AND deleted_at = 0)",
|
||||
)
|
||||
abstract suspend fun findAllManga(categoryId: Int): List<MangaEntity>
|
||||
|
||||
suspend fun findCovers(categoryId: Long, order: ListSortOrder): List<Cover> {
|
||||
val orderBy = getOrderBy(order)
|
||||
|
||||
@@ -114,21 +93,9 @@ abstract class FavouritesDao {
|
||||
@Query("SELECT COUNT(DISTINCT manga_id) FROM favourites WHERE deleted_at = 0")
|
||||
abstract fun observeMangaCount(): Flow<Int>
|
||||
|
||||
@Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM favourites WHERE deleted_at = 0)")
|
||||
abstract suspend fun findAllManga(): List<MangaEntity>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM favourites WHERE manga_id = :id AND deleted_at = 0 GROUP BY manga_id")
|
||||
abstract suspend fun find(id: Long): FavouriteManga?
|
||||
|
||||
@Query("SELECT * FROM favourites WHERE manga_id = :mangaId AND deleted_at = 0")
|
||||
abstract suspend fun findAllRaw(mangaId: Long): List<FavouriteEntity>
|
||||
|
||||
@Transaction
|
||||
@Deprecated("Ignores order")
|
||||
@Query("SELECT * FROM favourites WHERE manga_id = :id AND deleted_at = 0 GROUP BY manga_id")
|
||||
abstract fun observe(id: Long): Flow<FavouriteManga?>
|
||||
|
||||
@Query("SELECT DISTINCT category_id FROM favourites WHERE manga_id = :id AND deleted_at = 0")
|
||||
abstract fun observeIds(id: Long): Flow<List<Long>>
|
||||
|
||||
@@ -138,9 +105,6 @@ abstract class FavouritesDao {
|
||||
@Query("SELECT DISTINCT category_id FROM favourites WHERE manga_id IN (:mangaIds) AND deleted_at = 0 ORDER BY favourites.created_at ASC")
|
||||
abstract suspend fun findCategoriesIds(mangaIds: Collection<Long>): List<Long>
|
||||
|
||||
@Query("SELECT DISTINCT favourite_categories.category_id FROM favourites LEFT JOIN favourite_categories ON favourites.category_id = favourite_categories.category_id WHERE manga_id = :mangaId AND favourites.deleted_at = 0 AND favourite_categories.deleted_at = 0 AND favourite_categories.track = 1")
|
||||
abstract suspend fun findCategoriesIdsWithTrack(mangaId: Long): List<Long>
|
||||
|
||||
/** INSERT **/
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
@@ -194,7 +158,7 @@ abstract class FavouritesDao {
|
||||
protected abstract suspend fun setDeletedAt(mangaId: Long, deletedAt: Long)
|
||||
|
||||
@Query("UPDATE favourites SET deleted_at = :deletedAt WHERE manga_id = :mangaId AND category_id = :categoryId")
|
||||
abstract suspend fun setDeletedAt(categoryId: Long, mangaId: Long, deletedAt: Long)
|
||||
protected abstract suspend fun setDeletedAt(categoryId: Long, mangaId: Long, deletedAt: Long)
|
||||
|
||||
@Query("UPDATE favourites SET deleted_at = :deletedAt WHERE category_id = :categoryId AND deleted_at = 0")
|
||||
protected abstract suspend fun setDeletedAtAll(categoryId: Long, deletedAt: Long)
|
||||
|
||||
@@ -76,7 +76,7 @@ class FavouritesRepository @Inject constructor(
|
||||
}
|
||||
|
||||
fun observeCategoriesForLibrary(): Flow<List<FavouriteCategory>> {
|
||||
return db.getFavouriteCategoriesDao().observeAllForLibrary().mapItems {
|
||||
return db.getFavouriteCategoriesDao().observeAllVisible().mapItems {
|
||||
it.toFavouriteCategory()
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
@@ -157,7 +157,7 @@ class FavouritesRepository @Inject constructor(
|
||||
}
|
||||
|
||||
suspend fun updateCategory(id: Long, isVisibleInLibrary: Boolean) {
|
||||
db.getFavouriteCategoriesDao().updateLibVisibility(id, isVisibleInLibrary)
|
||||
db.getFavouriteCategoriesDao().updateVisibility(id, isVisibleInLibrary)
|
||||
}
|
||||
|
||||
suspend fun updateCategoryTracking(id: Long, isTrackingEnabled: Boolean) {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package org.koitharu.kotatsu.favourites.ui.categories
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -177,10 +175,4 @@ class FavouriteCategoriesActivity :
|
||||
viewModel.saveOrder(adapter.items ?: return)
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("")
|
||||
companion object {
|
||||
|
||||
fun newIntent(context: Context) = Intent(context, FavouriteCategoriesActivity::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.koitharu.kotatsu.favourites.ui.categories.select.adapter
|
||||
|
||||
import android.content.Intent
|
||||
import android.view.View
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
@@ -16,7 +17,7 @@ fun categoriesHeaderAD() = adapterDelegateViewBinding<CategoriesHeaderItem, List
|
||||
val onClickListener = View.OnClickListener { v ->
|
||||
val intent = when (v.id) {
|
||||
R.id.chip_create -> FavouritesCategoryEditActivity.newIntent(v.context)
|
||||
R.id.chip_manage -> FavouriteCategoriesActivity.newIntent(v.context)
|
||||
R.id.chip_manage -> Intent(v.context, FavouriteCategoriesActivity::class.java)
|
||||
else -> return@OnClickListener
|
||||
}
|
||||
v.context.startActivity(intent)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.koitharu.kotatsu.favourites.ui.container
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@@ -102,7 +103,7 @@ class FavouritesContainerFragment : BaseFragment<FragmentFavouritesContainerBind
|
||||
override fun onClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.button_retry -> startActivity(
|
||||
FavouriteCategoriesActivity.newIntent(v.context),
|
||||
Intent(v.context, FavouriteCategoriesActivity::class.java),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.koitharu.kotatsu.favourites.ui.container
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
@@ -19,7 +20,7 @@ class FavouritesContainerMenuProvider(
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
when (menuItem.itemId) {
|
||||
R.id.action_manage -> {
|
||||
context.startActivity(FavouriteCategoriesActivity.newIntent(context))
|
||||
context.startActivity(Intent(context, FavouriteCategoriesActivity::class.java))
|
||||
}
|
||||
|
||||
else -> return false
|
||||
|
||||
@@ -10,7 +10,6 @@ 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.core.db.entity.MangaEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||
import org.koitharu.kotatsu.list.domain.ListSortOrder
|
||||
|
||||
@@ -21,10 +20,6 @@ abstract class HistoryDao {
|
||||
@Query("SELECT * FROM history WHERE deleted_at = 0 ORDER BY updated_at DESC LIMIT :limit OFFSET :offset")
|
||||
abstract suspend fun findAll(offset: Int, limit: Int): List<HistoryWithManga>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM history WHERE deleted_at = 0 AND manga_id IN (:ids)")
|
||||
abstract suspend fun findAll(ids: Collection<Long>): List<HistoryEntity>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM history WHERE deleted_at = 0 ORDER BY updated_at DESC")
|
||||
abstract fun observeAll(): Flow<List<HistoryWithManga>>
|
||||
@@ -33,6 +28,7 @@ abstract class HistoryDao {
|
||||
@Query("SELECT * FROM history WHERE deleted_at = 0 ORDER BY updated_at DESC LIMIT :limit")
|
||||
abstract fun observeAll(limit: Int): Flow<List<HistoryWithManga>>
|
||||
|
||||
// TODO pagination
|
||||
fun observeAll(order: ListSortOrder): Flow<List<HistoryWithManga>> {
|
||||
val orderBy = when (order) {
|
||||
ListSortOrder.LAST_READ -> "history.updated_at DESC"
|
||||
@@ -56,9 +52,6 @@ abstract class HistoryDao {
|
||||
return observeAllImpl(query)
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM history WHERE deleted_at = 0)")
|
||||
abstract suspend fun findAllManga(): List<MangaEntity>
|
||||
|
||||
@Query("SELECT manga_id FROM history WHERE deleted_at = 0")
|
||||
abstract suspend fun findAllIds(): LongArray
|
||||
|
||||
|
||||
@@ -4,14 +4,14 @@ import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import com.google.android.material.badge.BadgeDrawable
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.databinding.ItemHeaderButtonBinding
|
||||
import org.koitharu.kotatsu.databinding.ItemHeaderBinding
|
||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
fun listHeaderAD(
|
||||
listener: ListHeaderClickListener?,
|
||||
) = adapterDelegateViewBinding<ListHeader, ListModel, ItemHeaderButtonBinding>(
|
||||
{ inflater, parent -> ItemHeaderButtonBinding.inflate(inflater, parent, false) },
|
||||
) = adapterDelegateViewBinding<ListHeader, ListModel, ItemHeaderBinding>(
|
||||
{ inflater, parent -> ItemHeaderBinding.inflate(inflater, parent, false) },
|
||||
) {
|
||||
var badge: BadgeDrawable? = null
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ class ListConfigViewModel @Inject constructor(
|
||||
ListConfigSection.General -> null
|
||||
ListConfigSection.Updated -> null
|
||||
ListConfigSection.History -> settings.historySortOrder
|
||||
ListConfigSection.Suggestions -> ListSortOrder.RELEVANCE // TODO
|
||||
ListConfigSection.Suggestions -> ListSortOrder.RELEVANCE
|
||||
}
|
||||
|
||||
fun setSortOrder(position: Int) {
|
||||
|
||||
@@ -150,9 +150,9 @@ class LocalMangaRepository @Inject constructor(
|
||||
return channelFlow {
|
||||
for (file in files) {
|
||||
launch {
|
||||
val mangaInput = LocalMangaInput.of(file)
|
||||
val mangaInput = LocalMangaInput.ofOrNull(file)
|
||||
runCatchingCancellable {
|
||||
val mangaInfo = mangaInput.getMangaInfo()
|
||||
val mangaInfo = mangaInput?.getMangaInfo()
|
||||
if (mangaInfo != null && mangaInfo.id == remoteManga.id) {
|
||||
send(mangaInput)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user