Merge branch 'feature/nextgen_dagger' into feature/nextgen

pull/189/head
Koitharu 4 years ago
commit 532ec0129a
No known key found for this signature in database
GPG Key ID: 8E861F8CE6E7CE27

@ -3,6 +3,7 @@ plugins {
id 'kotlin-android' id 'kotlin-android'
id 'kotlin-kapt' id 'kotlin-kapt'
id 'kotlin-parcelize' id 'kotlin-parcelize'
id 'dagger.hilt.android.plugin'
} }
android { android {
@ -29,7 +30,7 @@ android {
buildConfigField 'String', 'SHIKIMORI_CLIENT_ID', "\"${localProperty('shikimori.clientId')}\"" buildConfigField 'String', 'SHIKIMORI_CLIENT_ID', "\"${localProperty('shikimori.clientId')}\""
buildConfigField 'String', 'SHIKIMORI_CLIENT_SECRET', "\"${localProperty('shikimori.clientSecret')}\"" buildConfigField 'String', 'SHIKIMORI_CLIENT_SECRET', "\"${localProperty('shikimori.clientSecret')}\""
if (currentBranch() == "feature/nextgen") { if (currentBranch().startsWith("feature/nextgen")) {
applicationIdSuffix = '.next' applicationIdSuffix = '.next'
} }
} }
@ -118,7 +119,11 @@ dependencies {
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2' implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2' implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2'
implementation 'io.insert-koin:koin-android:3.2.0' implementation "com.google.dagger:hilt-android:2.42"
kapt "com.google.dagger:hilt-compiler:2.42"
implementation 'androidx.hilt:hilt-work:1.0.0'
kapt 'androidx.hilt:hilt-compiler:1.0.0'
implementation 'io.coil-kt:coil-base:2.1.0' implementation 'io.coil-kt:coil-base:2.1.0'
implementation 'io.coil-kt:coil-svg:2.1.0' implementation 'io.coil-kt:coil-svg:2.1.0'
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
@ -139,9 +144,10 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3' androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3'
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4' androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
androidTestImplementation 'io.insert-koin:koin-test:3.2.0'
androidTestImplementation 'io.insert-koin:koin-test-junit4:3.2.0'
androidTestImplementation 'androidx.room:room-testing:2.4.2' androidTestImplementation 'androidx.room:room-testing:2.4.2'
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.13.0' androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.13.0'
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.42'
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.42'
} }

@ -6,4 +6,4 @@ import kotlin.coroutines.suspendCoroutine
suspend fun Instrumentation.awaitForIdle() = suspendCoroutine<Unit> { cont -> suspend fun Instrumentation.awaitForIdle() = suspendCoroutine<Unit> { cont ->
waitForIdle { cont.resume(Unit) } waitForIdle { cont.resume(Unit) }
} }

@ -3,10 +3,10 @@ package org.koitharu.kotatsu.core.db
import androidx.room.testing.MigrationTestHelper import androidx.room.testing.MigrationTestHelper
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import kotlin.test.assertEquals
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class MangaDatabaseTest { class MangaDatabaseTest {
@ -37,7 +37,7 @@ class MangaDatabaseTest {
TEST_DB, TEST_DB,
migration.endVersion, migration.endVersion,
true, true,
migration migration,
).close() ).close()
} }
} }

@ -6,28 +6,40 @@ import android.os.Build
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import javax.inject.Inject
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.koin.test.KoinTest
import org.koin.test.inject
import org.koitharu.kotatsu.SampleData import org.koitharu.kotatsu.SampleData
import org.koitharu.kotatsu.awaitForIdle import org.koitharu.kotatsu.awaitForIdle
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.history.domain.HistoryRepository
import kotlin.test.assertEquals
import kotlin.test.assertTrue
@HiltAndroidTest
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ShortcutsUpdaterTest : KoinTest { class ShortcutsUpdaterTest {
private val historyRepository by inject<HistoryRepository>() @get:Rule
private val shortcutsUpdater by inject<ShortcutsUpdater>() var hiltRule = HiltAndroidRule(this)
private val database by inject<MangaDatabase>()
@Inject
lateinit var historyRepository: HistoryRepository
@Inject
lateinit var shortcutsUpdater: ShortcutsUpdater
@Inject
lateinit var database: MangaDatabase
@Before @Before
fun setUp() { fun setUp() {
hiltRule.inject()
database.clearAllTables() database.clearAllTables()
} }
@ -43,7 +55,7 @@ class ShortcutsUpdaterTest : KoinTest {
chapterId = SampleData.chapter.id, chapterId = SampleData.chapter.id,
page = 4, page = 4,
scroll = 2, scroll = 2,
percent = 0.3f percent = 0.3f,
) )
awaitUpdate() awaitUpdate()
@ -62,4 +74,4 @@ class ShortcutsUpdaterTest : KoinTest {
instrumentation.awaitForIdle() instrumentation.awaitForIdle()
shortcutsUpdater.await() shortcutsUpdater.await()
} }
} }

@ -3,33 +3,46 @@ package org.koitharu.kotatsu.settings.backup
import android.content.res.AssetManager import android.content.res.AssetManager
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import java.io.File
import javax.inject.Inject
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Assert.*
import org.junit.Before import org.junit.Before
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.koin.test.KoinTest
import org.koin.test.get
import org.koin.test.inject
import org.koitharu.kotatsu.SampleData import org.koitharu.kotatsu.SampleData
import org.koitharu.kotatsu.core.backup.BackupRepository import org.koitharu.kotatsu.core.backup.BackupRepository
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.toMangaTags import org.koitharu.kotatsu.core.db.entity.toMangaTags
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.history.domain.HistoryRepository
import java.io.File
import kotlin.test.*
@HiltAndroidTest
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class AppBackupAgentTest : KoinTest { class AppBackupAgentTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
@Inject
lateinit var historyRepository: HistoryRepository
private val historyRepository by inject<HistoryRepository>() @Inject
private val favouritesRepository by inject<FavouritesRepository>() lateinit var favouritesRepository: FavouritesRepository
private val backupRepository by inject<BackupRepository>()
private val database by inject<MangaDatabase>() @Inject
lateinit var backupRepository: BackupRepository
@Inject
lateinit var database: MangaDatabase
@Before @Before
fun setUp() { fun setUp() {
hiltRule.inject()
database.clearAllTables() database.clearAllTables()
} }
@ -51,7 +64,10 @@ class AppBackupAgentTest : KoinTest {
val history = checkNotNull(historyRepository.getOne(SampleData.manga)) val history = checkNotNull(historyRepository.getOne(SampleData.manga))
val agent = AppBackupAgent() val agent = AppBackupAgent()
val backup = agent.createBackupFile(get(), backupRepository) val backup = agent.createBackupFile(
context = InstrumentationRegistry.getInstrumentation().targetContext,
repository = backupRepository,
)
database.clearAllTables() database.clearAllTables()
assertTrue(favouritesRepository.getAllManga().isEmpty()) assertTrue(favouritesRepository.getAllManga().isEmpty())
@ -63,10 +79,10 @@ class AppBackupAgentTest : KoinTest {
assertEquals(category, favouritesRepository.getCategory(category.id)) assertEquals(category, favouritesRepository.getCategory(category.id))
assertEquals(history, historyRepository.getOne(SampleData.manga)) assertEquals(history, historyRepository.getOne(SampleData.manga))
assertContentEquals(listOf(SampleData.manga), favouritesRepository.getManga(category.id)) assertEquals(listOf(SampleData.manga), favouritesRepository.getManga(category.id))
val allTags = database.tagsDao.findTags(SampleData.tag.source.name).toMangaTags() val allTags = database.tagsDao.findTags(SampleData.tag.source.name).toMangaTags()
assertContains(allTags, SampleData.tag) assertTrue(SampleData.tag in allTags)
} }
@Test @Test

@ -1,24 +1,39 @@
package org.koitharu.kotatsu.tracker.domain package org.koitharu.kotatsu.tracker.domain
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import javax.inject.Inject
import junit.framework.TestCase.*
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.koin.test.KoinTest
import org.koin.test.inject
import org.koitharu.kotatsu.SampleData import org.koitharu.kotatsu.SampleData
import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
@HiltAndroidTest
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class TrackerTest : KoinTest { class TrackerTest {
private val repository by inject<TrackingRepository>() @get:Rule
private val dataRepository by inject<MangaDataRepository>() var hiltRule = HiltAndroidRule(this)
private val tracker by inject<Tracker>()
@Inject
lateinit var repository: TrackingRepository
@Inject
lateinit var dataRepository: MangaDataRepository
@Inject
lateinit var tracker: Tracker
@Before
fun setUp() {
hiltRule.inject()
}
@Test @Test
fun noUpdates() = runTest { fun noUpdates() = runTest {
@ -180,4 +195,4 @@ class TrackerTest : KoinTest {
dataRepository.storeManga(manga) dataRepository.storeManga(manga)
return manga return manga
} }
} }

@ -204,6 +204,10 @@
android:exported="false" android:exported="false"
android:label="@string/history" android:label="@string/history"
android:syncable="true" /> android:syncable="true" />
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove" />
<receiver <receiver
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetProvider" android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetProvider"

@ -5,85 +5,50 @@ import android.content.Context
import android.os.StrictMode import android.os.StrictMode
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.fragment.app.strictmode.FragmentStrictMode import androidx.fragment.app.strictmode.FragmentStrictMode
import androidx.hilt.work.HiltWorkerFactory
import androidx.room.InvalidationTracker import androidx.room.InvalidationTracker
import androidx.work.Configuration
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject
import org.acra.ReportField import org.acra.ReportField
import org.acra.config.dialog import org.acra.config.dialog
import org.acra.config.mailSender import org.acra.config.mailSender
import org.acra.data.StringFormat import org.acra.data.StringFormat
import org.acra.ktx.initAcra import org.acra.ktx.initAcra
import org.koin.android.ext.android.get
import org.koin.android.ext.android.getKoin
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin
import org.koitharu.kotatsu.bookmarks.bookmarksModule
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.databaseModule
import org.koitharu.kotatsu.core.github.appUpdateModule
import org.koitharu.kotatsu.core.network.networkModule
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.uiModule
import org.koitharu.kotatsu.details.detailsModule
import org.koitharu.kotatsu.explore.exploreModule
import org.koitharu.kotatsu.favourites.favouritesModule
import org.koitharu.kotatsu.history.historyModule
import org.koitharu.kotatsu.library.libraryModule
import org.koitharu.kotatsu.local.data.PagesCache import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.local.localModule
import org.koitharu.kotatsu.main.mainModule
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.reader.readerModule
import org.koitharu.kotatsu.remotelist.remoteListModule
import org.koitharu.kotatsu.scrobbling.shikimori.shikimoriModule
import org.koitharu.kotatsu.search.searchModule
import org.koitharu.kotatsu.settings.settingsModule
import org.koitharu.kotatsu.suggestions.suggestionsModule
import org.koitharu.kotatsu.sync.syncModule
import org.koitharu.kotatsu.tracker.trackerModule
import org.koitharu.kotatsu.widget.appWidgetModule
class KotatsuApp : Application() { @HiltAndroidApp
class KotatsuApp : Application(), Configuration.Provider {
@Inject
lateinit var databaseObservers: Set<@JvmSuppressWildcards InvalidationTracker.Observer>
@Inject
lateinit var activityLifecycleCallbacks: Set<@JvmSuppressWildcards ActivityLifecycleCallbacks>
@Inject
lateinit var database: MangaDatabase
@Inject
lateinit var settings: AppSettings
@Inject
lateinit var workerFactory: HiltWorkerFactory
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
enableStrictMode() enableStrictMode()
} }
initKoin() AppCompatDelegate.setDefaultNightMode(settings.theme)
AppCompatDelegate.setDefaultNightMode(get<AppSettings>().theme)
setupActivityLifecycleCallbacks() setupActivityLifecycleCallbacks()
setupDatabaseObservers() setupDatabaseObservers()
} }
private fun initKoin() {
startKoin {
androidContext(this@KotatsuApp)
modules(
networkModule,
databaseModule,
appUpdateModule,
uiModule,
mainModule,
searchModule,
localModule,
favouritesModule,
historyModule,
remoteListModule,
detailsModule,
trackerModule,
settingsModule,
readerModule,
appWidgetModule,
suggestionsModule,
syncModule,
shikimoriModule,
bookmarksModule,
libraryModule,
exploreModule,
)
}
}
override fun attachBaseContext(base: Context?) { override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base) super.attachBaseContext(base)
initAcra { initAcra {
@ -115,18 +80,21 @@ class KotatsuApp : Application() {
} }
} }
override fun getWorkManagerConfiguration(): Configuration {
return Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
}
private fun setupDatabaseObservers() { private fun setupDatabaseObservers() {
val observers = getKoin().getAll<InvalidationTracker.Observer>()
val database = get<MangaDatabase>()
val tracker = database.invalidationTracker val tracker = database.invalidationTracker
observers.forEach { databaseObservers.forEach {
tracker.addObserver(it) tracker.addObserver(it)
} }
} }
private fun setupActivityLifecycleCallbacks() { private fun setupActivityLifecycleCallbacks() {
val callbacks = getKoin().getAll<ActivityLifecycleCallbacks>() activityLifecycleCallbacks.forEach {
callbacks.forEach {
registerActivityLifecycleCallbacks(it) registerActivityLifecycleCallbacks(it)
} }
} }

@ -1,14 +1,35 @@
package org.koitharu.kotatsu.base.domain package org.koitharu.kotatsu.base.domain
import android.graphics.BitmapFactory
import android.net.Uri
import android.util.Size
import androidx.room.withTransaction import androidx.room.withTransaction
import java.io.File
import java.io.InputStream
import java.util.zip.ZipFile
import javax.inject.Inject
import kotlin.math.roundToInt
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import okhttp3.OkHttpClient
import okhttp3.Request
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.* import org.koitharu.kotatsu.core.db.entity.*
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.ReaderMode import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.util.await
class MangaDataRepository(private val db: MangaDatabase) { private const val MIN_WEBTOON_RATIO = 2
class MangaDataRepository @Inject constructor(
private val okHttpClient: OkHttpClient,
private val db: MangaDatabase,
) {
suspend fun savePreferences(manga: Manga, mode: ReaderMode) { suspend fun savePreferences(manga: Manga, mode: ReaderMode) {
val tags = manga.tags.toEntities() val tags = manga.tags.toEntities()
@ -18,8 +39,8 @@ class MangaDataRepository(private val db: MangaDatabase) {
db.preferencesDao.upsert( db.preferencesDao.upsert(
MangaPrefsEntity( MangaPrefsEntity(
mangaId = manga.id, mangaId = manga.id,
mode = mode.id mode = mode.id,
) ),
) )
} }
} }
@ -49,4 +70,59 @@ class MangaDataRepository(private val db: MangaDatabase) {
suspend fun findTags(source: MangaSource): Set<MangaTag> { suspend fun findTags(source: MangaSource): Set<MangaTag> {
return db.tagsDao.findTags(source.name).toMangaTags() return db.tagsDao.findTags(source.name).toMangaTags()
} }
}
/**
* Automatic determine type of manga by page size
* @return ReaderMode.WEBTOON if page is wide
*/
suspend fun determineMangaIsWebtoon(repository: MangaRepository, pages: List<MangaPage>): Boolean {
val pageIndex = (pages.size * 0.3).roundToInt()
val page = requireNotNull(pages.getOrNull(pageIndex)) { "No pages" }
val url = repository.getPageUrl(page)
val uri = Uri.parse(url)
val size = if (uri.scheme == "cbz") {
runInterruptible(Dispatchers.IO) {
val zip = ZipFile(uri.schemeSpecificPart)
val entry = zip.getEntry(uri.fragment)
zip.getInputStream(entry).use {
getBitmapSize(it)
}
}
} else {
val request = Request.Builder()
.url(url)
.get()
.header(CommonHeaders.REFERER, page.referer)
.cacheControl(CommonHeaders.CACHE_CONTROL_DISABLED)
.build()
okHttpClient.newCall(request).await().use {
runInterruptible(Dispatchers.IO) {
getBitmapSize(it.body?.byteStream())
}
}
}
return size.width * MIN_WEBTOON_RATIO < size.height
}
companion object {
suspend fun getImageMimeType(file: File): String? = runInterruptible(Dispatchers.IO) {
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeFile(file.path, options)?.recycle()
options.outMimeType
}
private fun getBitmapSize(input: InputStream?): Size {
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeStream(input, null, options)?.recycle()
val imageHeight: Int = options.outHeight
val imageWidth: Int = options.outWidth
check(imageHeight > 0 && imageWidth > 0)
return Size(imageWidth, imageHeight)
}
}
}

@ -1,76 +0,0 @@
package org.koitharu.kotatsu.base.domain
import android.graphics.BitmapFactory
import android.net.Uri
import android.util.Size
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import okhttp3.OkHttpClient
import okhttp3.Request
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.await
import java.io.File
import java.io.InputStream
import java.util.zip.ZipFile
import kotlin.math.roundToInt
object MangaUtils : KoinComponent {
private const val MIN_WEBTOON_RATIO = 2
/**
* Automatic determine type of manga by page size
* @return ReaderMode.WEBTOON if page is wide
*/
suspend fun determineMangaIsWebtoon(pages: List<MangaPage>): Boolean {
val pageIndex = (pages.size * 0.3).roundToInt()
val page = requireNotNull(pages.getOrNull(pageIndex)) { "No pages" }
val url = MangaRepository(page.source).getPageUrl(page)
val uri = Uri.parse(url)
val size = if (uri.scheme == "cbz") {
runInterruptible(Dispatchers.IO) {
val zip = ZipFile(uri.schemeSpecificPart)
val entry = zip.getEntry(uri.fragment)
zip.getInputStream(entry).use {
getBitmapSize(it)
}
}
} else {
val request = Request.Builder()
.url(url)
.get()
.header(CommonHeaders.REFERER, page.referer)
.cacheControl(CommonHeaders.CACHE_CONTROL_DISABLED)
.build()
get<OkHttpClient>().newCall(request).await().use {
runInterruptible(Dispatchers.IO) {
getBitmapSize(it.body?.byteStream())
}
}
}
return size.width * MIN_WEBTOON_RATIO < size.height
}
suspend fun getImageMimeType(file: File): String? = runInterruptible(Dispatchers.IO) {
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeFile(file.path, options)?.recycle()
options.outMimeType
}
private fun getBitmapSize(input: InputStream?): Size {
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeStream(input, null, options)?.recycle()
val imageHeight: Int = options.outHeight
val imageWidth: Int = options.outWidth
check(imageHeight > 0 && imageWidth > 0)
return Size(imageWidth, imageHeight)
}
}

@ -18,19 +18,24 @@ import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import org.koin.android.ext.android.get import dagger.hilt.android.EntryPointAccessors
import javax.inject.Inject
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.util.ActionModeDelegate import org.koitharu.kotatsu.base.ui.util.ActionModeDelegate
import org.koitharu.kotatsu.base.ui.util.BaseActivityEntryPoint
import org.koitharu.kotatsu.base.ui.util.WindowInsetsDelegate import org.koitharu.kotatsu.base.ui.util.WindowInsetsDelegate
import org.koitharu.kotatsu.base.ui.util.inject
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.settings.SettingsActivity
abstract class BaseActivity<B : ViewBinding> : abstract class BaseActivity<B : ViewBinding> :
AppCompatActivity(), AppCompatActivity(),
WindowInsetsDelegate.WindowInsetsListener { WindowInsetsDelegate.WindowInsetsListener {
@Inject
lateinit var settings: AppSettings
protected lateinit var binding: B protected lateinit var binding: B
private set private set
@ -43,7 +48,7 @@ abstract class BaseActivity<B : ViewBinding> :
val actionModeDelegate = ActionModeDelegate() val actionModeDelegate = ActionModeDelegate()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
val settings = get<AppSettings>() EntryPointAccessors.fromApplication(this, BaseActivityEntryPoint::class.java).inject(this)
val isAmoled = settings.isAmoledTheme val isAmoled = settings.isAmoledTheme
val isDynamic = settings.isDynamicTheme val isDynamic = settings.isDynamicTheme
// TODO support DialogWhenLarge theme // TODO support DialogWhenLarge theme
@ -97,7 +102,7 @@ abstract class BaseActivity<B : ViewBinding> :
protected fun isDarkAmoledTheme(): Boolean { protected fun isDarkAmoledTheme(): Boolean {
val uiMode = resources.configuration.uiMode val uiMode = resources.configuration.uiMode
val isNight = uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES val isNight = uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
return isNight && get<AppSettings>().isAmoledTheme return isNight && settings.isAmoledTheme
} }
@CallSuper @CallSuper
@ -129,4 +134,4 @@ abstract class BaseActivity<B : ViewBinding> :
super.onBackPressed() super.onBackPressed()
} }
} }
} }

@ -9,13 +9,13 @@ import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams import android.view.ViewGroup.LayoutParams
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.google.android.material.R as materialR
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.dialog.AppBottomSheetDialog import org.koitharu.kotatsu.base.ui.dialog.AppBottomSheetDialog
import org.koitharu.kotatsu.utils.ext.displayCompat import org.koitharu.kotatsu.utils.ext.displayCompat
import com.google.android.material.R as materialR
abstract class BaseBottomSheet<B : ViewBinding> : BottomSheetDialogFragment() { abstract class BaseBottomSheet<B : ViewBinding> : BottomSheetDialogFragment() {
@ -30,7 +30,7 @@ abstract class BaseBottomSheet<B : ViewBinding> : BottomSheetDialogFragment() {
final override fun onCreateView( final override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?,
): View { ): View {
val binding = onInflateView(inflater, container) val binding = onInflateView(inflater, container)
viewBinding = binding viewBinding = binding
@ -83,4 +83,4 @@ abstract class BaseBottomSheet<B : ViewBinding> : BottomSheetDialogFragment() {
} }
b.isDraggable = !isLocked b.isDraggable = !isLocked
} }
} }

@ -8,18 +8,21 @@ import androidx.core.graphics.Insets
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.koin.android.ext.android.inject import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner
import org.koitharu.kotatsu.base.ui.util.WindowInsetsDelegate import org.koitharu.kotatsu.base.ui.util.WindowInsetsDelegate
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.settings.SettingsHeadersFragment import org.koitharu.kotatsu.settings.SettingsHeadersFragment
@AndroidEntryPoint
abstract class BasePreferenceFragment(@StringRes private val titleId: Int) : abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
PreferenceFragmentCompat(), PreferenceFragmentCompat(),
WindowInsetsDelegate.WindowInsetsListener, WindowInsetsDelegate.WindowInsetsListener,
RecyclerViewOwner { RecyclerViewOwner {
protected val settings by inject<AppSettings>(mode = LazyThreadSafetyMode.NONE) @Inject
lateinit var settings: AppSettings
@Suppress("LeakingThis") @Suppress("LeakingThis")
protected val insetsDelegate = WindowInsetsDelegate(this) protected val insetsDelegate = WindowInsetsDelegate(this)
@ -48,7 +51,7 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
@CallSuper @CallSuper
override fun onWindowInsetsChanged(insets: Insets) { override fun onWindowInsetsChanged(insets: Insets) {
listView.updatePadding( listView.updatePadding(
bottom = insets.bottom bottom = insets.bottom,
) )
} }
@ -57,4 +60,4 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
(parentFragment as? SettingsHeadersFragment)?.setTitle(title) (parentFragment as? SettingsHeadersFragment)?.setTitle(title)
?: activity?.setTitle(title) ?: activity?.setTitle(title)
} }
} }

@ -34,4 +34,4 @@ abstract class CoroutineIntentService : BaseService() {
} }
protected abstract suspend fun processIntent(intent: Intent?) protected abstract suspend fun processIntent(intent: Intent?)
} }

@ -4,8 +4,11 @@ import android.app.Activity
import android.app.Application.ActivityLifecycleCallbacks import android.app.Application.ActivityLifecycleCallbacks
import android.os.Bundle import android.os.Bundle
import java.util.* import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
class ActivityRecreationHandle : ActivityLifecycleCallbacks { @Singleton
class ActivityRecreationHandle @Inject constructor() : ActivityLifecycleCallbacks {
private val activities = WeakHashMap<Activity, Unit>() private val activities = WeakHashMap<Activity, Unit>()
@ -31,4 +34,4 @@ class ActivityRecreationHandle : ActivityLifecycleCallbacks {
val snapshot = activities.keys.toList() val snapshot = activities.keys.toList()
snapshot.forEach { it.recreate() } snapshot.forEach { it.recreate() }
} }
} }

@ -0,0 +1,18 @@
package org.koitharu.kotatsu.base.ui.util
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.core.prefs.AppSettings
@EntryPoint
@InstallIn(SingletonComponent::class)
interface BaseActivityEntryPoint {
val settings: AppSettings
}
// Hilt cannot inject into parametrized classes
fun BaseActivityEntryPoint.inject(activity: BaseActivity<*>) {
activity.settings = settings
}

@ -1,14 +0,0 @@
package org.koitharu.kotatsu.bookmarks
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
import org.koitharu.kotatsu.bookmarks.ui.BookmarksViewModel
val bookmarksModule
get() = module {
factory { BookmarksRepository(get()) }
viewModel { BookmarksViewModel(get()) }
}

@ -2,6 +2,7 @@ package org.koitharu.kotatsu.bookmarks.domain
import android.database.SQLException import android.database.SQLException
import androidx.room.withTransaction import androidx.room.withTransaction
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.base.domain.ReversibleHandle import org.koitharu.kotatsu.base.domain.ReversibleHandle
@ -17,7 +18,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.ext.mapItems import org.koitharu.kotatsu.utils.ext.mapItems
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
class BookmarksRepository( class BookmarksRepository @Inject constructor(
private val db: MangaDatabase, private val db: MangaDatabase,
) { ) {
@ -86,4 +87,4 @@ class BookmarksRepository(
} }
} }
} }
} }

@ -6,10 +6,12 @@ import android.os.Bundle
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.commit import androidx.fragment.app.commit
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivityContainerBinding import org.koitharu.kotatsu.databinding.ActivityContainerBinding
@AndroidEntryPoint
class BookmarksActivity : BaseActivity<ActivityContainerBinding>() { class BookmarksActivity : BaseActivity<ActivityContainerBinding>() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -29,7 +31,7 @@ class BookmarksActivity : BaseActivity<ActivityContainerBinding>() {
with(binding.toolbar) { with(binding.toolbar) {
updatePadding( updatePadding(
left = insets.left, left = insets.left,
right = insets.right right = insets.right,
) )
} }
} }
@ -38,4 +40,4 @@ class BookmarksActivity : BaseActivity<ActivityContainerBinding>() {
fun newIntent(context: Context) = Intent(context, BookmarksActivity::class.java) fun newIntent(context: Context) = Intent(context, BookmarksActivity::class.java)
} }
} }

@ -5,9 +5,11 @@ import android.view.*
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.viewModels
import coil.ImageLoader
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import org.koin.android.ext.android.get import dagger.hilt.android.AndroidEntryPoint
import org.koin.androidx.viewmodel.ext.android.viewModel import javax.inject.Inject
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.reverseAsync import org.koitharu.kotatsu.base.domain.reverseAsync
import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.base.ui.BaseFragment
@ -30,13 +32,17 @@ import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.invalidateNestedItemDecorations import org.koitharu.kotatsu.utils.ext.invalidateNestedItemDecorations
import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf
@AndroidEntryPoint
class BookmarksFragment : class BookmarksFragment :
BaseFragment<FragmentListSimpleBinding>(), BaseFragment<FragmentListSimpleBinding>(),
ListStateHolderListener, ListStateHolderListener,
OnListItemClickListener<Bookmark>, OnListItemClickListener<Bookmark>,
SectionedSelectionController.Callback<Manga> { SectionedSelectionController.Callback<Manga> {
private val viewModel by viewModel<BookmarksViewModel>() @Inject
lateinit var coil: ImageLoader
private val viewModel by viewModels<BookmarksViewModel>()
private var adapter: BookmarksGroupAdapter? = null private var adapter: BookmarksGroupAdapter? = null
private var selectionController: SectionedSelectionController<Manga>? = null private var selectionController: SectionedSelectionController<Manga>? = null
@ -53,7 +59,7 @@ class BookmarksFragment :
) )
adapter = BookmarksGroupAdapter( adapter = BookmarksGroupAdapter(
lifecycleOwner = viewLifecycleOwner, lifecycleOwner = viewLifecycleOwner,
coil = get(), coil = coil,
listener = this, listener = this,
selectionController = checkNotNull(selectionController), selectionController = checkNotNull(selectionController),
bookmarkClickListener = this, bookmarkClickListener = this,

@ -2,6 +2,8 @@ package org.koitharu.kotatsu.bookmarks.ui
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@ -18,7 +20,8 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
class BookmarksViewModel( @HiltViewModel
class BookmarksViewModel @Inject constructor(
private val repository: BookmarksRepository, private val repository: BookmarksRepository,
) : BaseViewModel() { ) : BaseViewModel() {
@ -33,7 +36,7 @@ class BookmarksViewModel(
textPrimary = R.string.no_bookmarks_yet, textPrimary = R.string.no_bookmarks_yet,
textSecondary = R.string.no_bookmarks_summary, textSecondary = R.string.no_bookmarks_summary,
actionStringRes = 0, actionStringRes = 0,
) ),
) )
} else list.map { (manga, bookmarks) -> } else list.map { (manga, bookmarks) ->
BookmarksGroup(manga, bookmarks) BookmarksGroup(manga, bookmarks)
@ -42,11 +45,10 @@ class BookmarksViewModel(
.catch { e -> e.toErrorState(canRetry = false) } .catch { e -> e.toErrorState(canRetry = false) }
.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) .asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
fun removeBookmarks(ids: Map<Manga, Set<Long>>) { fun removeBookmarks(ids: Map<Manga, Set<Long>>) {
launchJob(Dispatchers.Default) { launchJob(Dispatchers.Default) {
val handle = repository.removeBookmarks(ids) val handle = repository.removeBookmarks(ids)
onActionDone.postCall(ReversibleAction(R.string.bookmarks_removed, handle)) onActionDone.postCall(ReversibleAction(R.string.bookmarks_removed, handle))
} }
} }
} }

@ -11,21 +11,27 @@ import android.webkit.WebSettings
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.fragment.app.setFragmentResult import androidx.fragment.app.setFragmentResult
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koin.android.ext.android.get import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.koitharu.kotatsu.base.ui.AlertDialogFragment import org.koitharu.kotatsu.base.ui.AlertDialogFragment
import org.koitharu.kotatsu.core.network.AndroidCookieJar
import org.koitharu.kotatsu.core.network.UserAgentInterceptor import org.koitharu.kotatsu.core.network.UserAgentInterceptor
import org.koitharu.kotatsu.databinding.FragmentCloudflareBinding import org.koitharu.kotatsu.databinding.FragmentCloudflareBinding
import org.koitharu.kotatsu.utils.ext.stringArgument import org.koitharu.kotatsu.utils.ext.stringArgument
import org.koitharu.kotatsu.utils.ext.withArgs import org.koitharu.kotatsu.utils.ext.withArgs
@AndroidEntryPoint
class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), CloudFlareCallback { class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), CloudFlareCallback {
private val url by stringArgument(ARG_URL) private val url by stringArgument(ARG_URL)
private val pendingResult = Bundle(1) private val pendingResult = Bundle(1)
@Inject
lateinit var cookieJar: AndroidCookieJar
override fun onInflateView( override fun onInflateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup? container: ViewGroup?,
) = FragmentCloudflareBinding.inflate(inflater, container, false) ) = FragmentCloudflareBinding.inflate(inflater, container, false)
@SuppressLint("SetJavaScriptEnabled") @SuppressLint("SetJavaScriptEnabled")
@ -38,7 +44,7 @@ class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), Cloud
databaseEnabled = true databaseEnabled = true
userAgentString = UserAgentInterceptor.userAgent userAgentString = UserAgentInterceptor.userAgent
} }
binding.webView.webViewClient = CloudFlareClient(get(), this, url.orEmpty()) binding.webView.webViewClient = CloudFlareClient(cookieJar, this, url.orEmpty())
CookieManager.getInstance().setAcceptThirdPartyCookies(binding.webView, true) CookieManager.getInstance().setAcceptThirdPartyCookies(binding.webView, true)
if (url.isNullOrEmpty()) { if (url.isNullOrEmpty()) {
dismissAllowingStateLoss() dismissAllowingStateLoss()
@ -90,4 +96,4 @@ class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), Cloud
putString(ARG_URL, url) putString(ARG_URL, url)
} }
} }
} }

@ -0,0 +1,160 @@
package org.koitharu.kotatsu.core
import android.app.Application
import android.content.Context
import android.provider.SearchRecentSuggestions
import android.text.Html
import androidx.collection.arraySetOf
import androidx.room.InvalidationTracker
import coil.ComponentRegistry
import coil.ImageLoader
import coil.decode.SvgDecoder
import coil.disk.DiskCache
import coil.util.DebugLogger
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ElementsIntoSet
import java.util.concurrent.TimeUnit
import javax.inject.Singleton
import kotlinx.coroutines.Dispatchers
import okhttp3.CookieJar
import okhttp3.OkHttpClient
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.base.ui.util.ActivityRecreationHandle
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.network.*
import org.koitharu.kotatsu.core.os.ShortcutsUpdater
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.favicon.FaviconFetcher
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.local.data.CacheDir
import org.koitharu.kotatsu.local.data.CbzFetcher
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
import org.koitharu.kotatsu.parsers.MangaLoaderContext
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.utils.ext.isLowRamDevice
import org.koitharu.kotatsu.utils.image.CoilImageGetter
import org.koitharu.kotatsu.widget.WidgetUpdater
@Module
@InstallIn(SingletonComponent::class)
interface AppModule {
@Binds
fun bindCookieJar(androidCookieJar: AndroidCookieJar): CookieJar
@Binds
fun bindMangaLoaderContext(mangaLoaderContextImpl: MangaLoaderContextImpl): MangaLoaderContext
@Binds
fun bindImageGetter(coilImageGetter: CoilImageGetter): Html.ImageGetter
companion object {
@Provides
@Singleton
fun provideOkHttpClient(
localStorageManager: LocalStorageManager,
cookieJar: CookieJar,
settings: AppSettings,
): OkHttpClient {
val cache = localStorageManager.createHttpCache()
return OkHttpClient.Builder().apply {
connectTimeout(20, TimeUnit.SECONDS)
readTimeout(60, TimeUnit.SECONDS)
writeTimeout(20, TimeUnit.SECONDS)
cookieJar(cookieJar)
dns(DoHManager(cache, settings))
cache(cache)
addInterceptor(GZipInterceptor())
addInterceptor(UserAgentInterceptor())
addInterceptor(CloudFlareInterceptor())
}.build()
}
@Provides
@Singleton
fun provideMangaDatabase(
@ApplicationContext context: Context,
): MangaDatabase {
return MangaDatabase(context)
}
@Provides
@Singleton
fun provideCoil(
@ApplicationContext context: Context,
okHttpClient: OkHttpClient,
mangaRepositoryFactory: MangaRepository.Factory,
): ImageLoader {
val httpClientFactory = {
okHttpClient.newBuilder()
.cache(null)
.build()
}
val diskCacheFactory = {
val rootDir = context.externalCacheDir ?: context.cacheDir
DiskCache.Builder()
.directory(rootDir.resolve(CacheDir.THUMBS.dir))
.build()
}
return ImageLoader.Builder(context)
.okHttpClient(httpClientFactory)
.interceptorDispatcher(Dispatchers.Default)
.fetcherDispatcher(Dispatchers.IO)
.decoderDispatcher(Dispatchers.Default)
.transformationDispatcher(Dispatchers.Default)
.diskCache(diskCacheFactory)
.logger(if (BuildConfig.DEBUG) DebugLogger() else null)
.allowRgb565(isLowRamDevice(context))
.components(
ComponentRegistry.Builder()
.add(SvgDecoder.Factory())
.add(CbzFetcher.Factory())
.add(FaviconFetcher.Factory(context, okHttpClient, mangaRepositoryFactory))
.build(),
).build()
}
@Provides
fun provideSearchSuggestions(
@ApplicationContext context: Context,
): SearchRecentSuggestions {
return MangaSuggestionsProvider.createSuggestions(context)
}
@Provides
@Singleton
@ElementsIntoSet
fun provideDatabaseObservers(
widgetUpdater: WidgetUpdater,
shortcutsUpdater: ShortcutsUpdater,
backupObserver: BackupObserver,
syncController: SyncController,
): Set<@JvmSuppressWildcards InvalidationTracker.Observer> = arraySetOf(
widgetUpdater,
shortcutsUpdater,
backupObserver,
syncController,
)
@Provides
@Singleton
@ElementsIntoSet
fun provideActivityLifecycleCallbacks(
appProtectHelper: AppProtectHelper,
activityRecreationHandle: ActivityRecreationHandle,
): Set<@JvmSuppressWildcards Application.ActivityLifecycleCallbacks> = arraySetOf(
appProtectHelper,
activityRecreationHandle,
)
}
}

@ -1,6 +1,7 @@
package org.koitharu.kotatsu.core.backup package org.koitharu.kotatsu.core.backup
import androidx.room.withTransaction import androidx.room.withTransaction
import javax.inject.Inject
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
@ -10,7 +11,7 @@ import org.koitharu.kotatsu.parsers.util.json.mapJSON
private const val PAGE_SIZE = 10 private const val PAGE_SIZE = 10
class BackupRepository(private val db: MangaDatabase) { class BackupRepository @Inject constructor(private val db: MangaDatabase) {
suspend fun dumpHistory(): BackupEntry { suspend fun dumpHistory(): BackupEntry {
var offset = 0 var offset = 0
@ -125,4 +126,4 @@ class BackupRepository(private val db: MangaDatabase) {
} }
return result return result
} }
} }

@ -1,9 +0,0 @@
package org.koitharu.kotatsu.core.db
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
val databaseModule
get() = module {
single { MangaDatabase(androidContext()) }
}

@ -1,9 +0,0 @@
package org.koitharu.kotatsu.core.github
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
val appUpdateModule
get() = module {
single { AppUpdateRepository(androidContext(), get()) }
}

@ -3,11 +3,14 @@ package org.koitharu.kotatsu.core.github
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.InputStream import java.io.InputStream
import java.security.MessageDigest import java.security.MessageDigest
import java.security.cert.CertificateFactory import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -22,8 +25,9 @@ import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
private const val CERT_SHA1 = "2C:19:C7:E8:07:61:2B:8E:94:51:1B:FD:72:67:07:64:5D:C2:58:AE" private const val CERT_SHA1 = "2C:19:C7:E8:07:61:2B:8E:94:51:1B:FD:72:67:07:64:5D:C2:58:AE"
class AppUpdateRepository( @Singleton
private val context: Context, class AppUpdateRepository @Inject constructor(
@ApplicationContext private val context: Context,
private val okHttp: OkHttpClient, private val okHttp: OkHttpClient,
) { ) {

@ -1,13 +1,16 @@
package org.koitharu.kotatsu.core.network package org.koitharu.kotatsu.core.network
import android.webkit.CookieManager import android.webkit.CookieManager
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import okhttp3.Cookie import okhttp3.Cookie
import okhttp3.CookieJar import okhttp3.CookieJar
import okhttp3.HttpUrl import okhttp3.HttpUrl
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class AndroidCookieJar : CookieJar { @Singleton
class AndroidCookieJar @Inject constructor() : CookieJar {
private val cookieManager = CookieManager.getInstance() private val cookieManager = CookieManager.getInstance()
@ -31,4 +34,4 @@ class AndroidCookieJar : CookieJar {
suspend fun clear() = suspendCoroutine<Boolean> { continuation -> suspend fun clear() = suspendCoroutine<Boolean> { continuation ->
cookieManager.removeAllCookies(continuation::resume) cookieManager.removeAllCookies(continuation::resume)
} }
} }

@ -1,30 +0,0 @@
package org.koitharu.kotatsu.core.network
import okhttp3.CookieJar
import okhttp3.OkHttpClient
import org.koin.dsl.bind
import org.koin.dsl.module
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import java.util.concurrent.TimeUnit
val networkModule
get() = module {
single { AndroidCookieJar() } bind CookieJar::class
single {
val cache = get<LocalStorageManager>().createHttpCache()
OkHttpClient.Builder().apply {
connectTimeout(20, TimeUnit.SECONDS)
readTimeout(60, TimeUnit.SECONDS)
writeTimeout(20, TimeUnit.SECONDS)
cookieJar(get())
dns(DoHManager(cache, get()))
cache(cache)
addInterceptor(GZipInterceptor())
addInterceptor(UserAgentInterceptor())
addInterceptor(CloudFlareInterceptor())
}.build()
}
single<MangaLoaderContext> { MangaLoaderContextImpl(get(), get(), get()) }
}

@ -13,6 +13,9 @@ import androidx.core.graphics.drawable.IconCompat
import androidx.room.InvalidationTracker import androidx.room.InvalidationTracker
import coil.ImageLoader import coil.ImageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -26,8 +29,9 @@ import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.processLifecycleScope import org.koitharu.kotatsu.utils.ext.processLifecycleScope
import org.koitharu.kotatsu.utils.ext.requireBitmap import org.koitharu.kotatsu.utils.ext.requireBitmap
class ShortcutsUpdater( @Singleton
private val context: Context, class ShortcutsUpdater @Inject constructor(
@ApplicationContext private val context: Context,
private val coil: ImageLoader, private val coil: ImageLoader,
private val historyRepository: HistoryRepository, private val historyRepository: HistoryRepository,
private val mangaRepository: MangaDataRepository, private val mangaRepository: MangaDataRepository,
@ -37,6 +41,9 @@ class ShortcutsUpdater(
private var shortcutsUpdateJob: Job? = null private var shortcutsUpdateJob: Job? = null
override fun onInvalidated(tables: MutableSet<String>) { override fun onInvalidated(tables: MutableSet<String>) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
return
}
val prevJob = shortcutsUpdateJob val prevJob = shortcutsUpdateJob
shortcutsUpdateJob = processLifecycleScope.launch(Dispatchers.Default) { shortcutsUpdateJob = processLifecycleScope.launch(Dispatchers.Default) {
prevJob?.join() prevJob?.join()
@ -48,7 +55,7 @@ class ShortcutsUpdater(
return ShortcutManagerCompat.requestPinShortcut( return ShortcutManagerCompat.requestPinShortcut(
context, context,
buildShortcutInfo(manga).build(), buildShortcutInfo(manga).build(),
null null,
) )
} }
@ -73,12 +80,12 @@ class ShortcutsUpdater(
ImageRequest.Builder(context) ImageRequest.Builder(context)
.data(manga.coverUrl) .data(manga.coverUrl)
.size(iconSize.width, iconSize.height) .size(iconSize.width, iconSize.height)
.build() .build(),
).requireBitmap() ).requireBitmap()
ThumbnailUtils.extractThumbnail(bmp, iconSize.width, iconSize.height, 0) ThumbnailUtils.extractThumbnail(bmp, iconSize.width, iconSize.height, 0)
}.fold( }.fold(
onSuccess = { IconCompat.createWithAdaptiveBitmap(it) }, onSuccess = { IconCompat.createWithAdaptiveBitmap(it) },
onFailure = { IconCompat.createWithResource(context, R.drawable.ic_shortcut_default) } onFailure = { IconCompat.createWithResource(context, R.drawable.ic_shortcut_default) },
) )
mangaRepository.storeManga(manga) mangaRepository.storeManga(manga)
return ShortcutInfoCompat.Builder(context, manga.id.toString()) return ShortcutInfoCompat.Builder(context, manga.id.toString())
@ -87,7 +94,7 @@ class ShortcutsUpdater(
.setIcon(icon) .setIcon(icon)
.setIntent( .setIntent(
ReaderActivity.newIntent(context, manga.id) ReaderActivity.newIntent(context, manga.id)
.setAction(ReaderActivity.ACTION_MANGA_READ) .setAction(ReaderActivity.ACTION_MANGA_READ),
) )
} }
@ -102,4 +109,4 @@ class ShortcutsUpdater(
} }
} }
} }
} }

@ -5,6 +5,12 @@ import android.content.Context
import android.util.Base64 import android.util.Base64
import android.webkit.WebView import android.webkit.WebView
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -14,14 +20,12 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.utils.ext.toList import org.koitharu.kotatsu.utils.ext.toList
import java.util.*
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class MangaLoaderContextImpl( @Singleton
class MangaLoaderContextImpl @Inject constructor(
override val httpClient: OkHttpClient, override val httpClient: OkHttpClient,
override val cookieJar: AndroidCookieJar, override val cookieJar: AndroidCookieJar,
private val androidContext: Context, @ApplicationContext private val androidContext: Context,
) : MangaLoaderContext() { ) : MangaLoaderContext() {
@SuppressLint("SetJavaScriptEnabled") @SuppressLint("SetJavaScriptEnabled")
@ -50,4 +54,4 @@ class MangaLoaderContextImpl(
override fun getPreferredLocales(): List<Locale> { override fun getPreferredLocales(): List<Locale> {
return LocaleListCompat.getAdjustedDefault().toList() return LocaleListCompat.getAdjustedDefault().toList()
} }
} }

@ -2,9 +2,11 @@ package org.koitharu.kotatsu.core.parser
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.* import java.util.*
import org.koin.core.component.KoinComponent import javax.inject.Inject
import org.koin.core.component.get import javax.inject.Singleton
import kotlin.collections.set
import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.model.*
interface MangaRepository { interface MangaRepository {
@ -25,21 +27,25 @@ interface MangaRepository {
suspend fun getTags(): Set<MangaTag> suspend fun getTags(): Set<MangaTag>
companion object : KoinComponent { @Singleton
class Factory @Inject constructor(
private val localMangaRepository: LocalMangaRepository,
private val loaderContext: MangaLoaderContext,
) {
private val cache = EnumMap<MangaSource, WeakReference<RemoteMangaRepository>>(MangaSource::class.java) private val cache = EnumMap<MangaSource, WeakReference<RemoteMangaRepository>>(MangaSource::class.java)
operator fun invoke(source: MangaSource): MangaRepository { fun create(source: MangaSource): MangaRepository {
if (source == MangaSource.LOCAL) { if (source == MangaSource.LOCAL) {
return get<LocalMangaRepository>() return localMangaRepository
} }
cache[source]?.get()?.let { return it } cache[source]?.get()?.let { return it }
return synchronized(cache) { return synchronized(cache) {
cache[source]?.get()?.let { return it } cache[source]?.get()?.let { return it }
val repository = RemoteMangaRepository(MangaParser(source, get())) val repository = RemoteMangaRepository(MangaParser(source, loaderContext))
cache[source] = WeakReference(repository) cache[source] = WeakReference(repository)
repository repository
} }
} }
} }
} }

@ -14,6 +14,7 @@ import coil.network.HttpException
import coil.request.Options import coil.request.Options
import coil.size.Size import coil.size.Size
import coil.size.pxOrElse import coil.size.pxOrElse
import java.net.HttpURLConnection
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
@ -25,7 +26,6 @@ import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.local.data.CacheDir import org.koitharu.kotatsu.local.data.CacheDir
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.parsers.util.await
import java.net.HttpURLConnection
private const val FALLBACK_SIZE = 9999 // largest icon private const val FALLBACK_SIZE = 9999 // largest icon
@ -34,6 +34,7 @@ class FaviconFetcher(
private val diskCache: Lazy<DiskCache?>, private val diskCache: Lazy<DiskCache?>,
private val mangaSource: MangaSource, private val mangaSource: MangaSource,
private val options: Options, private val options: Options,
private val mangaRepositoryFactory: MangaRepository.Factory,
) : Fetcher { ) : Fetcher {
private val diskCacheKey private val diskCacheKey
@ -44,7 +45,7 @@ class FaviconFetcher(
override suspend fun fetch(): FetchResult { override suspend fun fetch(): FetchResult {
getCached(options)?.let { return it } getCached(options)?.let { return it }
val repo = MangaRepository(mangaSource) as RemoteMangaRepository val repo = mangaRepositoryFactory.create(mangaSource) as RemoteMangaRepository
val favicons = repo.getFavicons() val favicons = repo.getFavicons()
val sizePx = maxOf( val sizePx = maxOf(
options.size.width.pxOrElse { FALLBACK_SIZE }, options.size.width.pxOrElse { FALLBACK_SIZE },
@ -136,6 +137,7 @@ class FaviconFetcher(
class Factory( class Factory(
context: Context, context: Context,
private val okHttpClient: OkHttpClient, private val okHttpClient: OkHttpClient,
private val mangaRepositoryFactory: MangaRepository.Factory,
) : Fetcher.Factory<Uri> { ) : Fetcher.Factory<Uri> {
private val diskCache = lazy { private val diskCache = lazy {
@ -148,7 +150,7 @@ class FaviconFetcher(
override fun create(data: Uri, options: Options, imageLoader: ImageLoader): Fetcher? { override fun create(data: Uri, options: Options, imageLoader: ImageLoader): Fetcher? {
return if (data.scheme == URI_SCHEME_FAVICON) { return if (data.scheme == URI_SCHEME_FAVICON) {
val mangaSource = MangaSource.valueOf(data.schemeSpecificPart) val mangaSource = MangaSource.valueOf(data.schemeSpecificPart)
FaviconFetcher(okHttpClient, diskCache, mangaSource, options) FaviconFetcher(okHttpClient, diskCache, mangaSource, options, mangaRepositoryFactory)
} else { } else {
null null
} }
@ -156,4 +158,4 @@ class FaviconFetcher(
} }
class FaviconMetadata(val source: MangaSource) : ImageSource.Metadata() class FaviconMetadata(val source: MangaSource) : ImageSource.Metadata()
} }

@ -10,6 +10,13 @@ import androidx.collection.arraySetOf
import androidx.core.content.edit import androidx.core.content.edit
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.material.color.DynamicColors import com.google.android.material.color.DynamicColors
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.File
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.model.ZoomMode import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.core.network.DoHProvider import org.koitharu.kotatsu.core.network.DoHProvider
@ -18,12 +25,9 @@ import org.koitharu.kotatsu.utils.ext.getEnumValue
import org.koitharu.kotatsu.utils.ext.observe import org.koitharu.kotatsu.utils.ext.observe
import org.koitharu.kotatsu.utils.ext.putEnumValue import org.koitharu.kotatsu.utils.ext.putEnumValue
import org.koitharu.kotatsu.utils.ext.toUriOrNull import org.koitharu.kotatsu.utils.ext.toUriOrNull
import java.io.File
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
class AppSettings(context: Context) { @Singleton
class AppSettings @Inject constructor(@ApplicationContext context: Context) {
private val prefs = PreferenceManager.getDefaultSharedPreferences(context) private val prefs = PreferenceManager.getDefaultSharedPreferences(context)

@ -1,53 +0,0 @@
package org.koitharu.kotatsu.core.ui
import android.text.Html
import coil.ComponentRegistry
import coil.ImageLoader
import coil.decode.SvgDecoder
import coil.disk.DiskCache
import coil.util.DebugLogger
import kotlinx.coroutines.Dispatchers
import okhttp3.OkHttpClient
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.parser.favicon.FaviconFetcher
import org.koitharu.kotatsu.local.data.CacheDir
import org.koitharu.kotatsu.local.data.CbzFetcher
import org.koitharu.kotatsu.utils.ext.isLowRamDevice
import org.koitharu.kotatsu.utils.image.CoilImageGetter
val uiModule
get() = module {
single {
val httpClientFactory = {
get<OkHttpClient>().newBuilder()
.cache(null)
.build()
}
val diskCacheFactory = {
val context = androidContext()
val rootDir = context.externalCacheDir ?: context.cacheDir
DiskCache.Builder()
.directory(rootDir.resolve(CacheDir.THUMBS.dir))
.build()
}
ImageLoader.Builder(androidContext())
.okHttpClient(httpClientFactory)
.interceptorDispatcher(Dispatchers.Default)
.fetcherDispatcher(Dispatchers.IO)
.decoderDispatcher(Dispatchers.Default)
.transformationDispatcher(Dispatchers.Default)
.diskCache(diskCacheFactory)
.logger(if (BuildConfig.DEBUG) DebugLogger() else null)
.allowRgb565(isLowRamDevice(androidContext()))
.components(
ComponentRegistry.Builder()
.add(SvgDecoder.Factory())
.add(CbzFetcher.Factory())
.add(FaviconFetcher.Factory(androidContext(), get()))
.build()
).build()
}
factory<Html.ImageGetter> { CoilImageGetter(androidContext(), get()) }
}

@ -1,13 +0,0 @@
package org.koitharu.kotatsu.details
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
import org.koitharu.kotatsu.details.ui.DetailsViewModel
val detailsModule
get() = module {
viewModel { intent ->
DetailsViewModel(intent.get(), get(), get(), get(), get(), get(), get(), get(), get(), get())
}
}

@ -11,8 +11,9 @@ import androidx.core.view.MenuProvider
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.activityViewModels
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import org.koin.androidx.viewmodel.ext.android.sharedViewModel import kotlin.math.roundToInt
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.list.ListSelectionController import org.koitharu.kotatsu.base.ui.list.ListSelectionController
@ -30,7 +31,6 @@ import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.utils.RecyclerViewScrollCallback import org.koitharu.kotatsu.utils.RecyclerViewScrollCallback
import org.koitharu.kotatsu.utils.ext.addMenuProvider import org.koitharu.kotatsu.utils.ext.addMenuProvider
import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf
import kotlin.math.roundToInt
class ChaptersFragment : class ChaptersFragment :
BaseFragment<FragmentChaptersBinding>(), BaseFragment<FragmentChaptersBinding>(),
@ -40,14 +40,14 @@ class ChaptersFragment :
SearchView.OnQueryTextListener, SearchView.OnQueryTextListener,
ListSelectionController.Callback { ListSelectionController.Callback {
private val viewModel by sharedViewModel<DetailsViewModel>() private val viewModel by activityViewModels<DetailsViewModel>()
private var chaptersAdapter: ChaptersAdapter? = null private var chaptersAdapter: ChaptersAdapter? = null
private var selectionController: ListSelectionController? = null private var selectionController: ListSelectionController? = null
override fun onInflateView( override fun onInflateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup? container: ViewGroup?,
) = FragmentChaptersBinding.inflate(inflater, container, false) ) = FragmentChaptersBinding.inflate(inflater, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -98,7 +98,7 @@ class ChaptersFragment :
manga = viewModel.manga.value ?: return, manga = viewModel.manga.value ?: return,
state = ReaderState(item.chapter.id, 0, 0), state = ReaderState(item.chapter.id, 0, 0),
), ),
scaleUpActivityOptionsOf(view).toBundle() scaleUpActivityOptionsOf(view).toBundle(),
) )
} }
@ -128,7 +128,7 @@ class ChaptersFragment :
Snackbar.make( Snackbar.make(
binding.recyclerViewChapters, binding.recyclerViewChapters,
R.string.chapters_will_removed_background, R.string.chapters_will_removed_background,
Snackbar.LENGTH_LONG Snackbar.LENGTH_LONG,
).show() ).show()
} }
} }
@ -286,4 +286,4 @@ class ChaptersFragment :
else -> false else -> false
} }
} }
} }

@ -8,7 +8,6 @@ import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView import android.widget.AdapterView
import android.widget.Spinner import android.widget.Spinner
import android.widget.Toast import android.widget.Toast
@ -16,7 +15,6 @@ import androidx.appcompat.view.ActionMode
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.commit import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -24,10 +22,9 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaIntent import org.koitharu.kotatsu.base.domain.MangaIntent
import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.base.ui.BaseActivity
@ -45,17 +42,25 @@ import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.scrobbling.ui.selector.ScrobblingSelectorBottomSheet import org.koitharu.kotatsu.scrobbling.ui.selector.ScrobblingSelectorBottomSheet
import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity
import org.koitharu.kotatsu.utils.ext.assistedViewModels
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.isReportable import org.koitharu.kotatsu.utils.ext.isReportable
import org.koitharu.kotatsu.utils.ext.report import org.koitharu.kotatsu.utils.ext.report
@AndroidEntryPoint
class DetailsActivity : class DetailsActivity :
BaseActivity<ActivityDetailsBinding>(), BaseActivity<ActivityDetailsBinding>(),
TabLayoutMediator.TabConfigurationStrategy, TabLayoutMediator.TabConfigurationStrategy,
AdapterView.OnItemSelectedListener { AdapterView.OnItemSelectedListener {
private val viewModel by viewModel<DetailsViewModel> { @Inject
parametersOf(MangaIntent(intent)) lateinit var viewModelFactory: DetailsViewModel.Factory
@Inject
lateinit var shortcutsUpdater: ShortcutsUpdater
private val viewModel by assistedViewModels<DetailsViewModel> {
viewModelFactory.create(MangaIntent(intent))
} }
private val downloadReceiver = object : BroadcastReceiver() { private val downloadReceiver = object : BroadcastReceiver() {
@ -103,8 +108,9 @@ class DetailsActivity :
private fun onMangaRemoved(manga: Manga) { private fun onMangaRemoved(manga: Manga) {
Toast.makeText( Toast.makeText(
this, getString(R.string._s_deleted_from_local_storage, manga.title), this,
Toast.LENGTH_SHORT getString(R.string._s_deleted_from_local_storage, manga.title),
Toast.LENGTH_SHORT,
).show() ).show()
finishAfterTransition() finishAfterTransition()
} }
@ -130,7 +136,7 @@ class DetailsActivity :
onActionClick = { onActionClick = {
e.report("DetailsActivity::onError") e.report("DetailsActivity::onError")
dismiss() dismiss()
} },
) )
} }
else -> { else -> {
@ -141,11 +147,11 @@ class DetailsActivity :
override fun onWindowInsetsChanged(insets: Insets) { override fun onWindowInsetsChanged(insets: Insets) {
binding.snackbar.updatePadding( binding.snackbar.updatePadding(
bottom = insets.bottom bottom = insets.bottom,
) )
binding.root.updatePadding( binding.root.updatePadding(
left = insets.left, left = insets.left,
right = insets.right right = insets.right,
) )
} }
@ -222,7 +228,7 @@ class DetailsActivity :
R.id.action_shortcut -> { R.id.action_shortcut -> {
viewModel.manga.value?.let { viewModel.manga.value?.let {
lifecycleScope.launch { lifecycleScope.launch {
if (!get<ShortcutsUpdater>().requestPinShortcut(it)) { if (!shortcutsUpdater.requestPinShortcut(it)) {
binding.snackbar.show(getString(R.string.operation_not_supported)) binding.snackbar.show(getString(R.string.operation_not_supported))
} }
} }
@ -272,8 +278,8 @@ class DetailsActivity :
ReaderActivity.newIntent( ReaderActivity.newIntent(
context = this@DetailsActivity, context = this@DetailsActivity,
manga = remoteManga, manga = remoteManga,
state = ReaderState(chapterId, 0, 0) state = ReaderState(chapterId, 0, 0),
) ),
) )
} }
setNeutralButton(R.string.download) { _, _ -> setNeutralButton(R.string.download) { _, _ ->
@ -347,8 +353,8 @@ class DetailsActivity :
dialogBuilder.setMessage( dialogBuilder.setMessage(
getString( getString(
R.string.large_manga_save_confirm, R.string.large_manga_save_confirm,
resources.getQuantityString(R.plurals.chapters, chaptersCount, chaptersCount) resources.getQuantityString(R.plurals.chapters, chaptersCount, chaptersCount),
) ),
).setPositiveButton(R.string.save) { _, _ -> ).setPositiveButton(R.string.save) { _, _ ->
DownloadService.start(this, manga) DownloadService.start(this, manga)
} }

@ -12,13 +12,14 @@ import androidx.core.view.MenuProvider
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.activityViewModels
import coil.ImageLoader import coil.ImageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import coil.util.CoilUtils import coil.util.CoilUtils
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
@ -45,6 +46,7 @@ import org.koitharu.kotatsu.utils.FileSize
import org.koitharu.kotatsu.utils.ShareHelper import org.koitharu.kotatsu.utils.ShareHelper
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
@AndroidEntryPoint
class DetailsFragment : class DetailsFragment :
BaseFragment<FragmentDetailsBinding>(), BaseFragment<FragmentDetailsBinding>(),
View.OnClickListener, View.OnClickListener,
@ -52,8 +54,10 @@ class DetailsFragment :
ChipsView.OnChipClickListener, ChipsView.OnChipClickListener,
OnListItemClickListener<Bookmark> { OnListItemClickListener<Bookmark> {
private val viewModel by sharedViewModel<DetailsViewModel>() @Inject
private val coil by inject<ImageLoader>(mode = LazyThreadSafetyMode.NONE) lateinit var coil: ImageLoader
private val viewModel by activityViewModels<DetailsViewModel>()
override fun onInflateView( override fun onInflateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -263,7 +267,7 @@ class DetailsFragment :
context = context ?: return, context = context ?: return,
manga = manga, manga = manga,
branch = viewModel.selectedBranchValue, branch = viewModel.selectedBranchValue,
) ),
) )
} }
} }
@ -273,13 +277,13 @@ class DetailsFragment :
context = v.context, context = v.context,
source = manga.source, source = manga.source,
query = manga.author ?: return, query = manga.author ?: return,
) ),
) )
} }
R.id.imageView_cover -> { R.id.imageView_cover -> {
startActivity( startActivity(
ImageActivity.newIntent(v.context, manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl }), ImageActivity.newIntent(v.context, manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl }),
scaleUpActivityOptionsOf(v).toBundle() scaleUpActivityOptionsOf(v).toBundle(),
) )
} }
} }
@ -305,8 +309,8 @@ class DetailsFragment :
c.chapter.branch == branch c.chapter.branch == branch
}?.let { c -> }?.let { c ->
ReaderState(c.chapter.id, 0, 0) ReaderState(c.chapter.id, 0, 0)
} },
) ),
) )
true true
} }
@ -329,7 +333,7 @@ class DetailsFragment :
binding.root.updatePadding( binding.root.updatePadding(
left = insets.left, left = insets.left,
right = insets.right, right = insets.right,
bottom = insets.bottom bottom = insets.bottom,
) )
} }
@ -343,7 +347,7 @@ class DetailsFragment :
isCheckable = false, isCheckable = false,
isChecked = false, isChecked = false,
) )
} },
) )
} }
@ -386,4 +390,4 @@ class DetailsFragment :
else -> false else -> false
} }
} }
} }

@ -6,6 +6,10 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.asFlow import androidx.lifecycle.asFlow
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import java.io.IOException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
@ -17,6 +21,7 @@ import org.koitharu.kotatsu.base.domain.MangaIntent
import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.details.domain.BranchComparator import org.koitharu.kotatsu.details.domain.BranchComparator
@ -33,10 +38,9 @@ import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import java.io.IOException
class DetailsViewModel( class DetailsViewModel @AssistedInject constructor(
intent: MangaIntent, @Assisted intent: MangaIntent,
private val historyRepository: HistoryRepository, private val historyRepository: HistoryRepository,
favouritesRepository: FavouritesRepository, favouritesRepository: FavouritesRepository,
private val localMangaRepository: LocalMangaRepository, private val localMangaRepository: LocalMangaRepository,
@ -44,16 +48,20 @@ class DetailsViewModel(
mangaDataRepository: MangaDataRepository, mangaDataRepository: MangaDataRepository,
private val bookmarksRepository: BookmarksRepository, private val bookmarksRepository: BookmarksRepository,
private val settings: AppSettings, private val settings: AppSettings,
private val scrobbler: Scrobbler, scrobblers: Set<@JvmSuppressWildcards Scrobbler>,
private val imageGetter: Html.ImageGetter, private val imageGetter: Html.ImageGetter,
mangaRepositoryFactory: MangaRepository.Factory,
) : BaseViewModel() { ) : BaseViewModel() {
private val scrobbler = scrobblers.first() // TODO support multiple scrobblers
private val delegate = MangaDetailsDelegate( private val delegate = MangaDetailsDelegate(
intent = intent, intent = intent,
settings = settings, settings = settings,
mangaDataRepository = mangaDataRepository, mangaDataRepository = mangaDataRepository,
historyRepository = historyRepository, historyRepository = historyRepository,
localMangaRepository = localMangaRepository, localMangaRepository = localMangaRepository,
mangaRepositoryFactory = mangaRepositoryFactory,
) )
private var loadingJob: Job private var loadingJob: Job
@ -110,7 +118,7 @@ class DetailsViewModel(
val selectedBranchIndex = combine( val selectedBranchIndex = combine(
branches.asFlow(), branches.asFlow(),
delegate.selectedBranch delegate.selectedBranch,
) { branches, selected -> ) { branches, selected ->
branches.indexOf(selected) branches.indexOf(selected)
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, -1) }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, -1)
@ -225,7 +233,7 @@ class DetailsViewModel(
fun unregisterScrobbling() { fun unregisterScrobbling() {
launchJob(Dispatchers.Default) { launchJob(Dispatchers.Default) {
scrobbler.unregisterScrobbling( scrobbler.unregisterScrobbling(
mangaId = delegate.mangaId mangaId = delegate.mangaId,
) )
} }
} }
@ -242,4 +250,10 @@ class DetailsViewModel(
it.chapter.name.contains(query, ignoreCase = true) it.chapter.name.contains(query, ignoreCase = true)
} }
} }
}
@AssistedFactory
interface Factory {
fun create(intent: MangaIntent): DetailsViewModel
}
}

@ -27,6 +27,7 @@ class MangaDetailsDelegate(
private val mangaDataRepository: MangaDataRepository, private val mangaDataRepository: MangaDataRepository,
private val historyRepository: HistoryRepository, private val historyRepository: HistoryRepository,
private val localMangaRepository: LocalMangaRepository, private val localMangaRepository: LocalMangaRepository,
private val mangaRepositoryFactory: MangaRepository.Factory,
) { ) {
private val mangaData = MutableStateFlow(intent.manga) private val mangaData = MutableStateFlow(intent.manga)
@ -42,7 +43,7 @@ class MangaDetailsDelegate(
suspend fun doLoad() { suspend fun doLoad() {
var manga = mangaDataRepository.resolveIntent(intent) ?: throw NotFoundException("Cannot find manga", "") var manga = mangaDataRepository.resolveIntent(intent) ?: throw NotFoundException("Cannot find manga", "")
mangaData.value = manga mangaData.value = manga
manga = MangaRepository(manga.source).getDetails(manga) manga = mangaRepositoryFactory.create(manga.source).getDetails(manga)
// find default branch // find default branch
val hist = historyRepository.getOne(manga) val hist = historyRepository.getOne(manga)
selectedBranch.value = if (hist != null) { selectedBranch.value = if (hist != null) {
@ -55,7 +56,7 @@ class MangaDetailsDelegate(
relatedManga.value = runCatching { relatedManga.value = runCatching {
if (manga.source == MangaSource.LOCAL) { if (manga.source == MangaSource.LOCAL) {
val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatching null val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatching null
MangaRepository(m.source).getDetails(m) mangaRepositoryFactory.create(m.source).getDetails(m)
} else { } else {
localMangaRepository.findSavedManga(manga) localMangaRepository.findSavedManga(manga)
} }
@ -181,4 +182,4 @@ class MangaDetailsDelegate(
} }
return groups.maxByOrNull { it.value.size }?.key return groups.maxByOrNull { it.value.size }?.key
} }
} }

@ -13,10 +13,11 @@ import android.widget.Toast
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.fragment.app.activityViewModels
import coil.ImageLoader import coil.ImageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import org.koin.android.ext.android.inject import dagger.hilt.android.AndroidEntryPoint
import org.koin.androidx.viewmodel.ext.android.sharedViewModel import javax.inject.Inject
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseBottomSheet import org.koitharu.kotatsu.base.ui.BaseBottomSheet
import org.koitharu.kotatsu.databinding.SheetScrobblingBinding import org.koitharu.kotatsu.databinding.SheetScrobblingBinding
@ -30,6 +31,7 @@ import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf
@AndroidEntryPoint
class ScrobblingInfoBottomSheet : class ScrobblingInfoBottomSheet :
BaseBottomSheet<SheetScrobblingBinding>(), BaseBottomSheet<SheetScrobblingBinding>(),
AdapterView.OnItemSelectedListener, AdapterView.OnItemSelectedListener,
@ -37,8 +39,10 @@ class ScrobblingInfoBottomSheet :
View.OnClickListener, View.OnClickListener,
PopupMenu.OnMenuItemClickListener { PopupMenu.OnMenuItemClickListener {
private val viewModel by sharedViewModel<DetailsViewModel>() private val viewModel by activityViewModels<DetailsViewModel>()
private val coil by inject<ImageLoader>(mode = LazyThreadSafetyMode.NONE)
@Inject
lateinit var coil: ImageLoader
private var menu: PopupMenu? = null private var menu: PopupMenu? = null
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetScrobblingBinding { override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetScrobblingBinding {
@ -131,7 +135,7 @@ class ScrobblingInfoBottomSheet :
val url = viewModel.scrobblingInfo.value?.externalUrl ?: return false val url = viewModel.scrobblingInfo.value?.externalUrl ?: return false
val intent = Intent(Intent.ACTION_VIEW, url.toUri()) val intent = Intent(Intent.ACTION_VIEW, url.toUri())
startActivity( startActivity(
Intent.createChooser(intent, getString(R.string.open_in_browser)) Intent.createChooser(intent, getString(R.string.open_in_browser)),
) )
} }
R.id.action_unregister -> { R.id.action_unregister -> {
@ -146,4 +150,4 @@ class ScrobblingInfoBottomSheet :
} }
return true return true
} }
} }

@ -5,6 +5,11 @@ import android.net.ConnectivityManager
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import coil.ImageLoader import coil.ImageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.File
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.Semaphore
@ -26,30 +31,30 @@ import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.referer import org.koitharu.kotatsu.utils.ext.referer
import org.koitharu.kotatsu.utils.ext.waitForNetwork import org.koitharu.kotatsu.utils.ext.waitForNetwork
import org.koitharu.kotatsu.utils.progress.ProgressJob import org.koitharu.kotatsu.utils.progress.ProgressJob
import java.io.File
private const val MAX_DOWNLOAD_ATTEMPTS = 3 private const val MAX_DOWNLOAD_ATTEMPTS = 3
private const val DOWNLOAD_ERROR_DELAY = 500L private const val DOWNLOAD_ERROR_DELAY = 500L
private const val SLOWDOWN_DELAY = 200L private const val SLOWDOWN_DELAY = 200L
class DownloadManager( class DownloadManager @AssistedInject constructor(
private val coroutineScope: CoroutineScope, @Assisted private val coroutineScope: CoroutineScope,
private val context: Context, @ApplicationContext private val context: Context,
private val imageLoader: ImageLoader, private val imageLoader: ImageLoader,
private val okHttp: OkHttpClient, private val okHttp: OkHttpClient,
private val cache: PagesCache, private val cache: PagesCache,
private val localMangaRepository: LocalMangaRepository, private val localMangaRepository: LocalMangaRepository,
private val settings: AppSettings, private val settings: AppSettings,
private val mangaRepositoryFactory: MangaRepository.Factory,
) { ) {
private val connectivityManager = context.getSystemService( private val connectivityManager = context.getSystemService(
Context.CONNECTIVITY_SERVICE Context.CONNECTIVITY_SERVICE,
) as ConnectivityManager ) as ConnectivityManager
private val coverWidth = context.resources.getDimensionPixelSize( private val coverWidth = context.resources.getDimensionPixelSize(
androidx.core.R.dimen.compat_notification_large_icon_max_width androidx.core.R.dimen.compat_notification_large_icon_max_width,
) )
private val coverHeight = context.resources.getDimensionPixelSize( private val coverHeight = context.resources.getDimensionPixelSize(
androidx.core.R.dimen.compat_notification_large_icon_max_height androidx.core.R.dimen.compat_notification_large_icon_max_height,
) )
private val semaphore = Semaphore(settings.downloadsParallelism) private val semaphore = Semaphore(settings.downloadsParallelism)
@ -59,7 +64,7 @@ class DownloadManager(
startId: Int, startId: Int,
): ProgressJob<DownloadState> { ): ProgressJob<DownloadState> {
val stateFlow = MutableStateFlow<DownloadState>( val stateFlow = MutableStateFlow<DownloadState>(
DownloadState.Queued(startId = startId, manga = manga, cover = null) DownloadState.Queued(startId = startId, manga = manga, cover = null),
) )
val job = downloadMangaImpl(manga, chaptersIds?.takeUnless { it.isEmpty() }, stateFlow, startId) val job = downloadMangaImpl(manga, chaptersIds?.takeUnless { it.isEmpty() }, stateFlow, startId)
return ProgressJob(job, stateFlow) return ProgressJob(job, stateFlow)
@ -71,7 +76,8 @@ class DownloadManager(
outState: MutableStateFlow<DownloadState>, outState: MutableStateFlow<DownloadState>,
startId: Int, startId: Int,
): Job = coroutineScope.launch(Dispatchers.Default + errorStateHandler(outState)) { ): Job = coroutineScope.launch(Dispatchers.Default + errorStateHandler(outState)) {
@Suppress("NAME_SHADOWING") var manga = manga @Suppress("NAME_SHADOWING")
var manga = manga
val chaptersIdsSet = chaptersIds?.toMutableSet() val chaptersIdsSet = chaptersIds?.toMutableSet()
val cover = loadCover(manga) val cover = loadCover(manga)
outState.value = DownloadState.Queued(startId, manga, cover) outState.value = DownloadState.Queued(startId, manga, cover)
@ -87,7 +93,7 @@ class DownloadManager(
if (manga.source == MangaSource.LOCAL) { if (manga.source == MangaSource.LOCAL) {
manga = localMangaRepository.getRemoteManga(manga) ?: error("Cannot obtain remote manga instance") manga = localMangaRepository.getRemoteManga(manga) ?: error("Cannot obtain remote manga instance")
} }
val repo = MangaRepository(manga.source) val repo = mangaRepositoryFactory.create(manga.source)
outState.value = DownloadState.Preparing(startId, manga, cover) outState.value = DownloadState.Preparing(startId, manga, cover)
val data = if (manga.chapters.isNullOrEmpty()) repo.getDetails(manga) else manga val data = if (manga.chapters.isNullOrEmpty()) repo.getDetails(manga) else manga
output = CbzMangaOutput.get(destination, data) output = CbzMangaOutput.get(destination, data)
@ -100,7 +106,7 @@ class DownloadManager(
data.chapters data.chapters
} else { } else {
data.chapters?.filter { x -> chaptersIdsSet.remove(x.id) } data.chapters?.filter { x -> chaptersIdsSet.remove(x.id) }
} },
) { "Chapters list must not be null" } ) { "Chapters list must not be null" }
check(chapters.isNotEmpty()) { "Chapters list must not be empty" } check(chapters.isNotEmpty()) { "Chapters list must not be empty" }
check(chaptersIdsSet.isNullOrEmpty()) { check(chaptersIdsSet.isNullOrEmpty()) {
@ -134,7 +140,9 @@ class DownloadManager(
} }
outState.value = DownloadState.Progress( outState.value = DownloadState.Progress(
startId, data, cover, startId,
data,
cover,
totalChapters = chapters.size, totalChapters = chapters.size,
currentChapter = chapterIndex, currentChapter = chapterIndex,
totalPages = pages.size, totalPages = pages.size,
@ -203,27 +211,13 @@ class DownloadManager(
.data(manga.coverUrl) .data(manga.coverUrl)
.referer(manga.publicUrl) .referer(manga.publicUrl)
.size(coverWidth, coverHeight) .size(coverWidth, coverHeight)
.build() .build(),
).drawable ).drawable
}.getOrNull() }.getOrNull()
class Factory( @AssistedFactory
private val context: Context, interface Factory {
private val imageLoader: ImageLoader,
private val okHttp: OkHttpClient,
private val cache: PagesCache,
private val localMangaRepository: LocalMangaRepository,
private val settings: AppSettings,
) {
fun create(coroutineScope: CoroutineScope) = DownloadManager( fun create(coroutineScope: CoroutineScope): DownloadManager
coroutineScope = coroutineScope,
context = context,
imageLoader = imageLoader,
okHttp = okHttp,
cache = cache,
localMangaRepository = localMangaRepository,
settings = settings,
)
} }
} }

@ -7,23 +7,29 @@ import androidx.core.graphics.Insets
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import coil.ImageLoader
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import org.koin.android.ext.android.get
import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivityDownloadsBinding import org.koitharu.kotatsu.databinding.ActivityDownloadsBinding
import org.koitharu.kotatsu.download.ui.service.DownloadService import org.koitharu.kotatsu.download.ui.service.DownloadService
import org.koitharu.kotatsu.utils.bindServiceWithLifecycle import org.koitharu.kotatsu.utils.bindServiceWithLifecycle
@AndroidEntryPoint
class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>() { class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>() {
@Inject
lateinit var coil: ImageLoader
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(ActivityDownloadsBinding.inflate(layoutInflater)) setContentView(ActivityDownloadsBinding.inflate(layoutInflater))
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
val adapter = DownloadsAdapter(lifecycleScope, get()) val adapter = DownloadsAdapter(lifecycleScope, coil)
binding.recyclerView.setHasFixedSize(true) binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
bindServiceWithLifecycle( bindServiceWithLifecycle(
@ -42,11 +48,11 @@ class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>() {
binding.recyclerView.updatePadding( binding.recyclerView.updatePadding(
left = insets.left, left = insets.left,
right = insets.right, right = insets.right,
bottom = insets.bottom bottom = insets.bottom,
) )
binding.toolbar.updatePadding( binding.toolbar.updatePadding(
left = insets.left, left = insets.left,
right = insets.right right = insets.right,
) )
} }
@ -54,4 +60,4 @@ class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>() {
fun newIntent(context: Context) = Intent(context, DownloadsActivity::class.java) fun newIntent(context: Context) = Intent(context, DownloadsActivity::class.java)
} }
} }

@ -11,32 +11,34 @@ import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlin.collections.set
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import org.koin.android.ext.android.get
import org.koin.core.context.GlobalContext
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseService import org.koitharu.kotatsu.base.ui.BaseService
import org.koitharu.kotatsu.base.ui.dialog.CheckBoxAlertDialog
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.download.domain.DownloadManager import org.koitharu.kotatsu.download.domain.DownloadManager
import org.koitharu.kotatsu.download.domain.DownloadState import org.koitharu.kotatsu.download.domain.DownloadState
import org.koitharu.kotatsu.download.domain.WakeLockNode import org.koitharu.kotatsu.download.domain.WakeLockNode
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.ext.connectivityManager
import org.koitharu.kotatsu.utils.ext.throttle import org.koitharu.kotatsu.utils.ext.throttle
import org.koitharu.kotatsu.utils.progress.ProgressJob import org.koitharu.kotatsu.utils.progress.ProgressJob
import org.koitharu.kotatsu.utils.progress.TimeLeftEstimator import org.koitharu.kotatsu.utils.progress.TimeLeftEstimator
import java.util.concurrent.TimeUnit
@AndroidEntryPoint
class DownloadService : BaseService() { class DownloadService : BaseService() {
private lateinit var downloadManager: DownloadManager private lateinit var downloadManager: DownloadManager
private lateinit var notificationSwitcher: ForegroundNotificationSwitcher private lateinit var notificationSwitcher: ForegroundNotificationSwitcher
@Inject
lateinit var downloadManagerFactory: DownloadManager.Factory
private val jobs = LinkedHashMap<Int, ProgressJob<DownloadState>>() private val jobs = LinkedHashMap<Int, ProgressJob<DownloadState>>()
private val jobCount = MutableStateFlow(0) private val jobCount = MutableStateFlow(0)
private val controlReceiver = ControlReceiver() private val controlReceiver = ControlReceiver()
@ -48,7 +50,7 @@ class DownloadService : BaseService() {
notificationSwitcher = ForegroundNotificationSwitcher(this) notificationSwitcher = ForegroundNotificationSwitcher(this)
val wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager) val wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager)
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "kotatsu:downloading") .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "kotatsu:downloading")
downloadManager = get<DownloadManager.Factory>().create( downloadManager = downloadManagerFactory.create(
coroutineScope = lifecycleScope + WakeLockNode(wakeLock, TimeUnit.HOURS.toMillis(1)), coroutineScope = lifecycleScope + WakeLockNode(wakeLock, TimeUnit.HOURS.toMillis(1)),
) )
DownloadNotification.createChannel(this) DownloadNotification.createChannel(this)
@ -122,7 +124,7 @@ class DownloadService : BaseService() {
(job.progressValue as? DownloadState.Done)?.let { (job.progressValue as? DownloadState.Done)?.let {
sendBroadcast( sendBroadcast(
Intent(ACTION_DOWNLOAD_COMPLETE) Intent(ACTION_DOWNLOAD_COMPLETE)
.putExtra(EXTRA_MANGA, ParcelableManga(it.localManga, withChapters = false)) .putExtra(EXTRA_MANGA, ParcelableManga(it.localManga, withChapters = false)),
) )
} }
notificationSwitcher.detach( notificationSwitcher.detach(
@ -131,7 +133,7 @@ class DownloadService : BaseService() {
null null
} else { } else {
notification.create(job.progressValue, -1L) notification.create(job.progressValue, -1L)
} },
) )
stopSelf(startId) stopSelf(startId)
} }
@ -182,27 +184,23 @@ class DownloadService : BaseService() {
if (chaptersIds?.isEmpty() == true) { if (chaptersIds?.isEmpty() == true) {
return return
} }
confirmDataTransfer(context) { val intent = Intent(context, DownloadService::class.java)
val intent = Intent(context, DownloadService::class.java) intent.putExtra(EXTRA_MANGA, ParcelableManga(manga, withChapters = false))
intent.putExtra(EXTRA_MANGA, ParcelableManga(manga, withChapters = false)) if (chaptersIds != null) {
if (chaptersIds != null) { intent.putExtra(EXTRA_CHAPTERS_IDS, chaptersIds.toLongArray())
intent.putExtra(EXTRA_CHAPTERS_IDS, chaptersIds.toLongArray())
}
ContextCompat.startForegroundService(context, intent)
Toast.makeText(context, R.string.manga_downloading_, Toast.LENGTH_SHORT).show()
} }
ContextCompat.startForegroundService(context, intent)
Toast.makeText(context, R.string.manga_downloading_, Toast.LENGTH_SHORT).show()
} }
fun start(context: Context, manga: Collection<Manga>) { fun start(context: Context, manga: Collection<Manga>) {
if (manga.isEmpty()) { if (manga.isEmpty()) {
return return
} }
confirmDataTransfer(context) { for (item in manga) {
for (item in manga) { val intent = Intent(context, DownloadService::class.java)
val intent = Intent(context, DownloadService::class.java) intent.putExtra(EXTRA_MANGA, ParcelableManga(item, withChapters = false))
intent.putExtra(EXTRA_MANGA, ParcelableManga(item, withChapters = false)) ContextCompat.startForegroundService(context, intent)
ContextCompat.startForegroundService(context, intent)
}
} }
} }
@ -225,24 +223,5 @@ class DownloadService : BaseService() {
} }
return null return null
} }
private fun confirmDataTransfer(context: Context, callback: () -> Unit) {
val settings = GlobalContext.get().get<AppSettings>()
if (context.connectivityManager.isActiveNetworkMetered && settings.isTrafficWarningEnabled) {
CheckBoxAlertDialog.Builder(context)
.setTitle(R.string.warning)
.setMessage(R.string.network_consumption_warning)
.setCheckBoxText(R.string.dont_ask_again)
.setCheckBoxChecked(false)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string._continue) { _, doNotAsk ->
settings.isTrafficWarningEnabled = !doNotAsk
callback()
}.create()
.show()
} else {
callback()
}
}
} }
} }

@ -1,14 +0,0 @@
package org.koitharu.kotatsu.explore
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
import org.koitharu.kotatsu.explore.domain.ExploreRepository
import org.koitharu.kotatsu.explore.ui.ExploreViewModel
val exploreModule
get() = module {
factory { ExploreRepository(get(), get()) }
viewModel { ExploreViewModel(get(), get()) }
}

@ -1,14 +1,16 @@
package org.koitharu.kotatsu.explore.domain package org.koitharu.kotatsu.explore.domain
import javax.inject.Inject
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
class ExploreRepository( class ExploreRepository @Inject constructor(
private val settings: AppSettings, private val settings: AppSettings,
private val historyRepository: HistoryRepository, private val historyRepository: HistoryRepository,
private val mangaRepositoryFactory: MangaRepository.Factory,
) { ) {
suspend fun findRandomManga(tagsLimit: Int): Manga { suspend fun findRandomManga(tagsLimit: Int): Manga {
@ -20,7 +22,7 @@ class ExploreRepository(
val source = checkNotNull(tag?.source ?: settings.getMangaSources(includeHidden = false).randomOrNull()) { val source = checkNotNull(tag?.source ?: settings.getMangaSources(includeHidden = false).randomOrNull()) {
"No sources found" "No sources found"
} }
val repo = MangaRepository(source) val repo = mangaRepositoryFactory.create(source)
val list = repo.getList( val list = repo.getList(
offset = 0, offset = 0,
sortOrder = if (SortOrder.UPDATED in repo.sortOrders) SortOrder.UPDATED else null, sortOrder = if (SortOrder.UPDATED in repo.sortOrders) SortOrder.UPDATED else null,
@ -37,4 +39,4 @@ class ExploreRepository(
} }
return list.random() return list.random()
} }
} }

@ -6,10 +6,12 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import org.koin.android.ext.android.get import dagger.hilt.android.AndroidEntryPoint
import org.koin.androidx.viewmodel.ext.android.viewModel import javax.inject.Inject
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
@ -30,12 +32,17 @@ import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity import org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
class ExploreFragment : BaseFragment<FragmentExploreBinding>(), @AndroidEntryPoint
class ExploreFragment :
BaseFragment<FragmentExploreBinding>(),
RecyclerViewOwner, RecyclerViewOwner,
ExploreListEventListener, ExploreListEventListener,
OnListItemClickListener<ExploreItem.Source> { OnListItemClickListener<ExploreItem.Source> {
private val viewModel by viewModel<ExploreViewModel>() @Inject
lateinit var coil: ImageLoader
private val viewModel by viewModels<ExploreViewModel>()
private var exploreAdapter: ExploreAdapter? = null private var exploreAdapter: ExploreAdapter? = null
private var paddingHorizontal = 0 private var paddingHorizontal = 0
@ -48,7 +55,7 @@ class ExploreFragment : BaseFragment<FragmentExploreBinding>(),
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
exploreAdapter = ExploreAdapter(get(), viewLifecycleOwner, this, this) exploreAdapter = ExploreAdapter(coil, viewLifecycleOwner, this, this)
with(binding.recyclerView) { with(binding.recyclerView) {
adapter = exploreAdapter adapter = exploreAdapter
setHasFixedSize(true) setHasFixedSize(true)
@ -112,7 +119,7 @@ class ExploreFragment : BaseFragment<FragmentExploreBinding>(),
val snackbar = Snackbar.make( val snackbar = Snackbar.make(
binding.recyclerView, binding.recyclerView,
e.getDisplayMessage(resources), e.getDisplayMessage(resources),
Snackbar.LENGTH_SHORT Snackbar.LENGTH_SHORT,
) )
snackbar.anchorView = (activity as? BottomNavOwner)?.bottomNav snackbar.anchorView = (activity as? BottomNavOwner)?.bottomNav
snackbar.show() snackbar.show()

@ -3,6 +3,8 @@ package org.koitharu.kotatsu.explore.ui
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.asFlow import androidx.lifecycle.asFlow
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
@ -15,7 +17,8 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
class ExploreViewModel( @HiltViewModel
class ExploreViewModel @Inject constructor(
private val settings: AppSettings, private val settings: AppSettings,
private val exploreRepository: ExploreRepository, private val exploreRepository: ExploreRepository,
) : BaseViewModel() { ) : BaseViewModel() {
@ -66,4 +69,4 @@ class ExploreViewModel(
} }
return result return result
} }
} }

@ -1,24 +0,0 @@
package org.koitharu.kotatsu.favourites
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.favourites.ui.categories.FavouritesCategoriesViewModel
import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditViewModel
import org.koitharu.kotatsu.favourites.ui.categories.select.MangaCategoriesViewModel
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListViewModel
val favouritesModule
get() = module {
single { FavouritesRepository(get(), get()) }
viewModel { categoryId ->
FavouritesListViewModel(categoryId.get(), get(), get(), get(), get())
}
viewModel { FavouritesCategoriesViewModel(get(), get()) }
viewModel { manga ->
MangaCategoriesViewModel(manga.get(), get())
}
viewModel { params -> FavouritesCategoryEditViewModel(params[0], get(), get()) }
}

@ -1,6 +1,8 @@
package org.koitharu.kotatsu.favourites.domain package org.koitharu.kotatsu.favourites.domain
import androidx.room.withTransaction import androidx.room.withTransaction
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import org.koitharu.kotatsu.base.domain.ReversibleHandle import org.koitharu.kotatsu.base.domain.ReversibleHandle
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
@ -14,7 +16,8 @@ import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels
import org.koitharu.kotatsu.utils.ext.mapItems import org.koitharu.kotatsu.utils.ext.mapItems
class FavouritesRepository( @Singleton
class FavouritesRepository @Inject constructor(
private val db: MangaDatabase, private val db: MangaDatabase,
private val channels: TrackerNotificationChannels, private val channels: TrackerNotificationChannels,
) { ) {

@ -6,6 +6,7 @@ import android.os.Bundle
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.commit import androidx.fragment.app.commit
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
@ -13,6 +14,7 @@ import org.koitharu.kotatsu.databinding.ActivityContainerBinding
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.NO_ID import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.NO_ID
@AndroidEntryPoint
class FavouritesActivity : BaseActivity<ActivityContainerBinding>() { class FavouritesActivity : BaseActivity<ActivityContainerBinding>() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -50,4 +52,4 @@ class FavouritesActivity : BaseActivity<ActivityContainerBinding>() {
.putExtra(EXTRA_CATEGORY_ID, category.id) .putExtra(EXTRA_CATEGORY_ID, category.id)
.putExtra(EXTRA_TITLE, category.title) .putExtra(EXTRA_TITLE, category.title)
} }
} }

@ -9,15 +9,17 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.activity.viewModels
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import org.koin.android.ext.android.get import dagger.hilt.android.AndroidEntryPoint
import org.koin.androidx.viewmodel.ext.android.viewModel import javax.inject.Inject
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.base.ui.list.ListSelectionController import org.koitharu.kotatsu.base.ui.list.ListSelectionController
@ -33,13 +35,17 @@ import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.measureHeight import org.koitharu.kotatsu.utils.ext.measureHeight
import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf
@AndroidEntryPoint
class FavouriteCategoriesActivity : class FavouriteCategoriesActivity :
BaseActivity<ActivityCategoriesBinding>(), BaseActivity<ActivityCategoriesBinding>(),
FavouriteCategoriesListListener, FavouriteCategoriesListListener,
View.OnClickListener, View.OnClickListener,
ListStateHolderListener { ListStateHolderListener {
private val viewModel by viewModel<FavouritesCategoriesViewModel>() @Inject
lateinit var coil: ImageLoader
private val viewModel by viewModels<FavouritesCategoriesViewModel>()
private lateinit var adapter: CategoriesAdapter private lateinit var adapter: CategoriesAdapter
private lateinit var selectionController: ListSelectionController private lateinit var selectionController: ListSelectionController
@ -49,7 +55,7 @@ class FavouriteCategoriesActivity :
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(ActivityCategoriesBinding.inflate(layoutInflater)) setContentView(ActivityCategoriesBinding.inflate(layoutInflater))
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
adapter = CategoriesAdapter(get(), this, this, this) adapter = CategoriesAdapter(coil, this, this, this)
selectionController = ListSelectionController( selectionController = ListSelectionController(
activity = this, activity = this,
decoration = CategoriesSelectionDecoration(this), decoration = CategoriesSelectionDecoration(this),
@ -169,7 +175,8 @@ class FavouriteCategoriesActivity :
} }
private inner class ReorderHelperCallback : ItemTouchHelper.SimpleCallback( private inner class ReorderHelperCallback : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.DOWN or ItemTouchHelper.UP, 0 ItemTouchHelper.DOWN or ItemTouchHelper.UP,
0,
) { ) {
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) = Unit override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) = Unit

@ -2,6 +2,9 @@ package org.koitharu.kotatsu.favourites.ui.categories
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import java.util.*
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -16,9 +19,9 @@ import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.mapItems import org.koitharu.kotatsu.utils.ext.mapItems
import org.koitharu.kotatsu.utils.ext.requireValue import org.koitharu.kotatsu.utils.ext.requireValue
import java.util.*
class FavouritesCategoriesViewModel( @HiltViewModel
class FavouritesCategoriesViewModel @Inject constructor(
private val repository: FavouritesRepository, private val repository: FavouritesRepository,
private val settings: AppSettings, private val settings: AppSettings,
) : BaseViewModel() { ) : BaseViewModel() {
@ -56,7 +59,7 @@ class FavouritesCategoriesViewModel(
textPrimary = R.string.text_empty_holder_primary, textPrimary = R.string.text_empty_holder_primary,
textSecondary = R.string.empty_favourite_categories, textSecondary = R.string.empty_favourite_categories,
actionStringRes = 0, actionStringRes = 0,
) ),
) )
} }
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))

@ -14,8 +14,8 @@ import androidx.core.graphics.Insets
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import org.koin.androidx.viewmodel.ext.android.viewModel import dagger.hilt.android.AndroidEntryPoint
import org.koin.core.parameter.parametersOf import javax.inject.Inject
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
@ -23,13 +23,21 @@ import org.koitharu.kotatsu.core.ui.titleRes
import org.koitharu.kotatsu.databinding.ActivityCategoryEditBinding import org.koitharu.kotatsu.databinding.ActivityCategoryEditBinding
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.utils.ext.assistedViewModels
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
class FavouritesCategoryEditActivity : BaseActivity<ActivityCategoryEditBinding>(), AdapterView.OnItemClickListener, @AndroidEntryPoint
View.OnClickListener, TextWatcher { class FavouritesCategoryEditActivity :
BaseActivity<ActivityCategoryEditBinding>(),
AdapterView.OnItemClickListener,
View.OnClickListener,
TextWatcher {
private val viewModel by viewModel<FavouritesCategoryEditViewModel> { @Inject
parametersOf(intent.getLongExtra(EXTRA_ID, NO_ID)) lateinit var viewModelFactory: FavouritesCategoryEditViewModel.Factory
private val viewModel by assistedViewModels<FavouritesCategoryEditViewModel> {
viewModelFactory.create(intent.getLongExtra(EXTRA_ID, NO_ID))
} }
private var selectedSortOrder: SortOrder? = null private var selectedSortOrder: SortOrder? = null
@ -164,4 +172,4 @@ class FavouritesCategoryEditActivity : BaseActivity<ActivityCategoryEditBinding>
.putExtra(EXTRA_ID, id) .putExtra(EXTRA_ID, id)
} }
} }
} }

@ -3,6 +3,9 @@ package org.koitharu.kotatsu.favourites.ui.categories.edit
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.liveData import androidx.lifecycle.liveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
@ -13,8 +16,8 @@ import org.koitharu.kotatsu.utils.SingleLiveEvent
private const val NO_ID = -1L private const val NO_ID = -1L
class FavouritesCategoryEditViewModel( class FavouritesCategoryEditViewModel @AssistedInject constructor(
private val categoryId: Long, @Assisted private val categoryId: Long,
private val repository: FavouritesRepository, private val repository: FavouritesRepository,
private val settings: AppSettings, private val settings: AppSettings,
) : BaseViewModel() { ) : BaseViewModel() {
@ -51,4 +54,10 @@ class FavouritesCategoryEditViewModel(
onSaved.call(Unit) onSaved.call(Unit)
} }
} }
}
@AssistedFactory
interface Factory {
fun create(categoryId: Long): FavouritesCategoryEditViewModel
}
}

@ -8,8 +8,7 @@ import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import org.koin.androidx.viewmodel.ext.android.viewModel import javax.inject.Inject
import org.koin.core.parameter.parametersOf
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseBottomSheet import org.koitharu.kotatsu.base.ui.BaseBottomSheet
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
@ -19,6 +18,7 @@ import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEdit
import org.koitharu.kotatsu.favourites.ui.categories.select.adapter.MangaCategoriesAdapter import org.koitharu.kotatsu.favourites.ui.categories.select.adapter.MangaCategoriesAdapter
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.ext.assistedViewModels
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.withArgs import org.koitharu.kotatsu.utils.ext.withArgs
@ -28,8 +28,13 @@ class FavouriteCategoriesBottomSheet :
View.OnClickListener, View.OnClickListener,
Toolbar.OnMenuItemClickListener { Toolbar.OnMenuItemClickListener {
private val viewModel by viewModel<MangaCategoriesViewModel> { @Inject
parametersOf(requireNotNull(arguments?.getParcelableArrayList<ParcelableManga>(KEY_MANGA_LIST)).map { it.manga }) lateinit var viewModelFactory: MangaCategoriesViewModel.Factory
private val viewModel by assistedViewModels {
viewModelFactory.create(
requireNotNull(arguments?.getParcelableArrayList<ParcelableManga>(KEY_MANGA_LIST)).map { it.manga },
)
} }
private var adapter: MangaCategoriesAdapter? = null private var adapter: MangaCategoriesAdapter? = null

@ -1,6 +1,9 @@
package org.koitharu.kotatsu.favourites.ui.categories.select package org.koitharu.kotatsu.favourites.ui.categories.select
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.base.ui.BaseViewModel
@ -10,9 +13,9 @@ import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryI
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
class MangaCategoriesViewModel( class MangaCategoriesViewModel @AssistedInject constructor(
private val manga: List<Manga>, @Assisted private val manga: List<Manga>,
private val favouritesRepository: FavouritesRepository private val favouritesRepository: FavouritesRepository,
) : BaseViewModel() { ) : BaseViewModel() {
val content = combine( val content = combine(
@ -23,7 +26,7 @@ class MangaCategoriesViewModel(
MangaCategoryItem( MangaCategoryItem(
id = it.id, id = it.id,
name = it.title, name = it.title,
isChecked = it.id in checked isChecked = it.id in checked,
) )
} }
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, emptyList()) }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
@ -43,7 +46,7 @@ class MangaCategoriesViewModel(
favouritesRepository.observeCategoriesIds(manga[0].id) favouritesRepository.observeCategoriesIds(manga[0].id)
} else { } else {
combine( combine(
manga.map { favouritesRepository.observeCategoriesIds(it.id) } manga.map { favouritesRepository.observeCategoriesIds(it.id) },
) { array -> ) { array ->
val result = HashSet<Long>() val result = HashSet<Long>()
var isFirst = true var isFirst = true
@ -58,4 +61,10 @@ class MangaCategoriesViewModel(
result result
} }
} }
}
@AssistedFactory
interface Factory {
fun create(manga: List<Manga>): MangaCategoriesViewModel
}
}

@ -6,20 +6,25 @@ import android.view.MenuItem
import android.view.View import android.view.View
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import org.koin.androidx.viewmodel.ext.android.viewModel import dagger.hilt.android.AndroidEntryPoint
import org.koin.core.parameter.parametersOf import javax.inject.Inject
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.ListSelectionController import org.koitharu.kotatsu.base.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.ui.titleRes import org.koitharu.kotatsu.core.ui.titleRes
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity
import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.utils.ext.assistedViewModels
import org.koitharu.kotatsu.utils.ext.withArgs import org.koitharu.kotatsu.utils.ext.withArgs
@AndroidEntryPoint
class FavouritesListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickListener { class FavouritesListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickListener {
override val viewModel by viewModel<FavouritesListViewModel> { @Inject
parametersOf(categoryId) lateinit var viewModelFactory: FavouritesListViewModel.Factory
override val viewModel by assistedViewModels<FavouritesListViewModel> {
viewModelFactory.create(categoryId)
} }
private val categoryId: Long private val categoryId: Long

@ -3,6 +3,9 @@ package org.koitharu.kotatsu.favourites.ui.list
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
@ -25,8 +28,8 @@ import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
class FavouritesListViewModel( class FavouritesListViewModel @AssistedInject constructor(
private val categoryId: Long, @Assisted private val categoryId: Long,
private val repository: FavouritesRepository, private val repository: FavouritesRepository,
private val trackingRepository: TrackingRepository, private val trackingRepository: TrackingRepository,
private val historyRepository: HistoryRepository, private val historyRepository: HistoryRepository,
@ -121,4 +124,10 @@ class FavouritesListViewModel(
PROGRESS_NONE PROGRESS_NONE
} }
} }
@AssistedFactory
interface Factory {
fun create(categoryId: Long): FavouritesListViewModel
}
} }

@ -1,14 +0,0 @@
package org.koitharu.kotatsu.history
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.history.ui.HistoryListViewModel
val historyModule
get() = module {
single { HistoryRepository(get(), get(), get(), getAll()) }
viewModel { HistoryListViewModel(get(), get(), get()) }
}

@ -1,6 +1,7 @@
package org.koitharu.kotatsu.history.domain package org.koitharu.kotatsu.history.domain
import androidx.room.withTransaction import androidx.room.withTransaction
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@ -20,11 +21,11 @@ import org.koitharu.kotatsu.utils.ext.mapItems
const val PROGRESS_NONE = -1f const val PROGRESS_NONE = -1f
class HistoryRepository( class HistoryRepository @Inject constructor(
private val db: MangaDatabase, private val db: MangaDatabase,
private val trackingRepository: TrackingRepository, private val trackingRepository: TrackingRepository,
private val settings: AppSettings, private val settings: AppSettings,
private val scrobblers: List<Scrobbler>, private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>,
) { ) {
suspend fun getList(offset: Int, limit: Int = 20): List<Manga> { suspend fun getList(offset: Int, limit: Int = 20): List<Manga> {
@ -82,7 +83,7 @@ class HistoryRepository(
scroll = scroll.toFloat(), // we migrate to int, but decide to not update database scroll = scroll.toFloat(), // we migrate to int, but decide to not update database
percent = percent, percent = percent,
deletedAt = 0L, deletedAt = 0L,
) ),
) )
trackingRepository.syncWithHistory(manga, chapterId) trackingRepository.syncWithHistory(manga, chapterId)
val chapter = manga.chapters?.find { x -> x.id == chapterId } val chapter = manga.chapters?.find { x -> x.id == chapterId }

@ -9,11 +9,13 @@ import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.commit import androidx.fragment.app.commit
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivityContainerBinding import org.koitharu.kotatsu.databinding.ActivityContainerBinding
import org.koitharu.kotatsu.main.ui.AppBarOwner import org.koitharu.kotatsu.main.ui.AppBarOwner
@AndroidEntryPoint
class HistoryActivity : class HistoryActivity :
BaseActivity<ActivityContainerBinding>(), BaseActivity<ActivityContainerBinding>(),
AppBarOwner { AppBarOwner {

@ -5,17 +5,18 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import org.koin.android.ext.android.get import androidx.fragment.app.viewModels
import org.koin.androidx.viewmodel.ext.android.viewModel import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.ListSelectionController import org.koitharu.kotatsu.base.ui.list.ListSelectionController
import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.utils.ext.addMenuProvider import org.koitharu.kotatsu.utils.ext.addMenuProvider
@AndroidEntryPoint
class HistoryListFragment : MangaListFragment() { class HistoryListFragment : MangaListFragment() {
override val viewModel by viewModel<HistoryListViewModel>() override val viewModel by viewModels<HistoryListViewModel>()
override val isSwipeRefreshEnabled = false override val isSwipeRefreshEnabled = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -51,7 +52,7 @@ class HistoryListFragment : MangaListFragment() {
} }
} }
override fun onCreateAdapter() = HistoryListAdapter(get(), viewLifecycleOwner, this) override fun onCreateAdapter() = HistoryListAdapter(coil, viewLifecycleOwner, this)
companion object { companion object {

@ -2,6 +2,10 @@ package org.koitharu.kotatsu.history.ui
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import java.util.*
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
@ -22,10 +26,9 @@ import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.daysDiff import org.koitharu.kotatsu.utils.ext.daysDiff
import org.koitharu.kotatsu.utils.ext.onFirst import org.koitharu.kotatsu.utils.ext.onFirst
import java.util.*
import java.util.concurrent.TimeUnit
class HistoryListViewModel( @HiltViewModel
class HistoryListViewModel @Inject constructor(
private val repository: HistoryRepository, private val repository: HistoryRepository,
private val settings: AppSettings, private val settings: AppSettings,
private val trackingRepository: TrackingRepository, private val trackingRepository: TrackingRepository,
@ -39,7 +42,7 @@ class HistoryListViewModel(
override val content = combine( override val content = combine(
repository.observeAllWithHistory(), repository.observeAllWithHistory(),
historyGrouping, historyGrouping,
createListModeFlow() createListModeFlow(),
) { list, grouped, mode -> ) { list, grouped, mode ->
when { when {
list.isEmpty() -> listOf( list.isEmpty() -> listOf(
@ -48,7 +51,7 @@ class HistoryListViewModel(
textPrimary = R.string.text_history_holder_primary, textPrimary = R.string.text_history_holder_primary,
textSecondary = R.string.text_history_holder_secondary, textSecondary = R.string.text_history_holder_secondary,
actionStringRes = 0, actionStringRes = 0,
) ),
) )
else -> mapList(list, grouped, mode) else -> mapList(list, grouped, mode)
} }
@ -87,7 +90,7 @@ class HistoryListViewModel(
private suspend fun mapList( private suspend fun mapList(
list: List<MangaWithHistory>, list: List<MangaWithHistory>,
grouped: Boolean, grouped: Boolean,
mode: ListMode mode: ListMode,
): List<ListModel> { ): List<ListModel> {
val result = ArrayList<ListModel>(if (grouped) (list.size * 1.4).toInt() else list.size + 1) val result = ArrayList<ListModel>(if (grouped) (list.size * 1.4).toInt() else list.size + 1)
val showPercent = settings.isReadingIndicatorsEnabled val showPercent = settings.isReadingIndicatorsEnabled

@ -16,15 +16,18 @@ import coil.request.ImageRequest
import coil.target.ViewTarget import coil.target.ViewTarget
import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import org.koin.android.ext.android.inject import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivityImageBinding import org.koitharu.kotatsu.databinding.ActivityImageBinding
import org.koitharu.kotatsu.utils.ext.enqueueWith import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.indicator import org.koitharu.kotatsu.utils.ext.indicator
@AndroidEntryPoint
class ImageActivity : BaseActivity<ActivityImageBinding>() { class ImageActivity : BaseActivity<ActivityImageBinding>() {
private val coil: ImageLoader by inject() @Inject
lateinit var coil: ImageLoader
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -40,7 +43,7 @@ class ImageActivity : BaseActivity<ActivityImageBinding>() {
with(binding.toolbar) { with(binding.toolbar) {
updatePadding( updatePadding(
left = insets.left, left = insets.left,
right = insets.right right = insets.right,
) )
updateLayoutParams<ViewGroup.MarginLayoutParams> { updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = insets.top topMargin = insets.top
@ -90,4 +93,4 @@ class ImageActivity : BaseActivity<ActivityImageBinding>() {
.setData(Uri.parse(url)) .setData(Uri.parse(url))
} }
} }
} }

@ -1,16 +0,0 @@
package org.koitharu.kotatsu.library
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
import org.koitharu.kotatsu.library.domain.LibraryRepository
import org.koitharu.kotatsu.library.ui.LibraryViewModel
import org.koitharu.kotatsu.library.ui.config.categories.LibraryCategoriesConfigViewModel
val libraryModule
get() = module {
factory { LibraryRepository(get()) }
viewModel { LibraryViewModel(get(), get(), get(), get(), get()) }
viewModel { LibraryCategoriesConfigViewModel(get()) }
}

@ -1,5 +1,6 @@
package org.koitharu.kotatsu.library.domain package org.koitharu.kotatsu.library.domain
import javax.inject.Inject
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.toManga import org.koitharu.kotatsu.core.db.entity.toManga
@ -9,7 +10,7 @@ import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.toFavouriteCategory import org.koitharu.kotatsu.favourites.data.toFavouriteCategory
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
class LibraryRepository( class LibraryRepository @Inject constructor(
private val db: MangaDatabase, private val db: MangaDatabase,
) { ) {

@ -6,14 +6,17 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.viewModels
import coil.ImageLoader
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import org.koin.android.ext.android.get import dagger.hilt.android.AndroidEntryPoint
import org.koin.androidx.viewmodel.ext.android.viewModel import javax.inject.Inject
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.reverseAsync import org.koitharu.kotatsu.base.domain.reverseAsync
import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
import org.koitharu.kotatsu.base.ui.util.ReversibleAction import org.koitharu.kotatsu.base.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.databinding.FragmentLibraryBinding import org.koitharu.kotatsu.databinding.FragmentLibraryBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.favourites.ui.FavouritesActivity import org.koitharu.kotatsu.favourites.ui.FavouritesActivity
@ -28,11 +31,18 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.ext.addMenuProvider import org.koitharu.kotatsu.utils.ext.addMenuProvider
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
@AndroidEntryPoint
class LibraryFragment : class LibraryFragment :
BaseFragment<FragmentLibraryBinding>(), BaseFragment<FragmentLibraryBinding>(),
LibraryListEventListener { LibraryListEventListener {
private val viewModel by viewModel<LibraryViewModel>() @Inject
lateinit var coil: ImageLoader
@Inject
lateinit var settings: AppSettings
private val viewModel by viewModels<LibraryViewModel>()
private var adapter: LibraryAdapter? = null private var adapter: LibraryAdapter? = null
private var selectionController: SectionedSelectionController<LibrarySectionModel>? = null private var selectionController: SectionedSelectionController<LibrarySectionModel>? = null
@ -42,7 +52,7 @@ class LibraryFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val sizeResolver = ItemSizeResolver(resources, get()) val sizeResolver = ItemSizeResolver(resources, settings)
selectionController = SectionedSelectionController( selectionController = SectionedSelectionController(
activity = requireActivity(), activity = requireActivity(),
owner = this, owner = this,
@ -50,7 +60,7 @@ class LibraryFragment :
) )
adapter = LibraryAdapter( adapter = LibraryAdapter(
lifecycleOwner = viewLifecycleOwner, lifecycleOwner = viewLifecycleOwner,
coil = get(), coil = coil,
listener = this, listener = this,
sizeResolver = sizeResolver, sizeResolver = sizeResolver,
selectionController = checkNotNull(selectionController), selectionController = checkNotNull(selectionController),

@ -3,7 +3,9 @@ package org.koitharu.kotatsu.library.ui
import androidx.collection.ArraySet import androidx.collection.ArraySet
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import java.util.* import java.util.*
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
@ -30,7 +32,8 @@ import org.koitharu.kotatsu.utils.ext.daysDiff
private const val HISTORY_MAX_SEGMENTS = 2 private const val HISTORY_MAX_SEGMENTS = 2
class LibraryViewModel( @HiltViewModel
class LibraryViewModel @Inject constructor(
repository: LibraryRepository, repository: LibraryRepository,
private val historyRepository: HistoryRepository, private val historyRepository: HistoryRepository,
private val favouritesRepository: FavouritesRepository, private val favouritesRepository: FavouritesRepository,

@ -6,19 +6,21 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import org.koin.androidx.viewmodel.ext.android.viewModel import androidx.fragment.app.viewModels
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseBottomSheet import org.koitharu.kotatsu.base.ui.BaseBottomSheet
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.databinding.SheetBaseBinding import org.koitharu.kotatsu.databinding.SheetBaseBinding
@AndroidEntryPoint
class LibraryCategoriesConfigSheet : class LibraryCategoriesConfigSheet :
BaseBottomSheet<SheetBaseBinding>(), BaseBottomSheet<SheetBaseBinding>(),
OnListItemClickListener<FavouriteCategory>, OnListItemClickListener<FavouriteCategory>,
View.OnClickListener { View.OnClickListener {
private val viewModel by viewModel<LibraryCategoriesConfigViewModel>() private val viewModel by viewModels<LibraryCategoriesConfigViewModel>()
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetBaseBinding { override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetBaseBinding {
return SheetBaseBinding.inflate(inflater, container, false) return SheetBaseBinding.inflate(inflater, container, false)

@ -1,6 +1,8 @@
package org.koitharu.kotatsu.library.ui.config.categories package org.koitharu.kotatsu.library.ui.config.categories
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.base.ui.BaseViewModel
@ -8,7 +10,8 @@ import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
class LibraryCategoriesConfigViewModel( @HiltViewModel
class LibraryCategoriesConfigViewModel @Inject constructor(
private val favouritesRepository: FavouritesRepository, private val favouritesRepository: FavouritesRepository,
) : BaseViewModel() { ) : BaseViewModel() {

@ -7,7 +7,8 @@ import android.view.ViewGroup
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import com.google.android.material.slider.LabelFormatter import com.google.android.material.slider.LabelFormatter
import com.google.android.material.slider.Slider import com.google.android.material.slider.Slider
import org.koin.android.ext.android.inject import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseBottomSheet import org.koitharu.kotatsu.base.ui.BaseBottomSheet
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
@ -15,12 +16,14 @@ import org.koitharu.kotatsu.databinding.SheetLibrarySizeBinding
import org.koitharu.kotatsu.utils.ext.setValueRounded import org.koitharu.kotatsu.utils.ext.setValueRounded
import org.koitharu.kotatsu.utils.progress.IntPercentLabelFormatter import org.koitharu.kotatsu.utils.progress.IntPercentLabelFormatter
@AndroidEntryPoint
class LibrarySizeBottomSheet : class LibrarySizeBottomSheet :
BaseBottomSheet<SheetLibrarySizeBinding>(), BaseBottomSheet<SheetLibrarySizeBinding>(),
Slider.OnChangeListener, Slider.OnChangeListener,
View.OnClickListener { View.OnClickListener {
private val settings by inject<AppSettings>(mode = LazyThreadSafetyMode.NONE) @Inject
lateinit var settings: AppSettings
private var labelFormatter: LabelFormatter? = null private var labelFormatter: LabelFormatter? = null
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetLibrarySizeBinding { override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetLibrarySizeBinding {

@ -8,7 +8,7 @@ import androidx.core.view.isVisible
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.slider.Slider import com.google.android.material.slider.Slider
import org.koin.android.ext.android.inject import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.AlertDialogFragment import org.koitharu.kotatsu.base.ui.AlertDialogFragment
import org.koitharu.kotatsu.base.ui.widgets.CheckableButtonGroup import org.koitharu.kotatsu.base.ui.widgets.CheckableButtonGroup
@ -17,13 +17,16 @@ import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.databinding.DialogListModeBinding import org.koitharu.kotatsu.databinding.DialogListModeBinding
import org.koitharu.kotatsu.utils.ext.setValueRounded import org.koitharu.kotatsu.utils.ext.setValueRounded
import org.koitharu.kotatsu.utils.progress.IntPercentLabelFormatter import org.koitharu.kotatsu.utils.progress.IntPercentLabelFormatter
import javax.inject.Inject
@AndroidEntryPoint
class ListModeSelectDialog : class ListModeSelectDialog :
AlertDialogFragment<DialogListModeBinding>(), AlertDialogFragment<DialogListModeBinding>(),
CheckableButtonGroup.OnCheckedChangeListener, CheckableButtonGroup.OnCheckedChangeListener,
Slider.OnChangeListener { Slider.OnChangeListener {
private val settings by inject<AppSettings>(mode = LazyThreadSafetyMode.NONE) @Inject
lateinit var settings: AppSettings
override fun onInflateView( override fun onInflateView(
inflater: LayoutInflater, inflater: LayoutInflater,

@ -12,9 +12,10 @@ import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import coil.ImageLoader
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import javax.inject.Inject
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.get
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.reverseAsync import org.koitharu.kotatsu.base.domain.reverseAsync
import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.base.ui.BaseFragment
@ -56,6 +57,9 @@ abstract class MangaListFragment :
ListSelectionController.Callback2, ListSelectionController.Callback2,
FastScroller.FastScrollListener { FastScroller.FastScrollListener {
@Inject
lateinit var coil: ImageLoader
private var listAdapter: MangaListAdapter? = null private var listAdapter: MangaListAdapter? = null
private var paginationListener: PaginationScrollListener? = null private var paginationListener: PaginationScrollListener? = null
private var selectionController: ListSelectionController? = null private var selectionController: ListSelectionController? = null
@ -188,7 +192,7 @@ abstract class MangaListFragment :
protected open fun onCreateAdapter(): MangaListAdapter { protected open fun onCreateAdapter(): MangaListAdapter {
return MangaListAdapter( return MangaListAdapter(
coil = get(), coil = coil,
lifecycleOwner = viewLifecycleOwner, lifecycleOwner = viewLifecycleOwner,
listener = this, listener = this,
) )

@ -6,11 +6,12 @@ import android.os.Bundle
import android.view.* import android.view.*
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import org.koin.androidx.viewmodel.ext.android.sharedViewModel import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseBottomSheet import org.koitharu.kotatsu.base.ui.BaseBottomSheet
import org.koitharu.kotatsu.databinding.SheetFilterBinding import org.koitharu.kotatsu.databinding.SheetFilterBinding
import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel
import org.koitharu.kotatsu.utils.ext.parentFragmentViewModels
class FilterBottomSheet : class FilterBottomSheet :
BaseBottomSheet<SheetFilterBinding>(), BaseBottomSheet<SheetFilterBinding>(),
@ -18,9 +19,7 @@ class FilterBottomSheet :
SearchView.OnQueryTextListener, SearchView.OnQueryTextListener,
DialogInterface.OnKeyListener { DialogInterface.OnKeyListener {
private val viewModel by sharedViewModel<RemoteListViewModel>( private val viewModel by parentFragmentViewModels<RemoteListViewModel>()
owner = { requireParentFragment() },
)
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return super.onCreateDialog(savedInstanceState).also { return super.onCreateDialog(savedInstanceState).also {

@ -1,20 +1 @@
package org.koitharu.kotatsu.local package org.koitharu.kotatsu.local
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
import org.koitharu.kotatsu.download.domain.DownloadManager
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.local.ui.LocalListViewModel
val localModule
get() = module {
factory { LocalStorageManager(androidContext(), get()) }
single { LocalMangaRepository(get()) }
factory { DownloadManager.Factory(androidContext(), get(), get(), get(), get(), get()) }
viewModel { LocalListViewModel(get(), get(), get()) }
}

@ -4,6 +4,10 @@ import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.os.StatFs import android.os.StatFs
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.File
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -12,15 +16,15 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.utils.ext.computeSize import org.koitharu.kotatsu.utils.ext.computeSize
import org.koitharu.kotatsu.utils.ext.getStorageName import org.koitharu.kotatsu.utils.ext.getStorageName
import java.io.File
private const val DIR_NAME = "manga" private const val DIR_NAME = "manga"
private const val CACHE_DISK_PERCENTAGE = 0.02 private const val CACHE_DISK_PERCENTAGE = 0.02
private const val CACHE_SIZE_MIN: Long = 10 * 1024 * 1024 // 10MB private const val CACHE_SIZE_MIN: Long = 10 * 1024 * 1024 // 10MB
private const val CACHE_SIZE_MAX: Long = 250 * 1024 * 1024 // 250MB private const val CACHE_SIZE_MAX: Long = 250 * 1024 * 1024 // 250MB
class LocalStorageManager( @Singleton
private val context: Context, class LocalStorageManager @Inject constructor(
@ApplicationContext private val context: Context,
private val settings: AppSettings, private val settings: AppSettings,
) { ) {
@ -131,4 +135,4 @@ class LocalStorageManager(
private fun File.isWriteable() = runCatching { private fun File.isWriteable() = runCatching {
canWrite() canWrite()
}.getOrDefault(false) }.getOrDefault(false)
} }

@ -2,15 +2,19 @@ package org.koitharu.kotatsu.local.data
import android.content.Context import android.content.Context
import com.tomclaw.cache.DiskLruCache import com.tomclaw.cache.DiskLruCache
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.File
import java.io.InputStream
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import org.koitharu.kotatsu.utils.FileSize import org.koitharu.kotatsu.utils.FileSize
import org.koitharu.kotatsu.utils.ext.longHashCode import org.koitharu.kotatsu.utils.ext.longHashCode
import org.koitharu.kotatsu.utils.ext.subdir import org.koitharu.kotatsu.utils.ext.subdir
import org.koitharu.kotatsu.utils.ext.takeIfReadable import org.koitharu.kotatsu.utils.ext.takeIfReadable
import java.io.File
import java.io.InputStream
class PagesCache(context: Context) { @Singleton
class PagesCache @Inject constructor(@ApplicationContext context: Context) {
private val cacheDir = context.externalCacheDir ?: context.cacheDir private val cacheDir = context.externalCacheDir ?: context.cacheDir
private val lruCache = createDiskLruCacheSafe( private val lruCache = createDiskLruCacheSafe(
@ -70,4 +74,4 @@ private fun createDiskLruCacheSafe(dir: File, size: Long): DiskLruCache {
dir.mkdir() dir.mkdir()
DiskLruCache.create(dir, size) DiskLruCache.create(dir, size)
} }
} }

@ -12,6 +12,8 @@ import java.io.IOException
import java.util.* import java.util.*
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipFile import java.util.zip.ZipFile
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
@ -31,7 +33,8 @@ import org.koitharu.kotatsu.utils.ext.resolveName
private const val MAX_PARALLELISM = 4 private const val MAX_PARALLELISM = 4
class LocalMangaRepository(private val storageManager: LocalStorageManager) : MangaRepository { @Singleton
class LocalMangaRepository @Inject constructor(private val storageManager: LocalStorageManager) : MangaRepository {
override val source = MangaSource.LOCAL override val source = MangaSource.LOCAL
private val filenameFilter = CbzFilter() private val filenameFilter = CbzFilter()
@ -86,7 +89,7 @@ class LocalMangaRepository(private val storageManager: LocalStorageManager) : Ma
entries.filter { x -> entries.filter { x ->
!x.isDirectory && x.name.substringBeforeLast( !x.isDirectory && x.name.substringBeforeLast(
File.separatorChar, File.separatorChar,
"" "",
) == parent ) == parent
} }
} }
@ -138,11 +141,11 @@ class LocalMangaRepository(private val storageManager: LocalStorageManager) : Ma
url = fileUri, url = fileUri,
coverUrl = zipUri( coverUrl = zipUri(
file, file,
entryName = index.getCoverEntry() ?: findFirstImageEntry(zip.entries())?.name.orEmpty() entryName = index.getCoverEntry() ?: findFirstImageEntry(zip.entries())?.name.orEmpty(),
), ),
chapters = info.chapters?.map { c -> chapters = info.chapters?.map { c ->
c.copy(url = fileUri, source = MangaSource.LOCAL) c.copy(url = fileUri, source = MangaSource.LOCAL)
} },
) )
} }
// fallback // fallback
@ -211,7 +214,7 @@ class LocalMangaRepository(private val storageManager: LocalStorageManager) : Ma
return@runInterruptible info.copy2( return@runInterruptible info.copy2(
source = MangaSource.LOCAL, source = MangaSource.LOCAL,
url = fileUri, url = fileUri,
chapters = info.chapters?.map { c -> c.copy(url = fileUri) } chapters = info.chapters?.map { c -> c.copy(url = fileUri) },
) )
} }
} }
@ -342,4 +345,4 @@ class LocalMangaRepository(private val storageManager: LocalStorageManager) : Ma
branch = branch, branch = branch,
source = source, source = source,
) )
} }

@ -8,7 +8,8 @@ import android.os.Build
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import org.koin.android.ext.android.inject import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.CoroutineIntentService import org.koitharu.kotatsu.base.ui.CoroutineIntentService
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
@ -16,9 +17,11 @@ import org.koitharu.kotatsu.download.ui.service.DownloadService
import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
@AndroidEntryPoint
class LocalChaptersRemoveService : CoroutineIntentService() { class LocalChaptersRemoveService : CoroutineIntentService() {
private val localMangaRepository by inject<LocalMangaRepository>() @Inject
lateinit var localMangaRepository: LocalMangaRepository
override suspend fun processIntent(intent: Intent?) { override suspend fun processIntent(intent: Intent?) {
val manga = intent?.getParcelableExtra<ParcelableManga>(EXTRA_MANGA)?.manga ?: return val manga = intent?.getParcelableExtra<ParcelableManga>(EXTRA_MANGA)?.manga ?: return
@ -28,7 +31,7 @@ class LocalChaptersRemoveService : CoroutineIntentService() {
localMangaRepository.deleteChapters(mangaWithChapters, chaptersIds) localMangaRepository.deleteChapters(mangaWithChapters, chaptersIds)
sendBroadcast( sendBroadcast(
Intent(DownloadService.ACTION_DOWNLOAD_COMPLETE) Intent(DownloadService.ACTION_DOWNLOAD_COMPLETE)
.putExtra(EXTRA_MANGA, ParcelableManga(manga, withChapters = false)) .putExtra(EXTRA_MANGA, ParcelableManga(manga, withChapters = false)),
) )
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
} }
@ -77,4 +80,4 @@ class LocalChaptersRemoveService : CoroutineIntentService() {
ContextCompat.startForegroundService(context, intent) ContextCompat.startForegroundService(context, intent)
} }
} }
} }

@ -11,9 +11,9 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.core.net.toFile import androidx.core.net.toFile
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.fragment.app.viewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.ListSelectionController import org.koitharu.kotatsu.base.ui.list.ListSelectionController
import org.koitharu.kotatsu.download.ui.service.DownloadService import org.koitharu.kotatsu.download.ui.service.DownloadService
@ -25,7 +25,7 @@ import org.koitharu.kotatsu.utils.progress.Progress
class LocalListFragment : MangaListFragment(), ActivityResultCallback<List<@JvmSuppressWildcards Uri>> { class LocalListFragment : MangaListFragment(), ActivityResultCallback<List<@JvmSuppressWildcards Uri>> {
override val viewModel by viewModel<LocalListViewModel>() override val viewModel by viewModels<LocalListViewModel>()
private val importCall = registerForActivityResult( private val importCall = registerForActivityResult(
ActivityResultContracts.OpenMultipleDocuments(), ActivityResultContracts.OpenMultipleDocuments(),
this, this,

@ -3,6 +3,9 @@ package org.koitharu.kotatsu.local.ui
import android.net.Uri import android.net.Uri
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import java.io.IOException
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -25,9 +28,9 @@ import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.progress.Progress import org.koitharu.kotatsu.utils.progress.Progress
import java.io.IOException
class LocalListViewModel( @HiltViewModel
class LocalListViewModel @Inject constructor(
private val repository: LocalMangaRepository, private val repository: LocalMangaRepository,
private val historyRepository: HistoryRepository, private val historyRepository: HistoryRepository,
settings: AppSettings, settings: AppSettings,
@ -42,7 +45,7 @@ class LocalListViewModel(
override val content = combine( override val content = combine(
mangaList, mangaList,
createListModeFlow(), createListModeFlow(),
listError listError,
) { list, mode, error -> ) { list, mode, error ->
when { when {
error != null -> listOf(error.toErrorState(canRetry = true)) error != null -> listOf(error.toErrorState(canRetry = true))
@ -53,7 +56,7 @@ class LocalListViewModel(
textPrimary = R.string.text_local_holder_primary, textPrimary = R.string.text_local_holder_primary,
textSecondary = R.string.text_local_holder_secondary, textSecondary = R.string.text_local_holder_secondary,
actionStringRes = R.string._import, actionStringRes = R.string._import,
) ),
) )
else -> list.toUi(mode) else -> list.toUi(mode)
} }
@ -125,4 +128,4 @@ class LocalListViewModel(
} }
} }
} }
} }

@ -1,29 +0,0 @@
package org.koitharu.kotatsu.main
import android.app.Application
import android.os.Build
import androidx.room.InvalidationTracker
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.bind
import org.koin.dsl.module
import org.koitharu.kotatsu.base.ui.util.ActivityRecreationHandle
import org.koitharu.kotatsu.core.os.ShortcutsUpdater
import org.koitharu.kotatsu.main.ui.MainViewModel
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
import org.koitharu.kotatsu.main.ui.protect.ProtectViewModel
val mainModule
get() = module {
single { AppProtectHelper(get()) } bind Application.ActivityLifecycleCallbacks::class
single { ActivityRecreationHandle() } bind Application.ActivityLifecycleCallbacks::class
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
single { ShortcutsUpdater(androidContext(), get(), get(), get()) } bind InvalidationTracker.Observer::class
} else {
factory { ShortcutsUpdater(androidContext(), get(), get(), get()) }
}
viewModel { MainViewModel(get(), get(), get(), get()) }
viewModel { ProtectViewModel(get(), get()) }
}

@ -1,7 +1,6 @@
package org.koitharu.kotatsu.main.ui package org.koitharu.kotatsu.main.ui
import android.view.View import android.view.View
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
@ -12,13 +11,13 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.get
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow import org.koitharu.kotatsu.core.prefs.observeAsFlow
class ExitCallback( class ExitCallback(
private val activity: ComponentActivity, private val activity: BaseActivity<*>,
private val snackbarHost: View, private val snackbarHost: View,
) : OnBackPressedCallback(false) { ) : OnBackPressedCallback(false) {
@ -46,7 +45,7 @@ class ExitCallback(
} }
private fun observeSettings() { private fun observeSettings() {
activity.get<AppSettings>() activity.settings
.observeAsFlow(AppSettings.KEY_EXIT_CONFIRM) { isExitConfirmationEnabled } .observeAsFlow(AppSettings.KEY_EXIT_CONFIRM) { isExitConfirmationEnabled }
.flowOn(Dispatchers.Default) .flowOn(Dispatchers.Default)
.onEach { isEnabled = it } .onEach { isEnabled = it }

@ -6,6 +6,7 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup.MarginLayoutParams import android.view.ViewGroup.MarginLayoutParams
import androidx.activity.result.ActivityResultCallback import androidx.activity.result.ActivityResultCallback
import androidx.activity.viewModels
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.core.app.ActivityOptionsCompat import androidx.core.app.ActivityOptionsCompat
@ -17,20 +18,19 @@ import androidx.fragment.app.FragmentTransaction
import androidx.fragment.app.commit import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.transition.TransitionManager import androidx.transition.TransitionManager
import com.google.android.material.R as materialR
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.AppBarLayout.LayoutParams.* import com.google.android.material.appbar.AppBarLayout.LayoutParams.*
import com.google.android.material.navigation.NavigationBarView import com.google.android.material.navigation.NavigationBarView
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner
import org.koitharu.kotatsu.base.ui.widgets.KotatsuBottomNavigationView import org.koitharu.kotatsu.base.ui.widgets.KotatsuBottomNavigationView
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.databinding.ActivityMainBinding import org.koitharu.kotatsu.databinding.ActivityMainBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.explore.ui.ExploreFragment import org.koitharu.kotatsu.explore.ui.ExploreFragment
@ -48,16 +48,15 @@ import org.koitharu.kotatsu.settings.newsources.NewSourcesDialogFragment
import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment
import org.koitharu.kotatsu.settings.tools.ToolsFragment import org.koitharu.kotatsu.settings.tools.ToolsFragment
import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker
import org.koitharu.kotatsu.sync.domain.SyncController
import org.koitharu.kotatsu.tracker.ui.FeedFragment import org.koitharu.kotatsu.tracker.ui.FeedFragment
import org.koitharu.kotatsu.tracker.work.TrackWorker import org.koitharu.kotatsu.tracker.work.TrackWorker
import org.koitharu.kotatsu.utils.VoiceInputContract import org.koitharu.kotatsu.utils.VoiceInputContract
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
import com.google.android.material.R as materialR
private const val TAG_PRIMARY = "primary" private const val TAG_PRIMARY = "primary"
private const val TAG_SEARCH = "search" private const val TAG_SEARCH = "search"
@AndroidEntryPoint
class MainActivity : class MainActivity :
BaseActivity<ActivityMainBinding>(), BaseActivity<ActivityMainBinding>(),
AppBarOwner, AppBarOwner,
@ -68,8 +67,8 @@ class MainActivity :
NavigationBarView.OnItemSelectedListener, NavigationBarView.OnItemSelectedListener,
NavigationBarView.OnItemReselectedListener { NavigationBarView.OnItemReselectedListener {
private val viewModel by viewModel<MainViewModel>() private val viewModel by viewModels<MainViewModel>()
private val searchSuggestionViewModel by viewModel<SearchSuggestionViewModel>() private val searchSuggestionViewModel by viewModels<SearchSuggestionViewModel>()
private val voiceInputLauncher = registerForActivityResult(VoiceInputContract(), VoiceInputCallback()) private val voiceInputLauncher = registerForActivityResult(VoiceInputContract(), VoiceInputCallback())
private lateinit var navBar: NavigationBarView private lateinit var navBar: NavigationBarView
@ -284,7 +283,8 @@ class MainActivity :
} }
private fun onError(e: Throwable) { private fun onError(e: Throwable) {
Snackbar.make(binding.container, e.getDisplayMessage(resources), Snackbar.LENGTH_SHORT).setAnchorView(bottomNav).show() Snackbar.make(binding.container, e.getDisplayMessage(resources), Snackbar.LENGTH_SHORT).setAnchorView(bottomNav)
.show()
} }
private fun onCountersChanged(counters: SparseIntArray) { private fun onCountersChanged(counters: SparseIntArray) {
@ -366,13 +366,12 @@ class MainActivity :
TrackWorker.setup(applicationContext) TrackWorker.setup(applicationContext)
SuggestionsWorker.setup(applicationContext) SuggestionsWorker.setup(applicationContext)
} }
val settings = get<AppSettings>()
when { when {
!settings.isSourcesSelected -> OnboardDialogFragment.showWelcome(supportFragmentManager) !settings.isSourcesSelected -> OnboardDialogFragment.showWelcome(supportFragmentManager)
settings.newSources.isNotEmpty() -> NewSourcesDialogFragment.show(supportFragmentManager) settings.newSources.isNotEmpty() -> NewSourcesDialogFragment.show(supportFragmentManager)
} }
yield() yield()
get<SyncController>().requestFullSyncAndGc(get()) // TODO get<SyncController>().requestFullSyncAndGc(get())
} }
} }

@ -3,6 +3,7 @@ package org.koitharu.kotatsu.main.ui
import android.util.SparseIntArray import android.util.SparseIntArray
import androidx.core.util.set import androidx.core.util.set
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
@ -15,8 +16,10 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import javax.inject.Inject
class MainViewModel( @HiltViewModel
class MainViewModel @Inject constructor(
private val historyRepository: HistoryRepository, private val historyRepository: HistoryRepository,
private val settings: AppSettings, private val settings: AppSettings,
private val appUpdateRepository: AppUpdateRepository, private val appUpdateRepository: AppUpdateRepository,

@ -4,9 +4,12 @@ import android.app.Activity
import android.app.Application import android.app.Application
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import javax.inject.Inject
import javax.inject.Singleton
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
class AppProtectHelper(private val settings: AppSettings) : Application.ActivityLifecycleCallbacks { @Singleton
class AppProtectHelper @Inject constructor(private val settings: AppSettings) : Application.ActivityLifecycleCallbacks {
private var isUnlocked = settings.appPassword.isNullOrEmpty() private var isUnlocked = settings.appPassword.isNullOrEmpty()
@ -46,4 +49,4 @@ class AppProtectHelper(private val settings: AppSettings) : Application.Activity
private fun restoreLock() { private fun restoreLock() {
isUnlocked = settings.appPassword.isNullOrEmpty() isUnlocked = settings.appPassword.isNullOrEmpty()
} }
} }

@ -10,25 +10,27 @@ import android.view.View
import android.view.WindowManager import android.view.WindowManager
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.widget.TextView import android.widget.TextView
import androidx.activity.viewModels
import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
import androidx.biometric.BiometricManager.BIOMETRIC_SUCCESS import androidx.biometric.BiometricManager.BIOMETRIC_SUCCESS
import androidx.biometric.BiometricPrompt import androidx.biometric.BiometricPrompt
import androidx.biometric.BiometricPrompt.AuthenticationCallback import androidx.biometric.BiometricPrompt.AuthenticationCallback
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import org.koin.androidx.viewmodel.ext.android.viewModel import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivityProtectBinding import org.koitharu.kotatsu.databinding.ActivityProtectBinding
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
@AndroidEntryPoint
class ProtectActivity : class ProtectActivity :
BaseActivity<ActivityProtectBinding>(), BaseActivity<ActivityProtectBinding>(),
TextView.OnEditorActionListener, TextView.OnEditorActionListener,
TextWatcher, TextWatcher,
View.OnClickListener { View.OnClickListener {
private val viewModel by viewModel<ProtectViewModel>() private val viewModel by viewModels<ProtectViewModel>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -58,7 +60,7 @@ class ProtectActivity :
basePadding + insets.left, basePadding + insets.left,
basePadding + insets.top, basePadding + insets.top,
basePadding + insets.right, basePadding + insets.right,
basePadding + insets.bottom basePadding + insets.bottom,
) )
} }
@ -129,4 +131,4 @@ class ProtectActivity :
.putExtra(EXTRA_INTENT, sourceIntent) .putExtra(EXTRA_INTENT, sourceIntent)
} }
} }
} }

@ -1,5 +1,7 @@
package org.koitharu.kotatsu.main.ui.protect package org.koitharu.kotatsu.main.ui.protect
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.base.ui.BaseViewModel
@ -10,7 +12,8 @@ import org.koitharu.kotatsu.utils.SingleLiveEvent
private const val PASSWORD_COMPARE_DELAY = 1_000L private const val PASSWORD_COMPARE_DELAY = 1_000L
class ProtectViewModel( @HiltViewModel
class ProtectViewModel @Inject constructor(
private val settings: AppSettings, private val settings: AppSettings,
private val protectHelper: AppProtectHelper, private val protectHelper: AppProtectHelper,
) : BaseViewModel() { ) : BaseViewModel() {
@ -42,4 +45,4 @@ class ProtectViewModel(
protectHelper.unlock() protectHelper.unlock()
onUnlockSuccess.call(Unit) onUnlockSuccess.call(Unit)
} }
} }

@ -1,31 +0,0 @@
package org.koitharu.kotatsu.reader
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.reader.ui.PageSaveHelper
import org.koitharu.kotatsu.reader.ui.ReaderViewModel
val readerModule
get() = module {
factory { MangaDataRepository(get()) }
single { PagesCache(get()) }
factory { PageSaveHelper(get(), androidContext()) }
viewModel { params ->
ReaderViewModel(
intent = params[0],
initialState = params[1],
preselectedBranch = params[2],
dataRepository = get(),
historyRepository = get(),
settings = get(),
pageSaveHelper = get(),
bookmarksRepository = get(),
)
}
}

@ -10,7 +10,9 @@ import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
private const val PAGES_TRIM_THRESHOLD = 120 private const val PAGES_TRIM_THRESHOLD = 120
class ChaptersLoader { class ChaptersLoader(
private val mangaRepositoryFactory: MangaRepository.Factory,
) {
val chapters = LongSparseArray<MangaChapter>() val chapters = LongSparseArray<MangaChapter>()
private val chapterPages = ChapterPages() private val chapterPages = ChapterPages()
@ -62,7 +64,7 @@ class ChaptersLoader {
private suspend fun loadChapter(manga: Manga, chapterId: Long): List<ReaderPage> { private suspend fun loadChapter(manga: Manga, chapterId: Long): List<ReaderPage> {
val chapter = checkNotNull(chapters[chapterId]) { "Requested chapter not found" } val chapter = checkNotNull(chapters[chapterId]) { "Requested chapter not found" }
val repo = MangaRepository(manga.source) val repo = mangaRepositoryFactory.create(manga.source)
return repo.getPages(chapter).mapIndexed { index, page -> return repo.getPages(chapter).mapIndexed { index, page ->
ReaderPage(page, index, chapterId) ReaderPage(page, index, chapterId)
} }

@ -6,6 +6,12 @@ import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import androidx.collection.LongSparseArray import androidx.collection.LongSparseArray
import androidx.collection.set import androidx.collection.set
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.File
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
import java.util.zip.ZipFile
import javax.inject.Inject
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -14,8 +20,6 @@ import kotlinx.coroutines.sync.withLock
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okio.Closeable import okio.Closeable
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koitharu.kotatsu.core.network.CommonHeaders import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
@ -27,26 +31,25 @@ import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.utils.ext.connectivityManager import org.koitharu.kotatsu.utils.ext.connectivityManager
import org.koitharu.kotatsu.utils.progress.ProgressDeferred import org.koitharu.kotatsu.utils.progress.ProgressDeferred
import java.io.File
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
import java.util.zip.ZipFile
private const val PROGRESS_UNDEFINED = -1f private const val PROGRESS_UNDEFINED = -1f
private const val PREFETCH_LIMIT_DEFAULT = 10 private const val PREFETCH_LIMIT_DEFAULT = 10
class PageLoader : KoinComponent, Closeable { class PageLoader @Inject constructor(
private val okHttp: OkHttpClient,
private val cache: PagesCache,
private val settings: AppSettings,
@ApplicationContext context: Context,
private val mangaRepositoryFactory: MangaRepository.Factory,
) : Closeable {
val loaderScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) val loaderScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val okHttp = get<OkHttpClient>() private val connectivityManager = context.connectivityManager
private val cache = get<PagesCache>()
private val settings = get<AppSettings>()
private val connectivityManager = get<Context>().connectivityManager
private val tasks = LongSparseArray<ProgressDeferred<File, Float>>() private val tasks = LongSparseArray<ProgressDeferred<File, Float>>()
private val convertLock = Mutex() private val convertLock = Mutex()
private var repository: MangaRepository? = null private var repository: MangaRepository? = null
private var prefetchQueue = LinkedList<MangaPage>() private val prefetchQueue = LinkedList<MangaPage>()
private val counter = AtomicInteger(0) private val counter = AtomicInteger(0)
private var prefetchQueueLimit = PREFETCH_LIMIT_DEFAULT // TODO adaptive private var prefetchQueueLimit = PREFETCH_LIMIT_DEFAULT // TODO adaptive
private val emptyProgressFlow: StateFlow<Float> = MutableStateFlow(-1f) private val emptyProgressFlow: StateFlow<Float> = MutableStateFlow(-1f)
@ -150,7 +153,7 @@ class PageLoader : KoinComponent, Closeable {
return if (result != null && result.source == source) { return if (result != null && result.source == source) {
result result
} else { } else {
MangaRepository(source).also { repository = it } mangaRepositoryFactory.create(source).also { repository = it }
} }
} }
@ -194,4 +197,4 @@ class PageLoader : KoinComponent, Closeable {
val deferred = CompletableDeferred(file) val deferred = CompletableDeferred(file)
return ProgressDeferred(deferred, emptyProgressFlow) return ProgressDeferred(deferred, emptyProgressFlow)
} }
} }

@ -5,8 +5,7 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import kotlin.math.roundToInt import dagger.hilt.android.AndroidEntryPoint
import org.koin.android.ext.android.get
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseBottomSheet import org.koitharu.kotatsu.base.ui.BaseBottomSheet
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
@ -19,9 +18,15 @@ import org.koitharu.kotatsu.details.ui.model.toListItem
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.utils.RecyclerViewScrollCallback import org.koitharu.kotatsu.utils.RecyclerViewScrollCallback
import org.koitharu.kotatsu.utils.ext.withArgs import org.koitharu.kotatsu.utils.ext.withArgs
import javax.inject.Inject
import kotlin.math.roundToInt
@AndroidEntryPoint
class ChaptersBottomSheet : BaseBottomSheet<SheetChaptersBinding>(), OnListItemClickListener<ChapterListItem> { class ChaptersBottomSheet : BaseBottomSheet<SheetChaptersBinding>(), OnListItemClickListener<ChapterListItem> {
@Inject
lateinit var settings: AppSettings
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetChaptersBinding { override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetChaptersBinding {
return SheetChaptersBinding.inflate(inflater, container, false) return SheetChaptersBinding.inflate(inflater, container, false)
} }
@ -35,7 +40,7 @@ class ChaptersBottomSheet : BaseBottomSheet<SheetChaptersBinding>(), OnListItemC
} }
val currentId = requireArguments().getLong(ARG_CURRENT_ID, 0L) val currentId = requireArguments().getLong(ARG_CURRENT_ID, 0L)
val currentPosition = chapters.indexOfFirst { it.id == currentId } val currentPosition = chapters.indexOfFirst { it.id == currentId }
val dateFormat = get<AppSettings>().getDateFormat() val dateFormat = settings.getDateFormat()
val items = chapters.mapIndexed { index, chapter -> val items = chapters.mapIndexed { index, chapter ->
chapter.toListItem( chapter.toListItem(
isCurrent = index == currentPosition, isCurrent = index == currentPosition,

@ -4,27 +4,27 @@ import android.content.Context
import android.net.Uri import android.net.Uri
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.File
import javax.inject.Inject
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okio.IOException import okio.IOException
import org.koitharu.kotatsu.base.domain.MangaUtils import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.toFileNameSafe import org.koitharu.kotatsu.parsers.util.toFileNameSafe
import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import java.io.File
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
private const val MAX_FILENAME_LENGTH = 10 private const val MAX_FILENAME_LENGTH = 10
private const val EXTENSION_FALLBACK = "png" private const val EXTENSION_FALLBACK = "png"
class PageSaveHelper( class PageSaveHelper @Inject constructor(
private val cache: PagesCache, @ApplicationContext context: Context,
context: Context,
) { ) {
private var continuation: Continuation<Uri>? = null private var continuation: Continuation<Uri>? = null
@ -65,7 +65,7 @@ class PageSaveHelper(
var extension = name.substringAfterLast('.', "") var extension = name.substringAfterLast('.', "")
name = name.substringBeforeLast('.') name = name.substringBeforeLast('.')
if (extension.length !in 2..4) { if (extension.length !in 2..4) {
val mimeType = MangaUtils.getImageMimeType(file) val mimeType = MangaDataRepository.getImageMimeType(file)
extension = if (mimeType != null) { extension = if (mimeType != null) {
MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) ?: EXTENSION_FALLBACK MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) ?: EXTENSION_FALLBACK
} else { } else {
@ -74,4 +74,4 @@ class PageSaveHelper(
} }
return name.toFileNameSafe().take(MAX_FILENAME_LENGTH) + "." + extension return name.toFileNameSafe().take(MAX_FILENAME_LENGTH) + "." + extension
} }
} }

@ -6,7 +6,6 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.* import android.view.*
import androidx.activity.result.ActivityResultCallback
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.OnApplyWindowInsetsListener import androidx.core.view.OnApplyWindowInsetsListener
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
@ -18,12 +17,12 @@ import androidx.transition.TransitionManager
import androidx.transition.TransitionSet import androidx.transition.TransitionSet
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaIntent import org.koitharu.kotatsu.base.domain.MangaIntent
@ -44,8 +43,8 @@ import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.utils.GridTouchHelper import org.koitharu.kotatsu.utils.GridTouchHelper
import org.koitharu.kotatsu.utils.ShareHelper import org.koitharu.kotatsu.utils.ShareHelper
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
import java.util.concurrent.TimeUnit
@AndroidEntryPoint
class ReaderActivity : class ReaderActivity :
BaseFullscreenActivity<ActivityReaderBinding>(), BaseFullscreenActivity<ActivityReaderBinding>(),
ChaptersBottomSheet.OnChapterChangeListener, ChaptersBottomSheet.OnChapterChangeListener,
@ -55,11 +54,14 @@ class ReaderActivity :
ReaderControlDelegate.OnInteractionListener, ReaderControlDelegate.OnInteractionListener,
OnApplyWindowInsetsListener { OnApplyWindowInsetsListener {
private val viewModel by viewModel<ReaderViewModel> { @Inject
parametersOf( lateinit var viewModelFactory: ReaderViewModel.Factory
MangaIntent(intent),
intent?.getParcelableExtra<ReaderState>(EXTRA_STATE), val viewModel by assistedViewModels {
intent?.getStringExtra(EXTRA_BRANCH), viewModelFactory.create(
intent = MangaIntent(intent),
initialState = intent?.getParcelableExtra(EXTRA_STATE),
preselectedBranch = intent?.getStringExtra(EXTRA_BRANCH),
) )
} }
@ -75,7 +77,7 @@ class ReaderActivity :
readerManager = ReaderManager(supportFragmentManager, R.id.container) readerManager = ReaderManager(supportFragmentManager, R.id.container)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
touchHelper = GridTouchHelper(this, this) touchHelper = GridTouchHelper(this, this)
controlDelegate = ReaderControlDelegate(lifecycleScope, get(), this) controlDelegate = ReaderControlDelegate(lifecycleScope, settings, this)
binding.toolbarBottom.setOnMenuItemClickListener(::onOptionsItemSelected) binding.toolbarBottom.setOnMenuItemClickListener(::onOptionsItemSelected)
binding.slider.setLabelFormatter(PageLabelFormatter()) binding.slider.setLabelFormatter(PageLabelFormatter())
ReaderSliderListener(this, viewModel).attachToSlider(binding.slider) ReaderSliderListener(this, viewModel).attachToSlider(binding.slider)
@ -121,7 +123,7 @@ class ReaderActivity :
ChaptersBottomSheet.show( ChaptersBottomSheet.show(
supportFragmentManager, supportFragmentManager,
viewModel.manga?.chapters.orEmpty(), viewModel.manga?.chapters.orEmpty(),
viewModel.getCurrentState()?.chapterId ?: 0L viewModel.getCurrentState()?.chapterId ?: 0L,
) )
} }
R.id.action_pages_thumbs -> { R.id.action_pages_thumbs -> {
@ -284,12 +286,12 @@ class ReaderActivity :
binding.appbarTop.updatePadding( binding.appbarTop.updatePadding(
top = systemBars.top, top = systemBars.top,
right = systemBars.right, right = systemBars.right,
left = systemBars.left left = systemBars.left,
) )
binding.appbarBottom?.updatePadding( binding.appbarBottom?.updatePadding(
bottom = systemBars.bottom, bottom = systemBars.bottom,
right = systemBars.right, right = systemBars.right,
left = systemBars.left left = systemBars.left,
) )
return WindowInsetsCompat.Builder(insets) return WindowInsetsCompat.Builder(insets)
.setInsets(WindowInsetsCompat.Type.systemBars(), Insets.NONE) .setInsets(WindowInsetsCompat.Type.systemBars(), Insets.NONE)

@ -7,12 +7,16 @@ import androidx.annotation.AnyThread
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import java.util.*
import javax.inject.Provider
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.base.domain.MangaIntent import org.koitharu.kotatsu.base.domain.MangaIntent
import org.koitharu.kotatsu.base.domain.MangaUtils
import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
@ -33,20 +37,21 @@ import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.processLifecycleScope import org.koitharu.kotatsu.utils.ext.processLifecycleScope
import org.koitharu.kotatsu.utils.ext.requireValue import org.koitharu.kotatsu.utils.ext.requireValue
import java.util.*
private const val BOUNDS_PAGE_OFFSET = 2 private const val BOUNDS_PAGE_OFFSET = 2
private const val PREFETCH_LIMIT = 10 private const val PREFETCH_LIMIT = 10
class ReaderViewModel( class ReaderViewModel @AssistedInject constructor(
private val intent: MangaIntent, @Assisted private val intent: MangaIntent,
initialState: ReaderState?, @Assisted initialState: ReaderState?,
private val preselectedBranch: String?, @Assisted private val preselectedBranch: String?,
private val mangaRepositoryFactory: MangaRepository.Factory,
private val dataRepository: MangaDataRepository, private val dataRepository: MangaDataRepository,
private val historyRepository: HistoryRepository, private val historyRepository: HistoryRepository,
private val bookmarksRepository: BookmarksRepository, private val bookmarksRepository: BookmarksRepository,
private val settings: AppSettings, private val settings: AppSettings,
private val pageSaveHelper: PageSaveHelper, private val pageSaveHelper: PageSaveHelper,
pageLoaderFactory: Provider<PageLoader>,
) : BaseViewModel() { ) : BaseViewModel() {
private var loadingJob: Job? = null private var loadingJob: Job? = null
@ -57,8 +62,8 @@ class ReaderViewModel(
private val chapters: LongSparseArray<MangaChapter> private val chapters: LongSparseArray<MangaChapter>
get() = chaptersLoader.chapters get() = chaptersLoader.chapters
val pageLoader = PageLoader() val pageLoader = pageLoaderFactory.get()
private val chaptersLoader = ChaptersLoader() private val chaptersLoader = ChaptersLoader(mangaRepositoryFactory)
val readerMode = MutableLiveData<ReaderMode>() val readerMode = MutableLiveData<ReaderMode>()
val onPageSaved = SingleLiveEvent<Uri?>() val onPageSaved = SingleLiveEvent<Uri?>()
@ -72,7 +77,7 @@ class ReaderViewModel(
val readerAnimation = settings.observeAsLiveData( val readerAnimation = settings.observeAsLiveData(
context = viewModelScope.coroutineContext + Dispatchers.Default, context = viewModelScope.coroutineContext + Dispatchers.Default,
key = AppSettings.KEY_READER_ANIMATION, key = AppSettings.KEY_READER_ANIMATION,
valueProducer = { readerAnimation } valueProducer = { readerAnimation },
) )
val isScreenshotsBlockEnabled = combine( val isScreenshotsBlockEnabled = combine(
@ -115,12 +120,12 @@ class ReaderViewModel(
val manga = checkNotNull(mangaData.value) val manga = checkNotNull(mangaData.value)
dataRepository.savePreferences( dataRepository.savePreferences(
manga = manga, manga = manga,
mode = newMode mode = newMode,
) )
readerMode.value = newMode readerMode.value = newMode
content.value?.run { content.value?.run {
content.value = copy( content.value = copy(
state = getCurrentState() state = getCurrentState(),
) )
} }
} }
@ -253,7 +258,7 @@ class ReaderViewModel(
loadingJob = launchLoadingJob(Dispatchers.Default) { loadingJob = launchLoadingJob(Dispatchers.Default) {
var manga = dataRepository.resolveIntent(intent) ?: throw NotFoundException("Cannot find manga", "") var manga = dataRepository.resolveIntent(intent) ?: throw NotFoundException("Cannot find manga", "")
mangaData.value = manga mangaData.value = manga
val repo = MangaRepository(manga.source) val repo = mangaRepositoryFactory.create(manga.source)
manga = repo.getDetails(manga) manga = repo.getDetails(manga)
manga.chapters?.forEach { manga.chapters?.forEach {
chapters.put(it.id, it) chapters.put(it.id, it)
@ -317,7 +322,7 @@ class ReaderViewModel(
?: error("There are no chapters in this manga") ?: error("There are no chapters in this manga")
val pages = repo.getPages(chapter) val pages = repo.getPages(chapter)
return runCatching { return runCatching {
val isWebtoon = MangaUtils.determineMangaIsWebtoon(pages) val isWebtoon = dataRepository.determineMangaIsWebtoon(repo, pages)
if (isWebtoon) ReaderMode.WEBTOON else defaultMode if (isWebtoon) ReaderMode.WEBTOON else defaultMode
}.onSuccess { }.onSuccess {
dataRepository.savePreferences(manga, it) dataRepository.savePreferences(manga, it)
@ -353,6 +358,16 @@ class ReaderViewModel(
val ppc = 1f / chaptersCount val ppc = 1f / chaptersCount
return ppc * chapterIndex + ppc * pagePercent return ppc * chapterIndex + ppc * pagePercent
} }
@AssistedFactory
interface Factory {
fun create(
intent: MangaIntent,
initialState: ReaderState?,
preselectedBranch: String?,
): ReaderViewModel
}
} }
/** /**

@ -8,10 +8,10 @@ import android.view.ViewGroup
import androidx.activity.result.ActivityResultCallback import androidx.activity.result.ActivityResultCallback
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.flowWithLifecycle
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseBottomSheet import org.koitharu.kotatsu.base.ui.BaseBottomSheet
import org.koitharu.kotatsu.base.ui.widgets.CheckableButtonGroup import org.koitharu.kotatsu.base.ui.widgets.CheckableButtonGroup
@ -30,7 +30,7 @@ class ReaderConfigBottomSheet :
ActivityResultCallback<Uri?>, ActivityResultCallback<Uri?>,
View.OnClickListener { View.OnClickListener {
private val viewModel by sharedViewModel<ReaderViewModel>() private val viewModel by activityViewModels<ReaderViewModel>()
private val savePageRequest = registerForActivityResult(PageSaveContract(), this) private val savePageRequest = registerForActivityResult(PageSaveContract(), this)
private var orientationHelper: ScreenOrientationHelper? = null private var orientationHelper: ScreenOrientationHelper? = null
private lateinit var mode: ReaderMode private lateinit var mode: ReaderMode

@ -3,8 +3,8 @@ package org.koitharu.kotatsu.reader.ui.pager
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.fragment.app.activityViewModels
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.reader.ui.ReaderViewModel import org.koitharu.kotatsu.reader.ui.ReaderViewModel
@ -13,7 +13,7 @@ private const val KEY_STATE = "state"
abstract class BaseReader<B : ViewBinding> : BaseFragment<B>() { abstract class BaseReader<B : ViewBinding> : BaseFragment<B>() {
protected val viewModel by sharedViewModel<ReaderViewModel>() protected val viewModel by activityViewModels<ReaderViewModel>()
private var stateToSave: ReaderState? = null private var stateToSave: ReaderState? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -53,4 +53,4 @@ abstract class BaseReader<B : ViewBinding> : BaseFragment<B>() {
abstract fun getCurrentState(): ReaderState? abstract fun getCurrentState(): ReaderState?
protected abstract fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) protected abstract fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?)
} }

@ -6,9 +6,11 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.children import androidx.core.view.children
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
import kotlinx.coroutines.async import kotlinx.coroutines.async
import org.koin.android.ext.android.get import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding
import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.reader.ui.pager.BaseReader import org.koitharu.kotatsu.reader.ui.pager.BaseReader
@ -20,19 +22,23 @@ import org.koitharu.kotatsu.utils.ext.recyclerView
import org.koitharu.kotatsu.utils.ext.resetTransformations import org.koitharu.kotatsu.utils.ext.resetTransformations
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
@AndroidEntryPoint
class ReversedReaderFragment : BaseReader<FragmentReaderStandardBinding>() { class ReversedReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
@Inject
lateinit var settings: AppSettings
private var pagerAdapter: ReversedPagesAdapter? = null private var pagerAdapter: ReversedPagesAdapter? = null
override fun onInflateView( override fun onInflateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup? container: ViewGroup?,
) = FragmentReaderStandardBinding.inflate(inflater, container, false) ) = FragmentReaderStandardBinding.inflate(inflater, container, false)
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
pagerAdapter = ReversedPagesAdapter(viewModel.pageLoader, get(), exceptionResolver) pagerAdapter = ReversedPagesAdapter(viewModel.pageLoader, settings, exceptionResolver)
with(binding.pager) { with(binding.pager) {
adapter = pagerAdapter adapter = pagerAdapter
offscreenPageLimit = 2 offscreenPageLimit = 2
@ -67,7 +73,7 @@ class ReversedReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
override fun switchPageTo(position: Int, smooth: Boolean) { override fun switchPageTo(position: Int, smooth: Boolean) {
binding.pager.setCurrentItem( binding.pager.setCurrentItem(
reversed(position), reversed(position),
smooth && (binding.pager.currentItem - position).absoluteValue < PagerReaderFragment.SMOOTH_SCROLL_LIMIT smooth && (binding.pager.currentItem - position).absoluteValue < PagerReaderFragment.SMOOTH_SCROLL_LIMIT,
) )
} }
@ -98,7 +104,7 @@ class ReversedReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
ReaderState( ReaderState(
chapterId = page.chapterId, chapterId = page.chapterId,
page = page.index, page = page.index,
scroll = 0 scroll = 0,
) )
} }
@ -109,4 +115,4 @@ class ReversedReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
private fun reversed(position: Int): Int { private fun reversed(position: Int): Int {
return ((pagerAdapter?.itemCount ?: 0) - position - 1).coerceAtLeast(0) return ((pagerAdapter?.itemCount ?: 0) - position - 1).coerceAtLeast(0)
} }
} }

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save