Merge branch 'feature/nextgen_dagger' into feature/nextgen

This commit is contained in:
Koitharu
2022-07-25 21:26:09 +03:00
181 changed files with 1750 additions and 1378 deletions

View File

@@ -5,85 +5,50 @@ import android.content.Context
import android.os.StrictMode
import androidx.appcompat.app.AppCompatDelegate
import androidx.fragment.app.strictmode.FragmentStrictMode
import androidx.hilt.work.HiltWorkerFactory
import androidx.room.InvalidationTracker
import androidx.work.Configuration
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject
import org.acra.ReportField
import org.acra.config.dialog
import org.acra.config.mailSender
import org.acra.data.StringFormat
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.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.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.domain.LocalMangaRepository
import org.koitharu.kotatsu.local.localModule
import org.koitharu.kotatsu.main.mainModule
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() {
super.onCreate()
if (BuildConfig.DEBUG) {
enableStrictMode()
}
initKoin()
AppCompatDelegate.setDefaultNightMode(get<AppSettings>().theme)
AppCompatDelegate.setDefaultNightMode(settings.theme)
setupActivityLifecycleCallbacks()
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?) {
super.attachBaseContext(base)
initAcra {
@@ -115,18 +80,21 @@ class KotatsuApp : Application() {
}
}
override fun getWorkManagerConfiguration(): Configuration {
return Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
}
private fun setupDatabaseObservers() {
val observers = getKoin().getAll<InvalidationTracker.Observer>()
val database = get<MangaDatabase>()
val tracker = database.invalidationTracker
observers.forEach {
databaseObservers.forEach {
tracker.addObserver(it)
}
}
private fun setupActivityLifecycleCallbacks() {
val callbacks = getKoin().getAll<ActivityLifecycleCallbacks>()
callbacks.forEach {
activityLifecycleCallbacks.forEach {
registerActivityLifecycleCallbacks(it)
}
}

View File

@@ -1,14 +1,35 @@
package org.koitharu.kotatsu.base.domain
import android.graphics.BitmapFactory
import android.net.Uri
import android.util.Size
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.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.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.util.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) {
val tags = manga.tags.toEntities()
@@ -18,8 +39,8 @@ class MangaDataRepository(private val db: MangaDatabase) {
db.preferencesDao.upsert(
MangaPrefsEntity(
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> {
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)
}
}
}

View File

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

View File

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

View File

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

View File

@@ -8,18 +8,21 @@ import androidx.core.graphics.Insets
import androidx.core.view.updatePadding
import androidx.preference.PreferenceFragmentCompat
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.WindowInsetsDelegate
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.settings.SettingsHeadersFragment
@AndroidEntryPoint
abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
PreferenceFragmentCompat(),
WindowInsetsDelegate.WindowInsetsListener,
RecyclerViewOwner {
protected val settings by inject<AppSettings>(mode = LazyThreadSafetyMode.NONE)
@Inject
lateinit var settings: AppSettings
@Suppress("LeakingThis")
protected val insetsDelegate = WindowInsetsDelegate(this)
@@ -48,7 +51,7 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
@CallSuper
override fun onWindowInsetsChanged(insets: Insets) {
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)
?: activity?.setTitle(title)
}
}
}

View File

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

View File

@@ -4,8 +4,11 @@ import android.app.Activity
import android.app.Application.ActivityLifecycleCallbacks
import android.os.Bundle
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>()
@@ -31,4 +34,4 @@ class ActivityRecreationHandle : ActivityLifecycleCallbacks {
val snapshot = activities.keys.toList()
snapshot.forEach { it.recreate() }
}
}
}

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.bookmarks.domain
import android.database.SQLException
import androidx.room.withTransaction
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
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.printStackTraceDebug
class BookmarksRepository(
class BookmarksRepository @Inject constructor(
private val db: MangaDatabase,
) {
@@ -86,4 +87,4 @@ class BookmarksRepository(
}
}
}
}
}

View File

@@ -6,10 +6,12 @@ import android.os.Bundle
import androidx.core.graphics.Insets
import androidx.core.view.updatePadding
import androidx.fragment.app.commit
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivityContainerBinding
@AndroidEntryPoint
class BookmarksActivity : BaseActivity<ActivityContainerBinding>() {
override fun onCreate(savedInstanceState: Bundle?) {
@@ -29,7 +31,7 @@ class BookmarksActivity : BaseActivity<ActivityContainerBinding>() {
with(binding.toolbar) {
updatePadding(
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)
}
}
}

View File

@@ -5,9 +5,11 @@ import android.view.*
import androidx.appcompat.view.ActionMode
import androidx.core.graphics.Insets
import androidx.core.view.updatePadding
import androidx.fragment.app.viewModels
import coil.ImageLoader
import com.google.android.material.snackbar.Snackbar
import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.viewModel
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.reverseAsync
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.scaleUpActivityOptionsOf
@AndroidEntryPoint
class BookmarksFragment :
BaseFragment<FragmentListSimpleBinding>(),
ListStateHolderListener,
OnListItemClickListener<Bookmark>,
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 selectionController: SectionedSelectionController<Manga>? = null
@@ -53,7 +59,7 @@ class BookmarksFragment :
)
adapter = BookmarksGroupAdapter(
lifecycleOwner = viewLifecycleOwner,
coil = get(),
coil = coil,
listener = this,
selectionController = checkNotNull(selectionController),
bookmarkClickListener = this,

View File

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

View File

@@ -11,21 +11,27 @@ import android.webkit.WebSettings
import androidx.core.view.isInvisible
import androidx.fragment.app.setFragmentResult
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.core.network.AndroidCookieJar
import org.koitharu.kotatsu.core.network.UserAgentInterceptor
import org.koitharu.kotatsu.databinding.FragmentCloudflareBinding
import org.koitharu.kotatsu.utils.ext.stringArgument
import org.koitharu.kotatsu.utils.ext.withArgs
@AndroidEntryPoint
class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), CloudFlareCallback {
private val url by stringArgument(ARG_URL)
private val pendingResult = Bundle(1)
@Inject
lateinit var cookieJar: AndroidCookieJar
override fun onInflateView(
inflater: LayoutInflater,
container: ViewGroup?
container: ViewGroup?,
) = FragmentCloudflareBinding.inflate(inflater, container, false)
@SuppressLint("SetJavaScriptEnabled")
@@ -38,7 +44,7 @@ class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), Cloud
databaseEnabled = true
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)
if (url.isNullOrEmpty()) {
dismissAllowingStateLoss()
@@ -90,4 +96,4 @@ class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), Cloud
putString(ARG_URL, url)
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,11 +3,14 @@ package org.koitharu.kotatsu.core.github
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.security.MessageDigest
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
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"
class AppUpdateRepository(
private val context: Context,
@Singleton
class AppUpdateRepository @Inject constructor(
@ApplicationContext private val context: Context,
private val okHttp: OkHttpClient,
) {

View File

@@ -1,13 +1,16 @@
package org.koitharu.kotatsu.core.network
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.CookieJar
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()
@@ -31,4 +34,4 @@ class AndroidCookieJar : CookieJar {
suspend fun clear() = suspendCoroutine<Boolean> { continuation ->
cookieManager.removeAllCookies(continuation::resume)
}
}
}

View File

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

View File

@@ -13,6 +13,9 @@ import androidx.core.graphics.drawable.IconCompat
import androidx.room.InvalidationTracker
import coil.ImageLoader
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.Job
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.requireBitmap
class ShortcutsUpdater(
private val context: Context,
@Singleton
class ShortcutsUpdater @Inject constructor(
@ApplicationContext private val context: Context,
private val coil: ImageLoader,
private val historyRepository: HistoryRepository,
private val mangaRepository: MangaDataRepository,
@@ -37,6 +41,9 @@ class ShortcutsUpdater(
private var shortcutsUpdateJob: Job? = null
override fun onInvalidated(tables: MutableSet<String>) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
return
}
val prevJob = shortcutsUpdateJob
shortcutsUpdateJob = processLifecycleScope.launch(Dispatchers.Default) {
prevJob?.join()
@@ -48,7 +55,7 @@ class ShortcutsUpdater(
return ShortcutManagerCompat.requestPinShortcut(
context,
buildShortcutInfo(manga).build(),
null
null,
)
}
@@ -73,12 +80,12 @@ class ShortcutsUpdater(
ImageRequest.Builder(context)
.data(manga.coverUrl)
.size(iconSize.width, iconSize.height)
.build()
.build(),
).requireBitmap()
ThumbnailUtils.extractThumbnail(bmp, iconSize.width, iconSize.height, 0)
}.fold(
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)
return ShortcutInfoCompat.Builder(context, manga.id.toString())
@@ -87,7 +94,7 @@ class ShortcutsUpdater(
.setIcon(icon)
.setIntent(
ReaderActivity.newIntent(context, manga.id)
.setAction(ReaderActivity.ACTION_MANGA_READ)
.setAction(ReaderActivity.ACTION_MANGA_READ),
)
}
@@ -102,4 +109,4 @@ class ShortcutsUpdater(
}
}
}
}
}

View File

@@ -5,6 +5,12 @@ import android.content.Context
import android.util.Base64
import android.webkit.WebView
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.withContext
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.model.MangaSource
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 cookieJar: AndroidCookieJar,
private val androidContext: Context,
@ApplicationContext private val androidContext: Context,
) : MangaLoaderContext() {
@SuppressLint("SetJavaScriptEnabled")
@@ -50,4 +54,4 @@ class MangaLoaderContextImpl(
override fun getPreferredLocales(): List<Locale> {
return LocaleListCompat.getAdjustedDefault().toList()
}
}
}

View File

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

View File

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

View File

@@ -10,6 +10,13 @@ import androidx.collection.arraySetOf
import androidx.core.content.edit
import androidx.preference.PreferenceManager
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.core.model.ZoomMode
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.putEnumValue
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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,11 @@ import android.net.ConnectivityManager
import android.webkit.MimeTypeMap
import coil.ImageLoader
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.flow.MutableStateFlow
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.waitForNetwork
import org.koitharu.kotatsu.utils.progress.ProgressJob
import java.io.File
private const val MAX_DOWNLOAD_ATTEMPTS = 3
private const val DOWNLOAD_ERROR_DELAY = 500L
private const val SLOWDOWN_DELAY = 200L
class DownloadManager(
private val coroutineScope: CoroutineScope,
private val context: Context,
class DownloadManager @AssistedInject constructor(
@Assisted private val coroutineScope: CoroutineScope,
@ApplicationContext private val context: Context,
private val imageLoader: ImageLoader,
private val okHttp: OkHttpClient,
private val cache: PagesCache,
private val localMangaRepository: LocalMangaRepository,
private val settings: AppSettings,
private val mangaRepositoryFactory: MangaRepository.Factory,
) {
private val connectivityManager = context.getSystemService(
Context.CONNECTIVITY_SERVICE
Context.CONNECTIVITY_SERVICE,
) as ConnectivityManager
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(
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)
@@ -59,7 +64,7 @@ class DownloadManager(
startId: Int,
): ProgressJob<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)
return ProgressJob(job, stateFlow)
@@ -71,7 +76,8 @@ class DownloadManager(
outState: MutableStateFlow<DownloadState>,
startId: Int,
): Job = coroutineScope.launch(Dispatchers.Default + errorStateHandler(outState)) {
@Suppress("NAME_SHADOWING") var manga = manga
@Suppress("NAME_SHADOWING")
var manga = manga
val chaptersIdsSet = chaptersIds?.toMutableSet()
val cover = loadCover(manga)
outState.value = DownloadState.Queued(startId, manga, cover)
@@ -87,7 +93,7 @@ class DownloadManager(
if (manga.source == MangaSource.LOCAL) {
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)
val data = if (manga.chapters.isNullOrEmpty()) repo.getDetails(manga) else manga
output = CbzMangaOutput.get(destination, data)
@@ -100,7 +106,7 @@ class DownloadManager(
data.chapters
} else {
data.chapters?.filter { x -> chaptersIdsSet.remove(x.id) }
}
},
) { "Chapters list must not be null" }
check(chapters.isNotEmpty()) { "Chapters list must not be empty" }
check(chaptersIdsSet.isNullOrEmpty()) {
@@ -134,7 +140,9 @@ class DownloadManager(
}
outState.value = DownloadState.Progress(
startId, data, cover,
startId,
data,
cover,
totalChapters = chapters.size,
currentChapter = chapterIndex,
totalPages = pages.size,
@@ -203,27 +211,13 @@ class DownloadManager(
.data(manga.coverUrl)
.referer(manga.publicUrl)
.size(coverWidth, coverHeight)
.build()
.build(),
).drawable
}.getOrNull()
class Factory(
private val context: Context,
private val imageLoader: ImageLoader,
private val okHttp: OkHttpClient,
private val cache: PagesCache,
private val localMangaRepository: LocalMangaRepository,
private val settings: AppSettings,
) {
@AssistedFactory
interface Factory {
fun create(coroutineScope: CoroutineScope) = DownloadManager(
coroutineScope = coroutineScope,
context = context,
imageLoader = imageLoader,
okHttp = okHttp,
cache = cache,
localMangaRepository = localMangaRepository,
settings = settings,
)
fun create(coroutineScope: CoroutineScope): DownloadManager
}
}
}

View File

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

View File

@@ -11,32 +11,34 @@ import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
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.launch
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.R
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.prefs.AppSettings
import org.koitharu.kotatsu.download.domain.DownloadManager
import org.koitharu.kotatsu.download.domain.DownloadState
import org.koitharu.kotatsu.download.domain.WakeLockNode
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.progress.ProgressJob
import org.koitharu.kotatsu.utils.progress.TimeLeftEstimator
import java.util.concurrent.TimeUnit
@AndroidEntryPoint
class DownloadService : BaseService() {
private lateinit var downloadManager: DownloadManager
private lateinit var notificationSwitcher: ForegroundNotificationSwitcher
@Inject
lateinit var downloadManagerFactory: DownloadManager.Factory
private val jobs = LinkedHashMap<Int, ProgressJob<DownloadState>>()
private val jobCount = MutableStateFlow(0)
private val controlReceiver = ControlReceiver()
@@ -48,7 +50,7 @@ class DownloadService : BaseService() {
notificationSwitcher = ForegroundNotificationSwitcher(this)
val wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager)
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "kotatsu:downloading")
downloadManager = get<DownloadManager.Factory>().create(
downloadManager = downloadManagerFactory.create(
coroutineScope = lifecycleScope + WakeLockNode(wakeLock, TimeUnit.HOURS.toMillis(1)),
)
DownloadNotification.createChannel(this)
@@ -122,7 +124,7 @@ class DownloadService : BaseService() {
(job.progressValue as? DownloadState.Done)?.let {
sendBroadcast(
Intent(ACTION_DOWNLOAD_COMPLETE)
.putExtra(EXTRA_MANGA, ParcelableManga(it.localManga, withChapters = false))
.putExtra(EXTRA_MANGA, ParcelableManga(it.localManga, withChapters = false)),
)
}
notificationSwitcher.detach(
@@ -131,7 +133,7 @@ class DownloadService : BaseService() {
null
} else {
notification.create(job.progressValue, -1L)
}
},
)
stopSelf(startId)
}
@@ -182,27 +184,23 @@ class DownloadService : BaseService() {
if (chaptersIds?.isEmpty() == true) {
return
}
confirmDataTransfer(context) {
val intent = Intent(context, DownloadService::class.java)
intent.putExtra(EXTRA_MANGA, ParcelableManga(manga, withChapters = false))
if (chaptersIds != null) {
intent.putExtra(EXTRA_CHAPTERS_IDS, chaptersIds.toLongArray())
}
ContextCompat.startForegroundService(context, intent)
Toast.makeText(context, R.string.manga_downloading_, Toast.LENGTH_SHORT).show()
val intent = Intent(context, DownloadService::class.java)
intent.putExtra(EXTRA_MANGA, ParcelableManga(manga, withChapters = false))
if (chaptersIds != null) {
intent.putExtra(EXTRA_CHAPTERS_IDS, chaptersIds.toLongArray())
}
ContextCompat.startForegroundService(context, intent)
Toast.makeText(context, R.string.manga_downloading_, Toast.LENGTH_SHORT).show()
}
fun start(context: Context, manga: Collection<Manga>) {
if (manga.isEmpty()) {
return
}
confirmDataTransfer(context) {
for (item in manga) {
val intent = Intent(context, DownloadService::class.java)
intent.putExtra(EXTRA_MANGA, ParcelableManga(item, withChapters = false))
ContextCompat.startForegroundService(context, intent)
}
for (item in manga) {
val intent = Intent(context, DownloadService::class.java)
intent.putExtra(EXTRA_MANGA, ParcelableManga(item, withChapters = false))
ContextCompat.startForegroundService(context, intent)
}
}
@@ -225,24 +223,5 @@ class DownloadService : BaseService() {
}
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()
}
}
}
}
}

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,8 @@ package org.koitharu.kotatsu.explore.ui
import androidx.lifecycle.LiveData
import androidx.lifecycle.asFlow
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
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.ext.asLiveDataDistinct
class ExploreViewModel(
@HiltViewModel
class ExploreViewModel @Inject constructor(
private val settings: AppSettings,
private val exploreRepository: ExploreRepository,
) : BaseViewModel() {
@@ -66,4 +69,4 @@ class ExploreViewModel(
}
return result
}
}
}

View File

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

View File

@@ -1,6 +1,8 @@
package org.koitharu.kotatsu.favourites.domain
import androidx.room.withTransaction
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.flow.*
import org.koitharu.kotatsu.base.domain.ReversibleHandle
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.utils.ext.mapItems
class FavouritesRepository(
@Singleton
class FavouritesRepository @Inject constructor(
private val db: MangaDatabase,
private val channels: TrackerNotificationChannels,
) {

View File

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

View File

@@ -9,15 +9,17 @@ import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.activity.viewModels
import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader
import com.google.android.material.snackbar.Snackbar
import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.viewModel
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
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.scaleUpActivityOptionsOf
@AndroidEntryPoint
class FavouriteCategoriesActivity :
BaseActivity<ActivityCategoriesBinding>(),
FavouriteCategoriesListListener,
View.OnClickListener,
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 selectionController: ListSelectionController
@@ -49,7 +55,7 @@ class FavouriteCategoriesActivity :
super.onCreate(savedInstanceState)
setContentView(ActivityCategoriesBinding.inflate(layoutInflater))
supportActionBar?.setDisplayHomeAsUpEnabled(true)
adapter = CategoriesAdapter(get(), this, this, this)
adapter = CategoriesAdapter(coil, this, this, this)
selectionController = ListSelectionController(
activity = this,
decoration = CategoriesSelectionDecoration(this),
@@ -169,7 +175,8 @@ class FavouriteCategoriesActivity :
}
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

View File

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

View File

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

View File

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

View File

@@ -8,8 +8,7 @@ import android.view.ViewGroup
import android.widget.Toast
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.FragmentManager
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
import javax.inject.Inject
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
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.model.MangaCategoryItem
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.withArgs
@@ -28,8 +28,13 @@ class FavouriteCategoriesBottomSheet :
View.OnClickListener,
Toolbar.OnMenuItemClickListener {
private val viewModel by viewModel<MangaCategoriesViewModel> {
parametersOf(requireNotNull(arguments?.getParcelableArrayList<ParcelableManga>(KEY_MANGA_LIST)).map { it.manga })
@Inject
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

View File

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

View File

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

View File

@@ -3,6 +3,9 @@ package org.koitharu.kotatsu.favourites.ui.list
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.catch
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.utils.ext.asLiveDataDistinct
class FavouritesListViewModel(
private val categoryId: Long,
class FavouritesListViewModel @AssistedInject constructor(
@Assisted private val categoryId: Long,
private val repository: FavouritesRepository,
private val trackingRepository: TrackingRepository,
private val historyRepository: HistoryRepository,
@@ -121,4 +124,10 @@ class FavouritesListViewModel(
PROGRESS_NONE
}
}
@AssistedFactory
interface Factory {
fun create(categoryId: Long): FavouritesListViewModel
}
}

View File

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

View File

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

View File

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

View File

@@ -5,17 +5,18 @@ import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.appcompat.view.ActionMode
import org.koin.android.ext.android.get
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.base.ui.list.ListSelectionController
import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.utils.ext.addMenuProvider
@AndroidEntryPoint
class HistoryListFragment : MangaListFragment() {
override val viewModel by viewModel<HistoryListViewModel>()
override val viewModel by viewModels<HistoryListViewModel>()
override val isSwipeRefreshEnabled = false
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 {

View File

@@ -2,6 +2,10 @@ package org.koitharu.kotatsu.history.ui
import androidx.lifecycle.MutableLiveData
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.flow.catch
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.daysDiff
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 settings: AppSettings,
private val trackingRepository: TrackingRepository,
@@ -39,7 +42,7 @@ class HistoryListViewModel(
override val content = combine(
repository.observeAllWithHistory(),
historyGrouping,
createListModeFlow()
createListModeFlow(),
) { list, grouped, mode ->
when {
list.isEmpty() -> listOf(
@@ -48,7 +51,7 @@ class HistoryListViewModel(
textPrimary = R.string.text_history_holder_primary,
textSecondary = R.string.text_history_holder_secondary,
actionStringRes = 0,
)
),
)
else -> mapList(list, grouped, mode)
}
@@ -87,7 +90,7 @@ class HistoryListViewModel(
private suspend fun mapList(
list: List<MangaWithHistory>,
grouped: Boolean,
mode: ListMode
mode: ListMode,
): List<ListModel> {
val result = ArrayList<ListModel>(if (grouped) (list.size * 1.4).toInt() else list.size + 1)
val showPercent = settings.isReadingIndicatorsEnabled

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.library.domain
import javax.inject.Inject
import kotlinx.coroutines.flow.*
import org.koitharu.kotatsu.core.db.MangaDatabase
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.parsers.model.Manga
class LibraryRepository(
class LibraryRepository @Inject constructor(
private val db: MangaDatabase,
) {

View File

@@ -6,14 +6,17 @@ import android.view.View
import android.view.ViewGroup
import androidx.core.graphics.Insets
import androidx.core.view.updatePadding
import androidx.fragment.app.viewModels
import coil.ImageLoader
import com.google.android.material.snackbar.Snackbar
import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.viewModel
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.reverseAsync
import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
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.details.ui.DetailsActivity
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.getDisplayMessage
@AndroidEntryPoint
class LibraryFragment :
BaseFragment<FragmentLibraryBinding>(),
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 selectionController: SectionedSelectionController<LibrarySectionModel>? = null
@@ -42,7 +52,7 @@ class LibraryFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val sizeResolver = ItemSizeResolver(resources, get())
val sizeResolver = ItemSizeResolver(resources, settings)
selectionController = SectionedSelectionController(
activity = requireActivity(),
owner = this,
@@ -50,7 +60,7 @@ class LibraryFragment :
)
adapter = LibraryAdapter(
lifecycleOwner = viewLifecycleOwner,
coil = get(),
coil = coil,
listener = this,
sizeResolver = sizeResolver,
selectionController = checkNotNull(selectionController),

View File

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

View File

@@ -6,19 +6,21 @@ import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
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.base.ui.BaseBottomSheet
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.databinding.SheetBaseBinding
@AndroidEntryPoint
class LibraryCategoriesConfigSheet :
BaseBottomSheet<SheetBaseBinding>(),
OnListItemClickListener<FavouriteCategory>,
View.OnClickListener {
private val viewModel by viewModel<LibraryCategoriesConfigViewModel>()
private val viewModel by viewModels<LibraryCategoriesConfigViewModel>()
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetBaseBinding {
return SheetBaseBinding.inflate(inflater, container, false)

View File

@@ -1,6 +1,8 @@
package org.koitharu.kotatsu.library.ui.config.categories
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
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.utils.ext.asLiveDataDistinct
class LibraryCategoriesConfigViewModel(
@HiltViewModel
class LibraryCategoriesConfigViewModel @Inject constructor(
private val favouritesRepository: FavouritesRepository,
) : BaseViewModel() {

View File

@@ -7,7 +7,8 @@ import android.view.ViewGroup
import androidx.fragment.app.FragmentManager
import com.google.android.material.slider.LabelFormatter
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.base.ui.BaseBottomSheet
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.progress.IntPercentLabelFormatter
@AndroidEntryPoint
class LibrarySizeBottomSheet :
BaseBottomSheet<SheetLibrarySizeBinding>(),
Slider.OnChangeListener,
View.OnClickListener {
private val settings by inject<AppSettings>(mode = LazyThreadSafetyMode.NONE)
@Inject
lateinit var settings: AppSettings
private var labelFormatter: LabelFormatter? = null
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetLibrarySizeBinding {

View File

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

View File

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

View File

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

View File

@@ -1,20 +1 @@
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()) }
}

View File

@@ -4,6 +4,10 @@ import android.content.ContentResolver
import android.content.Context
import android.os.StatFs
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.runInterruptible
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.utils.ext.computeSize
import org.koitharu.kotatsu.utils.ext.getStorageName
import java.io.File
private const val DIR_NAME = "manga"
private const val CACHE_DISK_PERCENTAGE = 0.02
private const val CACHE_SIZE_MIN: Long = 10 * 1024 * 1024 // 10MB
private const val CACHE_SIZE_MAX: Long = 250 * 1024 * 1024 // 250MB
class LocalStorageManager(
private val context: Context,
@Singleton
class LocalStorageManager @Inject constructor(
@ApplicationContext private val context: Context,
private val settings: AppSettings,
) {
@@ -131,4 +135,4 @@ class LocalStorageManager(
private fun File.isWriteable() = runCatching {
canWrite()
}.getOrDefault(false)
}
}

View File

@@ -2,15 +2,19 @@ package org.koitharu.kotatsu.local.data
import android.content.Context
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 org.koitharu.kotatsu.utils.FileSize
import org.koitharu.kotatsu.utils.ext.longHashCode
import org.koitharu.kotatsu.utils.ext.subdir
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 lruCache = createDiskLruCacheSafe(
@@ -70,4 +74,4 @@ private fun createDiskLruCacheSafe(dir: File, size: Long): DiskLruCache {
dir.mkdir()
DiskLruCache.create(dir, size)
}
}
}

View File

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

View File

@@ -8,7 +8,8 @@ import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
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.base.ui.CoroutineIntentService
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.parsers.model.Manga
@AndroidEntryPoint
class LocalChaptersRemoveService : CoroutineIntentService() {
private val localMangaRepository by inject<LocalMangaRepository>()
@Inject
lateinit var localMangaRepository: LocalMangaRepository
override suspend fun processIntent(intent: Intent?) {
val manga = intent?.getParcelableExtra<ParcelableManga>(EXTRA_MANGA)?.manga ?: return
@@ -28,7 +31,7 @@ class LocalChaptersRemoveService : CoroutineIntentService() {
localMangaRepository.deleteChapters(mangaWithChapters, chaptersIds)
sendBroadcast(
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)
}
@@ -77,4 +80,4 @@ class LocalChaptersRemoveService : CoroutineIntentService() {
ContextCompat.startForegroundService(context, intent)
}
}
}
}

View File

@@ -11,9 +11,9 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.view.ActionMode
import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.fragment.app.viewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.ListSelectionController
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>> {
override val viewModel by viewModel<LocalListViewModel>()
override val viewModel by viewModels<LocalListViewModel>()
private val importCall = registerForActivityResult(
ActivityResultContracts.OpenMultipleDocuments(),
this,

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
package org.koitharu.kotatsu.main.ui
import android.view.View
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback
import androidx.lifecycle.lifecycleScope
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.onEach
import kotlinx.coroutines.launch
import org.koin.android.ext.android.get
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.observeAsFlow
class ExitCallback(
private val activity: ComponentActivity,
private val activity: BaseActivity<*>,
private val snackbarHost: View,
) : OnBackPressedCallback(false) {
@@ -46,7 +45,7 @@ class ExitCallback(
}
private fun observeSettings() {
activity.get<AppSettings>()
activity.settings
.observeAsFlow(AppSettings.KEY_EXIT_CONFIRM) { isExitConfirmationEnabled }
.flowOn(Dispatchers.Default)
.onEach { isEnabled = it }

View File

@@ -6,6 +6,7 @@ import android.view.MenuItem
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
import androidx.activity.result.ActivityResultCallback
import androidx.activity.viewModels
import androidx.annotation.IdRes
import androidx.appcompat.view.ActionMode
import androidx.core.app.ActivityOptionsCompat
@@ -17,20 +18,19 @@ import androidx.fragment.app.FragmentTransaction
import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
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.LayoutParams.*
import com.google.android.material.navigation.NavigationBarView
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
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.base.ui.BaseActivity
import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner
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.details.ui.DetailsActivity
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.tools.ToolsFragment
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.work.TrackWorker
import org.koitharu.kotatsu.utils.VoiceInputContract
import org.koitharu.kotatsu.utils.ext.*
import com.google.android.material.R as materialR
private const val TAG_PRIMARY = "primary"
private const val TAG_SEARCH = "search"
@AndroidEntryPoint
class MainActivity :
BaseActivity<ActivityMainBinding>(),
AppBarOwner,
@@ -68,8 +67,8 @@ class MainActivity :
NavigationBarView.OnItemSelectedListener,
NavigationBarView.OnItemReselectedListener {
private val viewModel by viewModel<MainViewModel>()
private val searchSuggestionViewModel by viewModel<SearchSuggestionViewModel>()
private val viewModel by viewModels<MainViewModel>()
private val searchSuggestionViewModel by viewModels<SearchSuggestionViewModel>()
private val voiceInputLauncher = registerForActivityResult(VoiceInputContract(), VoiceInputCallback())
private lateinit var navBar: NavigationBarView
@@ -284,7 +283,8 @@ class MainActivity :
}
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) {
@@ -366,13 +366,12 @@ class MainActivity :
TrackWorker.setup(applicationContext)
SuggestionsWorker.setup(applicationContext)
}
val settings = get<AppSettings>()
when {
!settings.isSourcesSelected -> OnboardDialogFragment.showWelcome(supportFragmentManager)
settings.newSources.isNotEmpty() -> NewSourcesDialogFragment.show(supportFragmentManager)
}
yield()
get<SyncController>().requestFullSyncAndGc(get())
// TODO get<SyncController>().requestFullSyncAndGc(get())
}
}

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.main.ui
import android.util.SparseIntArray
import androidx.core.util.set
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
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.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import javax.inject.Inject
class MainViewModel(
@HiltViewModel
class MainViewModel @Inject constructor(
private val historyRepository: HistoryRepository,
private val settings: AppSettings,
private val appUpdateRepository: AppUpdateRepository,

View File

@@ -4,9 +4,12 @@ import android.app.Activity
import android.app.Application
import android.content.Intent
import android.os.Bundle
import javax.inject.Inject
import javax.inject.Singleton
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()
@@ -46,4 +49,4 @@ class AppProtectHelper(private val settings: AppSettings) : Application.Activity
private fun restoreLock() {
isUnlocked = settings.appPassword.isNullOrEmpty()
}
}
}

View File

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

View File

@@ -1,5 +1,7 @@
package org.koitharu.kotatsu.main.ui.protect
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
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
class ProtectViewModel(
@HiltViewModel
class ProtectViewModel @Inject constructor(
private val settings: AppSettings,
private val protectHelper: AppProtectHelper,
) : BaseViewModel() {
@@ -42,4 +45,4 @@ class ProtectViewModel(
protectHelper.unlock()
onUnlockSuccess.call(Unit)
}
}
}

View File

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

View File

@@ -10,7 +10,9 @@ import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
private const val PAGES_TRIM_THRESHOLD = 120
class ChaptersLoader {
class ChaptersLoader(
private val mangaRepositoryFactory: MangaRepository.Factory,
) {
val chapters = LongSparseArray<MangaChapter>()
private val chapterPages = ChapterPages()
@@ -62,7 +64,7 @@ class ChaptersLoader {
private suspend fun loadChapter(manga: Manga, chapterId: Long): List<ReaderPage> {
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 ->
ReaderPage(page, index, chapterId)
}

View File

@@ -6,6 +6,12 @@ import android.graphics.BitmapFactory
import android.net.Uri
import androidx.collection.LongSparseArray
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.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -14,8 +20,6 @@ import kotlinx.coroutines.sync.withLock
import okhttp3.OkHttpClient
import okhttp3.Request
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.parser.MangaRepository
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.utils.ext.connectivityManager
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 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)
private val okHttp = get<OkHttpClient>()
private val cache = get<PagesCache>()
private val settings = get<AppSettings>()
private val connectivityManager = get<Context>().connectivityManager
private val connectivityManager = context.connectivityManager
private val tasks = LongSparseArray<ProgressDeferred<File, Float>>()
private val convertLock = Mutex()
private var repository: MangaRepository? = null
private var prefetchQueue = LinkedList<MangaPage>()
private val prefetchQueue = LinkedList<MangaPage>()
private val counter = AtomicInteger(0)
private var prefetchQueueLimit = PREFETCH_LIMIT_DEFAULT // TODO adaptive
private val emptyProgressFlow: StateFlow<Float> = MutableStateFlow(-1f)
@@ -150,7 +153,7 @@ class PageLoader : KoinComponent, Closeable {
return if (result != null && result.source == source) {
result
} else {
MangaRepository(source).also { repository = it }
mangaRepositoryFactory.create(source).also { repository = it }
}
}
@@ -194,4 +197,4 @@ class PageLoader : KoinComponent, Closeable {
val deferred = CompletableDeferred(file)
return ProgressDeferred(deferred, emptyProgressFlow)
}
}
}

View File

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

View File

@@ -4,27 +4,27 @@ import android.content.Context
import android.net.Uri
import android.webkit.MimeTypeMap
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.runInterruptible
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import okhttp3.HttpUrl.Companion.toHttpUrl
import okio.IOException
import org.koitharu.kotatsu.base.domain.MangaUtils
import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.toFileNameSafe
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 EXTENSION_FALLBACK = "png"
class PageSaveHelper(
private val cache: PagesCache,
context: Context,
class PageSaveHelper @Inject constructor(
@ApplicationContext context: Context,
) {
private var continuation: Continuation<Uri>? = null
@@ -65,7 +65,7 @@ class PageSaveHelper(
var extension = name.substringAfterLast('.', "")
name = name.substringBeforeLast('.')
if (extension.length !in 2..4) {
val mimeType = MangaUtils.getImageMimeType(file)
val mimeType = MangaDataRepository.getImageMimeType(file)
extension = if (mimeType != null) {
MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) ?: EXTENSION_FALLBACK
} else {
@@ -74,4 +74,4 @@ class PageSaveHelper(
}
return name.toFileNameSafe().take(MAX_FILENAME_LENGTH) + "." + extension
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,9 +6,11 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.children
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlin.math.absoluteValue
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.reader.ui.ReaderState
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.viewLifecycleScope
@AndroidEntryPoint
class ReversedReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
@Inject
lateinit var settings: AppSettings
private var pagerAdapter: ReversedPagesAdapter? = null
override fun onInflateView(
inflater: LayoutInflater,
container: ViewGroup?
container: ViewGroup?,
) = FragmentReaderStandardBinding.inflate(inflater, container, false)
@SuppressLint("NotifyDataSetChanged")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
pagerAdapter = ReversedPagesAdapter(viewModel.pageLoader, get(), exceptionResolver)
pagerAdapter = ReversedPagesAdapter(viewModel.pageLoader, settings, exceptionResolver)
with(binding.pager) {
adapter = pagerAdapter
offscreenPageLimit = 2
@@ -67,7 +73,7 @@ class ReversedReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
override fun switchPageTo(position: Int, smooth: Boolean) {
binding.pager.setCurrentItem(
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(
chapterId = page.chapterId,
page = page.index,
scroll = 0
scroll = 0,
)
}
@@ -109,4 +115,4 @@ class ReversedReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
private fun reversed(position: Int): Int {
return ((pagerAdapter?.itemCount ?: 0) - position - 1).coerceAtLeast(0)
}
}
}

View File

@@ -6,9 +6,11 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.children
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlin.math.absoluteValue
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.reader.ui.ReaderState
import org.koitharu.kotatsu.reader.ui.pager.BaseReader
@@ -19,19 +21,23 @@ import org.koitharu.kotatsu.utils.ext.recyclerView
import org.koitharu.kotatsu.utils.ext.resetTransformations
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
@AndroidEntryPoint
class PagerReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
@Inject
lateinit var settings: AppSettings
private var pagesAdapter: PagesAdapter? = null
override fun onInflateView(
inflater: LayoutInflater,
container: ViewGroup?
container: ViewGroup?,
) = FragmentReaderStandardBinding.inflate(inflater, container, false)
@SuppressLint("NotifyDataSetChanged")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
pagesAdapter = PagesAdapter(viewModel.pageLoader, get(), exceptionResolver)
pagesAdapter = PagesAdapter(viewModel.pageLoader, settings, exceptionResolver)
with(binding.pager) {
adapter = pagesAdapter
offscreenPageLimit = 2
@@ -86,7 +92,7 @@ class PagerReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
override fun switchPageTo(position: Int, smooth: Boolean) {
binding.pager.setCurrentItem(
position,
smooth && (binding.pager.currentItem - position).absoluteValue < SMOOTH_SCROLL_LIMIT
smooth && (binding.pager.currentItem - position).absoluteValue < SMOOTH_SCROLL_LIMIT,
)
}
@@ -96,7 +102,7 @@ class PagerReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
ReaderState(
chapterId = page.chapterId,
page = page.index,
scroll = 0
scroll = 0,
)
}
@@ -108,4 +114,4 @@ class PagerReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
const val SMOOTH_SCROLL_LIMIT = 3
}
}
}

View File

@@ -5,8 +5,10 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.async
import org.koin.android.ext.android.get
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.databinding.FragmentReaderWebtoonBinding
import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.reader.ui.pager.BaseReader
@@ -16,19 +18,23 @@ import org.koitharu.kotatsu.utils.ext.findCenterViewPosition
import org.koitharu.kotatsu.utils.ext.firstVisibleItemPosition
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
@AndroidEntryPoint
class WebtoonReaderFragment : BaseReader<FragmentReaderWebtoonBinding>() {
@Inject
lateinit var settings: AppSettings
private val scrollInterpolator = AccelerateDecelerateInterpolator()
private var webtoonAdapter: WebtoonAdapter? = null
override fun onInflateView(
inflater: LayoutInflater,
container: ViewGroup?
container: ViewGroup?,
) = FragmentReaderWebtoonBinding.inflate(inflater, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
webtoonAdapter = WebtoonAdapter(viewModel.pageLoader, get(), exceptionResolver)
webtoonAdapter = WebtoonAdapter(viewModel.pageLoader, settings, exceptionResolver)
with(binding.recyclerView) {
setHasFixedSize(true)
adapter = webtoonAdapter
@@ -73,7 +79,7 @@ class WebtoonReaderFragment : BaseReader<FragmentReaderWebtoonBinding>() {
chapterId = page.chapterId,
page = page.index,
scroll = (recyclerView.findViewHolderForAdapterPosition(currentItem) as? WebtoonHolder)
?.getScrollY() ?: 0
?.getScrollY() ?: 0,
)
}
@@ -85,7 +91,7 @@ class WebtoonReaderFragment : BaseReader<FragmentReaderWebtoonBinding>() {
binding.recyclerView.smoothScrollBy(
0,
(binding.recyclerView.height * 0.9).toInt() * delta,
scrollInterpolator
scrollInterpolator,
)
}
@@ -100,4 +106,4 @@ class WebtoonReaderFragment : BaseReader<FragmentReaderWebtoonBinding>() {
notifyPageChanged(index)
}
}
}
}

View File

@@ -6,8 +6,10 @@ import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.GridLayoutManager
import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.getViewModel
import coil.ImageLoader
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import javax.inject.Provider
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
@@ -21,16 +23,28 @@ import org.koitharu.kotatsu.list.ui.MangaListSpanResolver
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.reader.ui.ReaderViewModel
import org.koitharu.kotatsu.reader.ui.thumbnails.adapter.PageThumbnailAdapter
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
import org.koitharu.kotatsu.utils.ext.withArgs
@AndroidEntryPoint
class PagesThumbnailsSheet :
BaseBottomSheet<SheetPagesBinding>(),
OnListItemClickListener<MangaPage>,
BottomSheetHeaderBar.OnExpansionChangeListener {
@Inject
lateinit var mangaRepositoryFactory: MangaRepository.Factory
@Inject
lateinit var pageLoaderProvider: Provider<PageLoader>
@Inject
lateinit var coil: ImageLoader
@Inject
lateinit var settings: AppSettings
private lateinit var thumbnails: List<PageThumbnail>
private var spanResolver: MangaListSpanResolver? = null
private var currentPageIndex = -1
@@ -44,7 +58,7 @@ class PagesThumbnailsSheet :
return
}
currentPageIndex = requireArguments().getInt(ARG_CURRENT, currentPageIndex)
val repository = MangaRepository(pages.first().source)
val repository = mangaRepositoryFactory.create(pages.first().source)
thumbnails = pages.mapIndexed { i, x ->
PageThumbnail(
number = i + 1,
@@ -75,13 +89,13 @@ class PagesThumbnailsSheet :
)
adapter = PageThumbnailAdapter(
dataSet = thumbnails,
coil = get(),
coil = coil,
scope = viewLifecycleScope,
loader = getPageLoader(),
clickListener = this@PagesThumbnailsSheet,
)
addOnLayoutChangeListener(spanResolver)
spanResolver?.setGridSize(get<AppSettings>().gridSize / 100f, this)
spanResolver?.setGridSize(settings.gridSize / 100f, this)
if (currentPageIndex > 0) {
val offset = resources.getDimensionPixelOffset(R.dimen.preferred_grid_width)
(layoutManager as GridLayoutManager).scrollToPositionWithOffset(currentPageIndex, offset)
@@ -119,8 +133,8 @@ class PagesThumbnailsSheet :
}
private fun getPageLoader(): PageLoader {
val viewModel = (activity as? ReaderActivity)?.getViewModel<ReaderViewModel>()
return viewModel?.pageLoader ?: PageLoader().also { pageLoader = it }
val viewModel = (activity as? ReaderActivity)?.viewModel
return viewModel?.pageLoader ?: pageLoaderProvider.get().also { pageLoader = it }
}
companion object {

View File

@@ -1,20 +0,0 @@
package org.koitharu.kotatsu.remotelist
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel
val remoteListModule
get() = module {
viewModel { params ->
RemoteListViewModel(
repository = MangaRepository(params[0]) as RemoteMangaRepository,
settings = get(),
dataRepository = get(),
searchRepository = get(),
)
}
}

View File

@@ -8,8 +8,8 @@ import android.view.View
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView
import androidx.core.view.MenuProvider
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.ListSelectionController
import org.koitharu.kotatsu.list.ui.MangaListFragment
@@ -19,13 +19,18 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.search.ui.SearchActivity
import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.utils.ext.addMenuProvider
import org.koitharu.kotatsu.utils.ext.assistedViewModels
import org.koitharu.kotatsu.utils.ext.serializableArgument
import org.koitharu.kotatsu.utils.ext.withArgs
@AndroidEntryPoint
class RemoteListFragment : MangaListFragment() {
override val viewModel by viewModel<RemoteListViewModel> {
parametersOf(source)
@Inject
lateinit var viewModelFactory: RemoteListViewModel.Factory
public override val viewModel by assistedViewModels {
viewModelFactory.create(source)
}
private val source by serializableArgument<MangaSource>(ARG_SOURCE)

View File

@@ -2,6 +2,10 @@ package org.koitharu.kotatsu.remotelist.ui
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import java.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
@@ -9,6 +13,7 @@ import kotlinx.coroutines.flow.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.list.ui.MangaListViewModel
@@ -18,21 +23,23 @@ import org.koitharu.kotatsu.list.ui.filter.FilterState
import org.koitharu.kotatsu.list.ui.filter.OnFilterChangedListener
import org.koitharu.kotatsu.list.ui.model.*
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import java.util.*
private const val FILTER_MIN_INTERVAL = 250L
class RemoteListViewModel(
private val repository: RemoteMangaRepository,
class RemoteListViewModel @AssistedInject constructor(
@Assisted source: MangaSource,
mangaRepositoryFactory: MangaRepository.Factory,
private val searchRepository: MangaSearchRepository,
settings: AppSettings,
dataRepository: MangaDataRepository,
) : MangaListViewModel(settings), OnFilterChangedListener {
private val repository = mangaRepositoryFactory.create(source) as RemoteMangaRepository
private val filter = FilterCoordinator(repository, dataRepository, viewModelScope)
private val mangaList = MutableStateFlow<List<Manga>?>(null)
private val hasNextPage = MutableStateFlow(false)
@@ -158,7 +165,7 @@ class RemoteListViewModel(
private suspend fun createChipsList(
filterState: FilterState,
availableTags: Set<MangaTag>
availableTags: Set<MangaTag>,
): List<ChipsView.ChipModel> {
val selectedTags = filterState.tags.toMutableSet()
var tags = searchRepository.getTagsSuggestion("", 6, repository.source)
@@ -195,4 +202,10 @@ class RemoteListViewModel(
}
return result
}
}
@AssistedFactory
interface Factory {
fun create(source: MangaSource): RemoteListViewModel
}
}

View File

@@ -0,0 +1,41 @@
package org.koitharu.kotatsu.scrobbling
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ElementsIntoSet
import okhttp3.OkHttpClient
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.scrobbling.domain.Scrobbler
import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriAuthenticator
import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriInterceptor
import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriRepository
import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriStorage
import org.koitharu.kotatsu.scrobbling.shikimori.domain.ShikimoriScrobbler
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object ScrobblingModule {
@Provides
@Singleton
fun provideShikimoriRepository(
storage: ShikimoriStorage,
database: MangaDatabase,
authenticator: ShikimoriAuthenticator,
): ShikimoriRepository {
val okHttp = OkHttpClient.Builder().apply {
authenticator(authenticator)
addInterceptor(ShikimoriInterceptor(storage))
}.build()
return ShikimoriRepository(okHttp, storage, database)
}
@Provides
@ElementsIntoSet
fun provideScrobblers(
shikimoriScrobbler: ShikimoriScrobbler,
): Set<@JvmSuppressWildcards Scrobbler> = setOf(shikimoriScrobbler)
}

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