Merge branch 'feature/mvvm' into devel

pull/26/head
Koitharu 5 years ago
commit b1be45af8b

@ -1,15 +1,9 @@
language: android
dist: trusty
jdk:
- oraclejdk8
android:
components:
- tools
- platform-tools-30.0.3
- build-tools-30.0.2
- android-30
licenses:
- android-sdk-preview-license-.+
- android-sdk-license-.+
- google-gdk-license-.+
- build-tools-30.0.2
- platform-tools-30.0.3
- tools
script: ./gradlew -Dorg.gradle.jvmargs=-Xmx1536m assembleDebug lintDebug

@ -3,6 +3,7 @@ plugins {
id 'kotlin-android'
id 'kotlin-android-extensions'
id 'kotlin-kapt'
// TODO id 'kotlin-parcelize'
}
def gitCommits = 'git rev-list --count HEAD'.execute([], rootDir).text.trim().toInteger()
@ -70,6 +71,7 @@ dependencies {
implementation 'androidx.activity:activity-ktx:1.2.0-beta01'
implementation 'androidx.fragment:fragment-ktx:1.3.0-beta01'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-beta01'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.0-beta01'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha06'
@ -84,24 +86,22 @@ dependencies {
implementation 'androidx.room:room-ktx:2.2.5'
kapt 'androidx.room:room-compiler:2.2.5'
implementation 'com.github.moxy-community:moxy:2.2.0'
implementation 'com.github.moxy-community:moxy-androidx:2.2.0'
implementation 'com.github.moxy-community:moxy-material:2.2.0'
implementation 'com.github.moxy-community:moxy-ktx:2.2.0'
kapt 'com.github.moxy-community:moxy-compiler:2.2.0'
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
implementation 'com.squareup.okio:okio:2.9.0'
implementation 'org.jsoup:jsoup:1.13.1'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.0'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-layoutcontainer:4.3.0'
implementation 'org.koin:koin-android:2.2.0'
implementation 'io.coil-kt:coil-base:1.0.0'
implementation 'org.koin:koin-android-viewmodel:2.2.0'
implementation 'io.coil-kt:coil-base:1.1.0'
implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0'
implementation 'com.tomclaw.cache:cache:1.0'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.5'
testImplementation 'junit:junit:4.13.1'
testImplementation 'org.json:json:20200518'
testImplementation 'org.json:json:20201115'
testImplementation 'org.koin:koin-test:2.2.0-rc-2'
}

@ -23,7 +23,7 @@
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true"
tools:ignore="UnusedAttribute">
<activity android:name=".ui.list.MainActivity">
<activity android:name="org.koitharu.kotatsu.main.ui.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@ -32,62 +32,62 @@
android:name="android.app.default_searchable"
android:value=".ui.search.SearchActivity" />
</activity>
<activity android:name=".ui.details.MangaDetailsActivity">
<activity android:name="org.koitharu.kotatsu.details.ui.DetailsActivity">
<intent-filter>
<action android:name="${applicationId}.action.VIEW_MANGA" />
</intent-filter>
</activity>
<activity android:name=".ui.reader.ReaderActivity" />
<activity android:name="org.koitharu.kotatsu.reader.ui.ReaderActivity" />
<activity
android:name=".ui.search.SearchActivity"
android:name="org.koitharu.kotatsu.search.ui.SearchActivity"
android:label="@string/search" />
<activity
android:name=".ui.settings.SettingsActivity"
android:name="org.koitharu.kotatsu.settings.SettingsActivity"
android:label="@string/settings" />
<activity
android:name=".ui.reader.SimpleSettingsActivity"
android:name="org.koitharu.kotatsu.reader.ui.SimpleSettingsActivity"
android:label="@string/settings">
<intent-filter>
<action android:name="android.intent.action.MANAGE_NETWORK_USAGE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity android:name=".ui.browser.BrowserActivity" />
<activity android:name="org.koitharu.kotatsu.browser.BrowserActivity" />
<activity
android:name=".ui.utils.CrashActivity"
android:name="org.koitharu.kotatsu.core.ui.CrashActivity"
android:label="@string/error_occurred"
android:theme="@android:style/Theme.DeviceDefault"
android:windowSoftInputMode="stateAlwaysHidden" />
<activity
android:name="org.koitharu.kotatsu.ui.list.favourites.categories.CategoriesActivity"
android:name="org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity"
android:label="@string/favourites_categories"
android:windowSoftInputMode="stateAlwaysHidden" />
<activity
android:name=".ui.widget.shelf.ShelfConfigActivity"
android:name="org.koitharu.kotatsu.widget.shelf.ShelfConfigActivity"
android:label="@string/manga_shelf">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<activity
android:name=".ui.search.global.GlobalSearchActivity"
android:name="org.koitharu.kotatsu.search.ui.global.GlobalSearchActivity"
android:label="@string/search" />
<activity
android:name=".ui.utils.protect.ProtectActivity"
android:name="org.koitharu.kotatsu.main.ui.protect.ProtectActivity"
android:windowSoftInputMode="adjustResize" />
<service
android:name=".ui.download.DownloadService"
android:name="org.koitharu.kotatsu.download.DownloadService"
android:foregroundServiceType="dataSync" />
<service
android:name=".ui.widget.shelf.ShelfWidgetService"
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS" />
<service
android:name=".ui.widget.recent.RecentWidgetService"
android:name="org.koitharu.kotatsu.widget.recent.RecentWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS" />
<provider
android:name=".ui.search.MangaSuggestionsProvider"
android:name="org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider"
android:authorities="${applicationId}.MangaSuggestionsProvider"
android:exported="false" />
<provider
@ -101,7 +101,7 @@
</provider>
<receiver
android:name=".ui.widget.shelf.ShelfWidgetProvider"
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetProvider"
android:label="@string/manga_shelf">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@ -111,7 +111,7 @@
android:resource="@xml/widget_shelf" />
</receiver>
<receiver
android:name=".ui.widget.recent.RecentWidgetProvider"
android:name="org.koitharu.kotatsu.widget.recent.RecentWidgetProvider"
android:label="@string/recent_manga">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />

@ -6,25 +6,30 @@ import androidx.appcompat.app.AppCompatDelegate
import org.koin.android.ext.android.get
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin
import org.koin.dsl.module
import org.koitharu.kotatsu.core.backup.BackupRepository
import org.koitharu.kotatsu.core.backup.RestoreRepository
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.db.databaseModule
import org.koitharu.kotatsu.core.github.githubModule
import org.koitharu.kotatsu.core.local.PagesCache
import org.koitharu.kotatsu.core.network.networkModule
import org.koitharu.kotatsu.core.parser.LocalMangaRepository
import org.koitharu.kotatsu.core.parser.parserModule
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.domain.MangaDataRepository
import org.koitharu.kotatsu.domain.MangaLoaderContext
import org.koitharu.kotatsu.domain.MangaSearchRepository
import org.koitharu.kotatsu.domain.favourites.FavouritesRepository
import org.koitharu.kotatsu.domain.history.HistoryRepository
import org.koitharu.kotatsu.domain.tracking.TrackingRepository
import org.koitharu.kotatsu.ui.base.uiModule
import org.koitharu.kotatsu.ui.utils.AppCrashHandler
import org.koitharu.kotatsu.ui.widget.WidgetUpdater
import org.koitharu.kotatsu.core.ui.AppCrashHandler
import org.koitharu.kotatsu.core.ui.uiModule
import org.koitharu.kotatsu.details.detailsModule
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.favourites.favouritesModule
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.history.historyModule
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.reader.readerModule
import org.koitharu.kotatsu.remotelist.remoteListModule
import org.koitharu.kotatsu.search.searchModule
import org.koitharu.kotatsu.settings.settingsModule
import org.koitharu.kotatsu.tracker.trackerModule
import org.koitharu.kotatsu.widget.WidgetUpdater
import org.koitharu.kotatsu.widget.appWidgetModule
class KotatsuApp : Application() {
@ -62,20 +67,19 @@ class KotatsuApp : Application() {
networkModule,
databaseModule,
githubModule,
parserModule,
uiModule,
module {
single { FavouritesRepository(get()) }
single { HistoryRepository(get()) }
single { TrackingRepository(get(), get()) }
single { MangaDataRepository(get()) }
single { BackupRepository(get()) }
single { RestoreRepository(get()) }
single { MangaSearchRepository() }
single { MangaLoaderContext() }
single { AppSettings(get()) }
single { PagesCache(get()) }
}
parserModule,
mainModule,
searchModule,
localModule,
favouritesModule,
historyModule,
remoteListModule,
detailsModule,
trackerModule,
settingsModule,
readerModule,
appWidgetModule
)
}
}

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.domain
package org.koitharu.kotatsu.base.domain
import androidx.room.withTransaction
import org.koitharu.kotatsu.core.db.MangaDatabase
@ -32,6 +32,12 @@ class MangaDataRepository(private val db: MangaDatabase) {
return db.mangaDao.find(mangaId)?.toManga()
}
suspend fun resolveIntent(intent: MangaIntent): Manga? = when {
intent.manga != null -> intent.manga
intent.mangaId != MangaIntent.ID_NONE -> db.mangaDao.find(intent.mangaId)?.toManga()
else -> null // TODO resolve uri
}
suspend fun storeManga(manga: Manga) {
val tags = manga.tags.map(TagEntity.Companion::fromMangaTag)
db.withTransaction {

@ -0,0 +1,33 @@
package org.koitharu.kotatsu.base.domain
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import org.koitharu.kotatsu.core.model.Manga
data class MangaIntent(
val manga: Manga?,
val mangaId: Long,
val uri: Uri?
) {
companion object {
fun from(intent: Intent?) = MangaIntent(
manga = intent?.getParcelableExtra(KEY_MANGA),
mangaId = intent?.getLongExtra(KEY_ID, ID_NONE) ?: ID_NONE,
uri = intent?.data
)
fun from(args: Bundle?) = MangaIntent(
manga = args?.getParcelable(KEY_MANGA),
mangaId = args?.getLong(KEY_ID, ID_NONE) ?: ID_NONE,
uri = null
)
const val ID_NONE = 0L
const val KEY_MANGA = "manga"
const val KEY_ID = "id"
}
}

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.domain
package org.koitharu.kotatsu.base.domain
import okhttp3.*
import org.koin.core.component.KoinComponent

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.domain
package org.koitharu.kotatsu.base.domain
import org.koin.core.component.KoinComponent
import org.koin.core.component.get

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.domain
package org.koitharu.kotatsu.base.domain
import android.graphics.BitmapFactory
import android.net.Uri

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.base
package org.koitharu.kotatsu.base.ui
import android.app.Dialog
import android.os.Bundle
@ -6,11 +6,11 @@ import android.view.View
import androidx.annotation.CallSuper
import androidx.annotation.LayoutRes
import androidx.appcompat.app.AlertDialog
import moxy.MvpAppCompatDialogFragment
import androidx.fragment.app.DialogFragment
abstract class AlertDialogFragment(
@LayoutRes private val layoutResId: Int
) : MvpAppCompatDialogFragment() {
) : DialogFragment() {
private var rootView: View? = null

@ -1,15 +1,15 @@
package org.koitharu.kotatsu.ui.base
package org.koitharu.kotatsu.base.ui
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import moxy.MvpAppCompatActivity
import org.koin.android.ext.android.get
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings
abstract class BaseActivity : MvpAppCompatActivity() {
abstract class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
if (get<AppSettings>().isAmoledTheme) {

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.base
package org.koitharu.kotatsu.base.ui
import android.app.Dialog
import android.os.Bundle
@ -7,11 +7,11 @@ import android.view.View
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.appcompat.app.AppCompatDialog
import moxy.MvpBottomSheetDialogFragment
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.koitharu.kotatsu.utils.UiUtils
abstract class BaseBottomSheet(@LayoutRes private val layoutResId: Int) :
MvpBottomSheetDialogFragment() {
BottomSheetDialogFragment() {
final override fun onCreateView(
inflater: LayoutInflater,

@ -1,14 +1,14 @@
package org.koitharu.kotatsu.ui.base
package org.koitharu.kotatsu.base.ui
import android.content.Context
import androidx.annotation.LayoutRes
import androidx.fragment.app.Fragment
import coil.ImageLoader
import moxy.MvpAppCompatFragment
import org.koin.android.ext.android.inject
abstract class BaseFragment(
@LayoutRes contentLayoutId: Int
) : MvpAppCompatFragment(contentLayoutId) {
) : Fragment(contentLayoutId) {
protected val coil by inject<ImageLoader>()

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.base
package org.koitharu.kotatsu.base.ui
import android.graphics.Color
import android.os.Build

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.base
package org.koitharu.kotatsu.base.ui
import androidx.annotation.StringRes
import androidx.preference.PreferenceFragmentCompat

@ -0,0 +1,5 @@
package org.koitharu.kotatsu.base.ui
import androidx.lifecycle.LifecycleService
abstract class BaseService : LifecycleService()

@ -1,35 +1,35 @@
package org.koitharu.kotatsu.ui.base
package org.koitharu.kotatsu.base.ui
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.*
import moxy.MvpPresenter
import moxy.presenterScope
import org.koin.core.component.KoinComponent
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.utils.SingleLiveEvent
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
abstract class BasePresenter<V : BaseMvpView> : MvpPresenter<V>(), KoinComponent {
abstract class BaseViewModel : ViewModel() {
val onError = SingleLiveEvent<Throwable>()
val isLoading = MutableLiveData(false)
protected fun launchJob(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
) {
presenterScope.launch(context + createErrorHandler(), start, block)
}
): Job = viewModelScope.launch(context + createErrorHandler(), start, block)
protected fun launchLoadingJob(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
) {
presenterScope.launch(context + createErrorHandler(), start) {
viewState.onLoadingStateChanged(isLoading = true)
): Job = viewModelScope.launch(context + createErrorHandler(), start) {
isLoading.postValue(true)
try {
block()
} finally {
viewState.onLoadingStateChanged(isLoading = false)
}
isLoading.postValue(false)
}
}
@ -38,7 +38,7 @@ abstract class BasePresenter<V : BaseMvpView> : MvpPresenter<V>(), KoinComponent
throwable.printStackTrace()
}
if (throwable !is CancellationException) {
viewState.onError(throwable)
onError.postCall(throwable)
}
}
}

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.base.dialog
package org.koitharu.kotatsu.base.ui.dialog
import android.annotation.SuppressLint
import android.content.Context
@ -8,7 +8,6 @@ import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import com.google.android.material.checkbox.MaterialCheckBox
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R
class CheckBoxAlertDialog private constructor(private val delegate: AlertDialog) :
@ -23,7 +22,7 @@ class CheckBoxAlertDialog private constructor(private val delegate: AlertDialog)
.inflate(R.layout.dialog_checkbox, null, false)
private val checkBox = view.findViewById<MaterialCheckBox>(android.R.id.checkbox)
private val delegate = MaterialAlertDialogBuilder(context)
private val delegate = AlertDialog.Builder(context)
.setView(view)
fun setTitle(@StringRes titleResId: Int): Builder {

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.base.dialog
package org.koitharu.kotatsu.base.ui.dialog
import android.content.Context
import android.content.DialogInterface
@ -7,10 +7,9 @@ import android.view.ViewGroup
import android.widget.BaseAdapter
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.android.synthetic.main.item_storage.view.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.LocalMangaRepository
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.utils.ext.getStorageName
import org.koitharu.kotatsu.utils.ext.inflate
import org.koitharu.kotatsu.utils.ext.longHashCode
@ -24,7 +23,7 @@ class StorageSelectDialog private constructor(private val delegate: AlertDialog)
class Builder(context: Context, defaultValue: File?, listener: OnStorageSelectListener) {
private val adapter = VolumesAdapter(context)
private val delegate = MaterialAlertDialogBuilder(context)
private val delegate = AlertDialog.Builder(context)
init {
if (adapter.isEmpty) {

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.base.dialog
package org.koitharu.kotatsu.base.ui.dialog
import android.annotation.SuppressLint
import android.content.Context
@ -7,7 +7,6 @@ import android.text.InputFilter
import android.view.LayoutInflater
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.android.synthetic.main.dialog_input.view.*
import org.koitharu.kotatsu.R
@ -23,7 +22,7 @@ class TextInputDialog private constructor(
private val view = LayoutInflater.from(context)
.inflate(R.layout.dialog_input, null, false)
private val delegate = MaterialAlertDialogBuilder(context)
private val delegate = AlertDialog.Builder(context)
.setView(view)
fun setTitle(@StringRes titleResId: Int): Builder {

@ -1,9 +1,10 @@
package org.koitharu.kotatsu.ui.base.list
package org.koitharu.kotatsu.base.ui.list
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import java.util.*
@Deprecated("")
class AdapterUpdater<T>(oldList: List<T>, newList: List<T>, getId: (T) -> Long) {
private val diff = DiffUtil.calculateDiff(object : DiffUtil.Callback() {

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.base.list
package org.koitharu.kotatsu.base.ui.list
import android.view.View
import android.view.ViewGroup
@ -30,25 +30,7 @@ abstract class BaseViewHolder<T, E> protected constructor(view: View) :
return boundData ?: throw IllegalStateException("Calling requireData() before bind()")
}
fun setOnItemClickListener(listener: OnRecyclerItemClickListener<T>?) {
val listenersAdapter = listener?.let { HolderListenersAdapter(it) }
itemView.setOnClickListener(listenersAdapter)
itemView.setOnLongClickListener(listenersAdapter)
}
open fun onRecycled() = Unit
abstract fun onBind(data: T, extra: E)
private inner class HolderListenersAdapter(private val listener: OnRecyclerItemClickListener<T>) :
View.OnClickListener, View.OnLongClickListener {
override fun onClick(v: View) {
listener.onItemClick(boundData ?: return, bindingAdapterPosition, v)
}
override fun onLongClick(v: View): Boolean {
return listener.onItemLongClick(boundData ?: return false, bindingAdapterPosition, v)
}
}
}

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.base.list
package org.koitharu.kotatsu.base.ui.list
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView

@ -0,0 +1,10 @@
package org.koitharu.kotatsu.base.ui.list
import android.view.View
interface OnListItemClickListener<I> {
fun onItemClick(item: I, view: View)
fun onItemLongClick(item: I, view: View) = false
}

@ -0,0 +1,18 @@
package org.koitharu.kotatsu.base.ui.list
import androidx.recyclerview.widget.RecyclerView
class PaginationScrollListener(offset: Int, private val callback: Callback) :
BoundsScrollListener(0, offset) {
override fun onScrolledToStart(recyclerView: RecyclerView) = Unit
override fun onScrolledToEnd(recyclerView: RecyclerView) {
callback.onScrolledToEnd()
}
interface Callback {
fun onScrolledToEnd()
}
}

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.base.list
package org.koitharu.kotatsu.base.ui.list
import android.view.View
import android.view.ViewGroup

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.base.list.decor
package org.koitharu.kotatsu.base.ui.list.decor
import android.content.Context
import android.graphics.Canvas

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.base.list.decor
package org.koitharu.kotatsu.base.ui.list.decor
import android.graphics.Canvas
import android.graphics.Rect

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.base.list.decor
package org.koitharu.kotatsu.base.ui.list.decor
import android.graphics.Rect
import android.view.View

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.base.widgets
package org.koitharu.kotatsu.base.ui.widgets
import android.content.Context
import android.util.AttributeSet

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.base.widgets
package org.koitharu.kotatsu.base.ui.widgets
import android.content.Context
import android.util.AttributeSet

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.base.widgets
package org.koitharu.kotatsu.base.ui.widgets
import android.content.Context
import android.util.AttributeSet

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.browser
package org.koitharu.kotatsu.browser
import android.annotation.SuppressLint
import android.content.ActivityNotFoundException
@ -11,7 +11,7 @@ import android.view.MenuItem
import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.activity_browser.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.ui.base.BaseActivity
import org.koitharu.kotatsu.base.ui.BaseActivity
@SuppressLint("SetJavaScriptEnabled")
class BrowserActivity : BaseActivity(), BrowserCallback {

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.browser
package org.koitharu.kotatsu.browser
interface BrowserCallback {

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.browser
package org.koitharu.kotatsu.browser
import android.graphics.Bitmap
import android.webkit.WebResourceRequest

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.utils.cloudflare
package org.koitharu.kotatsu.browser.cloudflare
interface CloudFlareCallback {

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.utils.cloudflare
package org.koitharu.kotatsu.browser.cloudflare
import android.graphics.Bitmap
import android.webkit.CookieManager

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.utils.cloudflare
package org.koitharu.kotatsu.browser.cloudflare
import android.annotation.SuppressLint
import android.os.Bundle
@ -10,8 +10,8 @@ import androidx.core.view.isInvisible
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import kotlinx.android.synthetic.main.fragment_cloudflare.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
import org.koitharu.kotatsu.core.network.UserAgentInterceptor
import org.koitharu.kotatsu.ui.base.AlertDialogFragment
import org.koitharu.kotatsu.utils.ext.stringArgument
import org.koitharu.kotatsu.utils.ext.withArgs

@ -4,7 +4,11 @@ import org.json.JSONArray
import org.json.JSONObject
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.*
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.history.data.HistoryEntity
class BackupRepository(private val db: MangaDatabase) {

@ -3,7 +3,11 @@ package org.koitharu.kotatsu.core.backup
import androidx.room.withTransaction
import org.json.JSONObject
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.*
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.history.data.HistoryEntity
import org.koitharu.kotatsu.utils.ext.getStringOrNull
import org.koitharu.kotatsu.utils.ext.iterator
import org.koitharu.kotatsu.utils.ext.map

@ -4,6 +4,12 @@ import androidx.room.Database
import androidx.room.RoomDatabase
import org.koitharu.kotatsu.core.db.dao.*
import org.koitharu.kotatsu.core.db.entity.*
import org.koitharu.kotatsu.favourites.data.FavouriteCategoriesDao
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.favourites.data.FavouritesDao
import org.koitharu.kotatsu.history.data.HistoryDao
import org.koitharu.kotatsu.history.data.HistoryEntity
@Database(
entities = [

@ -1,3 +1,4 @@
package org.koitharu.kotatsu.core.exceptions
class ParseException(message: String? = null, cause: Throwable? = null) : RuntimeException(message, cause)
class ParseException(message: String? = null, cause: Throwable? = null) :
RuntimeException(message, cause)

@ -4,9 +4,10 @@ import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
import org.koin.core.context.GlobalContext
import org.koin.core.error.NoBeanDefFoundException
import org.koitharu.kotatsu.core.parser.LocalMangaRepository
import org.koin.core.qualifier.named
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.site.*
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
@Suppress("SpellCheckingInspection")
@Parcelize
@ -30,6 +31,7 @@ enum class MangaSource(
// HENTAILIB("HentaiLib", "ru", HentaiLibRepository::class.java)
@get:Throws(NoBeanDefFoundException::class)
@Deprecated("")
val repository: MangaRepository
get() = GlobalContext.get().get(cls.kotlin)
get() = GlobalContext.get().get(named(this))
}

@ -6,7 +6,12 @@ interface MangaRepository {
val sortOrders: Set<SortOrder>
suspend fun getList(offset: Int, query: String? = null, sortOrder: SortOrder? = null, tag: MangaTag? = null): List<Manga>
suspend fun getList(
offset: Int,
query: String? = null,
sortOrder: SortOrder? = null,
tag: MangaTag? = null
): List<Manga>
suspend fun getDetails(manga: Manga): Manga

@ -1,22 +1,25 @@
package org.koitharu.kotatsu.core.parser
import org.koin.dsl.bind
import org.koin.core.qualifier.named
import org.koin.dsl.module
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.parser.site.*
val parserModule
get() = module {
single { LocalMangaRepository(get()) } bind MangaRepository::class
factory { ReadmangaRepository(get()) } bind MangaRepository::class
factory { MintMangaRepository(get()) } bind MangaRepository::class
factory { SelfMangaRepository(get()) } bind MangaRepository::class
factory { MangaChanRepository(get()) } bind MangaRepository::class
factory { DesuMeRepository(get()) } bind MangaRepository::class
factory { HenChanRepository(get()) } bind MangaRepository::class
factory { YaoiChanRepository(get()) } bind MangaRepository::class
factory { MangaTownRepository(get()) } bind MangaRepository::class
factory { MangaLibRepository(get()) } bind MangaRepository::class
factory { NudeMoonRepository(get()) } bind MangaRepository::class
factory { MangareadRepository(get()) } bind MangaRepository::class
single { MangaLoaderContext() }
factory<MangaRepository>(named(MangaSource.READMANGA_RU)) { ReadmangaRepository(get()) }
factory<MangaRepository>(named(MangaSource.MINTMANGA)) { MintMangaRepository(get()) }
factory<MangaRepository>(named(MangaSource.SELFMANGA)) { SelfMangaRepository(get()) }
factory<MangaRepository>(named(MangaSource.MANGACHAN)) { MangaChanRepository(get()) }
factory<MangaRepository>(named(MangaSource.DESUME)) { DesuMeRepository(get()) }
factory<MangaRepository>(named(MangaSource.HENCHAN)) { HenChanRepository(get()) }
factory<MangaRepository>(named(MangaSource.YAOICHAN)) { YaoiChanRepository(get()) }
factory<MangaRepository>(named(MangaSource.MANGATOWN)) { MangaTownRepository(get()) }
factory<MangaRepository>(named(MangaSource.MANGALIB)) { MangaLibRepository(get()) }
factory<MangaRepository>(named(MangaSource.NUDEMOON)) { NudeMoonRepository(get()) }
factory<MangaRepository>(named(MangaSource.MANGAREAD)) { MangareadRepository(get()) }
}

@ -1,10 +1,10 @@
package org.koitharu.kotatsu.core.parser
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.MangaTag
import org.koitharu.kotatsu.core.model.SortOrder
import org.koitharu.kotatsu.domain.MangaLoaderContext
abstract class RemoteMangaRepository(
protected val loaderContext: MangaLoaderContext

@ -1,11 +1,11 @@
package org.koitharu.kotatsu.core.parser.site
import androidx.collection.arraySetOf
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.prefs.SourceSettings
import org.koitharu.kotatsu.domain.MangaLoaderContext
import org.koitharu.kotatsu.utils.ext.*
import java.util.*

@ -1,11 +1,11 @@
package org.koitharu.kotatsu.core.parser.site
import androidx.collection.arraySetOf
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.prefs.SourceSettings
import org.koitharu.kotatsu.domain.MangaLoaderContext
import org.koitharu.kotatsu.utils.ext.*
import java.util.*
import kotlin.collections.ArrayList

@ -2,11 +2,11 @@ package org.koitharu.kotatsu.core.parser.site
import androidx.collection.arraySetOf
import androidx.core.net.toUri
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.prefs.SourceSettings
import org.koitharu.kotatsu.domain.MangaLoaderContext
import org.koitharu.kotatsu.utils.ext.*
import java.util.*

@ -1,8 +1,8 @@
package org.koitharu.kotatsu.core.parser.site
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.domain.MangaLoaderContext
import org.koitharu.kotatsu.utils.ext.longHashCode
import org.koitharu.kotatsu.utils.ext.mapToSet
import org.koitharu.kotatsu.utils.ext.parseHtml

@ -1,7 +1,7 @@
package org.koitharu.kotatsu.core.parser.site
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.domain.MangaLoaderContext
class MangaChanRepository(loaderContext: MangaLoaderContext) : ChanRepository(loaderContext) {

@ -4,11 +4,11 @@ import androidx.collection.ArraySet
import androidx.collection.arraySetOf
import org.json.JSONArray
import org.json.JSONObject
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.prefs.SourceSettings
import org.koitharu.kotatsu.domain.MangaLoaderContext
import org.koitharu.kotatsu.utils.ext.*
import java.util.*
import kotlin.collections.ArrayList

@ -2,11 +2,11 @@ package org.koitharu.kotatsu.core.parser.site
import androidx.collection.arraySetOf
import org.intellij.lang.annotations.Language
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.prefs.SourceSettings
import org.koitharu.kotatsu.domain.MangaLoaderContext
import org.koitharu.kotatsu.utils.ext.*
import java.util.*

@ -1,11 +1,11 @@
package org.koitharu.kotatsu.core.parser.site
import androidx.collection.arraySetOf
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.prefs.SourceSettings
import org.koitharu.kotatsu.domain.MangaLoaderContext
import org.koitharu.kotatsu.utils.ext.*
import java.util.*

@ -1,7 +1,7 @@
package org.koitharu.kotatsu.core.parser.site
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.domain.MangaLoaderContext
class MintMangaRepository(loaderContext: MangaLoaderContext) : GroupleRepository(loaderContext) {

@ -1,11 +1,11 @@
package org.koitharu.kotatsu.core.parser.site
import androidx.collection.arraySetOf
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.prefs.SourceSettings
import org.koitharu.kotatsu.domain.MangaLoaderContext
import org.koitharu.kotatsu.utils.ext.*
import java.util.*
import java.util.regex.Pattern

@ -1,7 +1,7 @@
package org.koitharu.kotatsu.core.parser.site
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.domain.MangaLoaderContext
class ReadmangaRepository(loaderContext: MangaLoaderContext) : GroupleRepository(loaderContext) {

@ -1,7 +1,7 @@
package org.koitharu.kotatsu.core.parser.site
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.domain.MangaLoaderContext
class SelfMangaRepository(loaderContext: MangaLoaderContext) : GroupleRepository(loaderContext) {

@ -1,10 +1,10 @@
package org.koitharu.kotatsu.core.parser.site
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.domain.MangaLoaderContext
import org.koitharu.kotatsu.utils.ext.longHashCode
import org.koitharu.kotatsu.utils.ext.parseHtml
import org.koitharu.kotatsu.utils.ext.withDomain

@ -7,8 +7,11 @@ import androidx.appcompat.app.AppCompatDelegate
import androidx.collection.arraySetOf
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.sendBlocking
import kotlinx.coroutines.flow.callbackFlow
import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.core.parser.LocalMangaRepository
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.utils.delegates.prefs.*
import java.io.File
@ -19,7 +22,7 @@ class AppSettings private constructor(private val prefs: SharedPreferences) :
PreferenceManager.getDefaultSharedPreferences(context)
)
var listMode by IntEnumPreferenceDelegate(
var listMode by EnumPreferenceDelegate(
ListMode::class.java,
KEY_LIST_MODE,
ListMode.DETAILED_LIST
@ -38,7 +41,7 @@ class AppSettings private constructor(private val prefs: SharedPreferences) :
val isAmoledTheme by BoolPreferenceDelegate(KEY_THEME_AMOLED, defaultValue = false)
val gridSize by IntPreferenceDelegate(KEY_GRID_SIZE, defaultValue = 100)
var gridSize by IntPreferenceDelegate(KEY_GRID_SIZE, defaultValue = 100)
val readerPageSwitch by StringSetPreferenceDelegate(
KEY_READER_SWITCHERS,
@ -117,6 +120,16 @@ class AppSettings private constructor(private val prefs: SharedPreferences) :
prefs.unregisterOnSharedPreferenceChangeListener(listener)
}
fun observe() = callbackFlow<String> {
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
sendBlocking(key)
}
prefs.registerOnSharedPreferenceChangeListener(listener)
awaitClose {
prefs.unregisterOnSharedPreferenceChangeListener(listener)
}
}
companion object {
const val PAGE_SWITCH_TAPS = "taps"
@ -125,7 +138,7 @@ class AppSettings private constructor(private val prefs: SharedPreferences) :
const val TRACK_HISTORY = "history"
const val TRACK_FAVOURITES = "favourites"
const val KEY_LIST_MODE = "list_mode"
const val KEY_LIST_MODE = "list_mode_2"
const val KEY_APP_SECTION = "app_section"
const val KEY_THEME = "theme"
const val KEY_THEME_AMOLED = "amoled_theme"

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.utils
package org.koitharu.kotatsu.core.ui
import android.content.Context
import android.content.Intent

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.base
package org.koitharu.kotatsu.core.ui
import android.content.Context
import android.view.View

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.utils
package org.koitharu.kotatsu.core.ui
import android.app.Activity
import android.content.ActivityNotFoundException
@ -10,7 +10,7 @@ import android.view.MenuItem
import android.view.View
import kotlinx.android.synthetic.main.activity_crash.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.ui.list.MainActivity
import org.koitharu.kotatsu.main.ui.MainActivity
import org.koitharu.kotatsu.utils.ShareHelper
class CrashActivity : Activity(), View.OnClickListener {

@ -1,11 +1,11 @@
package org.koitharu.kotatsu.ui.base
package org.koitharu.kotatsu.core.ui
import coil.ComponentRegistry
import coil.ImageLoader
import okhttp3.OkHttpClient
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
import org.koitharu.kotatsu.core.local.CbzFetcher
import org.koitharu.kotatsu.local.data.CbzFetcher
val uiModule
get() = module {

@ -0,0 +1,14 @@
package org.koitharu.kotatsu.details
import org.koin.android.viewmodel.dsl.viewModel
import org.koin.dsl.module
import org.koitharu.kotatsu.base.domain.MangaIntent
import org.koitharu.kotatsu.details.ui.DetailsViewModel
val detailsModule
get() = module {
viewModel { (intent: MangaIntent) ->
DetailsViewModel(intent, get(), get(), get(), get(), get())
}
}

@ -0,0 +1,148 @@
package org.koitharu.kotatsu.details.ui
import android.app.ActivityOptions
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.fragment_chapters.*
import org.koin.android.viewmodel.ext.android.sharedViewModel
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter
import org.koitharu.kotatsu.details.ui.adapter.ChaptersSelectionDecoration
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.download.DownloadService
import org.koitharu.kotatsu.reader.ui.ReaderActivity
class ChaptersFragment : BaseFragment(R.layout.fragment_chapters),
OnListItemClickListener<MangaChapter>, ActionMode.Callback {
private val viewModel by sharedViewModel<DetailsViewModel>()
private var chaptersAdapter: ChaptersAdapter? = null
private var actionMode: ActionMode? = null
private var selectionDecoration: ChaptersSelectionDecoration? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
chaptersAdapter = ChaptersAdapter(this)
selectionDecoration = ChaptersSelectionDecoration(view.context)
with(recyclerView_chapters) {
addItemDecoration(DividerItemDecoration(view.context, RecyclerView.VERTICAL))
addItemDecoration(selectionDecoration!!)
setHasFixedSize(true)
adapter = chaptersAdapter
}
viewModel.isLoading.observe(viewLifecycleOwner, this::onLoadingStateChanged)
viewModel.chapters.observe(viewLifecycleOwner, this::onChaptersChanged)
}
override fun onDestroyView() {
chaptersAdapter = null
selectionDecoration = null
super.onDestroyView()
}
private fun onChaptersChanged(list: List<ChapterListItem>) {
chaptersAdapter?.items = list
}
private fun onLoadingStateChanged(isLoading: Boolean) {
progressBar.isVisible = isLoading
}
override fun onItemClick(item: MangaChapter, view: View) {
if (selectionDecoration?.checkedItemsCount != 0) {
selectionDecoration?.toggleItemChecked(item.id)
if (selectionDecoration?.checkedItemsCount == 0) {
actionMode?.finish()
} else {
actionMode?.invalidate()
recyclerView_chapters.invalidateItemDecorations()
}
return
}
val options = ActivityOptions.makeScaleUpAnimation(
view,
0,
0,
view.measuredWidth,
view.measuredHeight
)
startActivity(
ReaderActivity.newIntent(
context ?: return,
viewModel.manga.value ?: return,
item.id
), options.toBundle()
)
}
override fun onItemLongClick(item: MangaChapter, view: View): Boolean {
if (actionMode == null) {
actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this)
}
return actionMode?.also {
selectionDecoration?.setItemIsChecked(item.id, true)
recyclerView_chapters.invalidateItemDecorations()
it.invalidate()
} != null
}
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_save -> {
DownloadService.start(
context ?: return false,
viewModel.manga.value ?: return false,
selectionDecoration?.checkedItemsIds
)
mode.finish()
true
}
R.id.action_select_all -> {
val ids = chaptersAdapter?.items?.map { it.chapter.id } ?: return false
selectionDecoration?.checkAll(ids)
recyclerView_chapters.invalidateItemDecorations()
mode.invalidate()
true
}
else -> false
}
}
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
val manga = viewModel.manga.value
mode.menuInflater.inflate(R.menu.mode_chapters, menu)
menu.findItem(R.id.action_save).isVisible = manga?.source != MangaSource.LOCAL
mode.title = manga?.title
return true
}
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
val count = selectionDecoration?.checkedItemsCount ?: return false
mode.subtitle = resources.getQuantityString(
R.plurals.chapters_from_x,
count,
count,
chaptersAdapter?.itemCount ?: 0
)
return true
}
override fun onDestroyActionMode(mode: ActionMode?) {
selectionDecoration?.clearSelection()
recyclerView_chapters.invalidateItemDecorations()
actionMode = null
}
}

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.details
package org.koitharu.kotatsu.details.ui
import android.content.Context
import android.content.Intent
@ -7,70 +7,58 @@ import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.view.ActionMode
import androidx.core.content.ContextCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.net.toFile
import androidx.lifecycle.lifecycleScope
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 kotlinx.android.synthetic.main.activity_details.*
import kotlinx.coroutines.launch
import moxy.MvpDelegate
import moxy.ktx.moxyPresenter
import org.koin.android.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.base.domain.MangaIntent
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.browser.BrowserActivity
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.ui.base.BaseActivity
import org.koitharu.kotatsu.ui.browser.BrowserActivity
import org.koitharu.kotatsu.ui.download.DownloadService
import org.koitharu.kotatsu.download.DownloadService
import org.koitharu.kotatsu.search.ui.global.GlobalSearchActivity
import org.koitharu.kotatsu.utils.MangaShortcut
import org.koitharu.kotatsu.utils.ShareHelper
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.getThemeColor
class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
TabLayoutMediator.TabConfigurationStrategy {
class DetailsActivity : BaseActivity(), TabLayoutMediator.TabConfigurationStrategy {
private val presenter by moxyPresenter {
MangaDetailsPresenter.getInstance(hashCode())
private val viewModel by viewModel<DetailsViewModel> {
parametersOf(MangaIntent.from(intent))
}
private var manga: Manga? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_details)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
pager.adapter = MangaDetailsAdapter(this)
TabLayoutMediator(tabs, pager, this).attach()
if (savedInstanceState?.containsKey(MvpDelegate.MOXY_DELEGATE_TAGS_KEY) != true) {
intent?.getParcelableExtra<Manga>(EXTRA_MANGA)?.let {
presenter.loadDetails(it, true)
} ?: intent?.getLongExtra(EXTRA_MANGA_ID, 0)?.takeUnless { it == 0L }?.let {
presenter.findMangaById(it)
} ?: finishAfterTransition()
}
viewModel.manga.observe(this, ::onMangaUpdated)
viewModel.newChaptersCount.observe(this, ::onNewChaptersChanged)
viewModel.onMangaRemoved.observe(this, ::onMangaRemoved)
viewModel.onError.observe(this, ::onError)
}
override fun onMangaUpdated(manga: Manga) {
this.manga = manga
private fun onMangaUpdated(manga: Manga) {
title = manga.title
invalidateOptionsMenu()
}
override fun onHistoryChanged(history: MangaHistory?) = Unit
override fun onFavouriteChanged(categories: List<FavouriteCategory>) = Unit
override fun onLoadingStateChanged(isLoading: Boolean) = Unit
override fun onMangaRemoved(manga: Manga) {
private fun onMangaRemoved(manga: Manga) {
Toast.makeText(
this, getString(R.string._s_deleted_from_local_storage, manga.title),
Toast.LENGTH_SHORT
@ -78,8 +66,8 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
finishAfterTransition()
}
override fun onError(e: Throwable) {
if (manga == null) {
private fun onError(e: Throwable) {
if (viewModel.manga.value == null) {
Toast.makeText(this, e.getDisplayMessage(resources), Toast.LENGTH_LONG).show()
finishAfterTransition()
} else {
@ -87,7 +75,7 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
}
}
override fun onNewChaptersChanged(newChapters: Int) {
private fun onNewChaptersChanged(newChapters: Int) {
val tab = tabs.getTabAt(1) ?: return
if (newChapters == 0) {
tab.removeBadge()
@ -104,8 +92,9 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
}
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
val manga = viewModel.manga.value
menu.findItem(R.id.action_save).isVisible =
manga?.source != null && manga?.source != MangaSource.LOCAL
manga?.source != null && manga.source != MangaSource.LOCAL
menu.findItem(R.id.action_delete).isVisible =
manga?.source == MangaSource.LOCAL
menu.findItem(R.id.action_browser).isVisible =
@ -117,7 +106,7 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.action_share -> {
manga?.let {
viewModel.manga.value?.let {
if (it.source == MangaSource.LOCAL) {
ShareHelper.shareCbz(this, Uri.parse(it.url).toFile())
} else {
@ -127,12 +116,12 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
true
}
R.id.action_delete -> {
manga?.let { m ->
MaterialAlertDialogBuilder(this)
viewModel.manga.value?.let { m ->
AlertDialog.Builder(this)
.setTitle(R.string.delete_manga)
.setMessage(getString(R.string.text_delete_local_manga, m.title))
.setPositiveButton(R.string.delete) { _, _ ->
presenter.deleteLocal(m)
viewModel.deleteLocal(m)
}
.setNegativeButton(android.R.string.cancel, null)
.show()
@ -140,10 +129,10 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
true
}
R.id.action_save -> {
manga?.let {
viewModel.manga.value?.let {
val chaptersCount = it.chapters?.size ?: 0
if (chaptersCount > 5) {
MaterialAlertDialogBuilder(this)
AlertDialog.Builder(this)
.setTitle(R.string.save_manga)
.setMessage(
getString(
@ -166,15 +155,21 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
true
}
R.id.action_browser -> {
manga?.let {
viewModel.manga.value?.let {
startActivity(BrowserActivity.newIntent(this, it.url))
}
true
}
R.id.action_related -> {
viewModel.manga.value?.let {
startActivity(GlobalSearchActivity.newIntent(this, it.title))
}
true
}
R.id.action_shortcut -> {
manga?.let {
viewModel.manga.value?.let {
lifecycleScope.launch {
if (!MangaShortcut(it).requestPinShortcut(this@MangaDetailsActivity)) {
if (!MangaShortcut(it).requestPinShortcut(this@DetailsActivity)) {
Snackbar.make(
pager,
R.string.operation_not_supported,
@ -192,7 +187,6 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
tab.text = when (position) {
0 -> getString(R.string.details)
1 -> getString(R.string.chapters)
2 -> getString(R.string.related)
else -> null
}
}
@ -211,17 +205,14 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
companion object {
private const val EXTRA_MANGA = "manga"
const val EXTRA_MANGA_ID = "manga_id"
const val ACTION_MANGA_VIEW = "${BuildConfig.APPLICATION_ID}.action.VIEW_MANGA"
fun newIntent(context: Context, manga: Manga) =
Intent(context, MangaDetailsActivity::class.java)
.putExtra(EXTRA_MANGA, manga)
Intent(context, DetailsActivity::class.java)
.putExtra(MangaIntent.KEY_MANGA, manga)
fun newIntent(context: Context, mangaId: Long) =
Intent(context, MangaDetailsActivity::class.java)
.putExtra(EXTRA_MANGA_ID, mangaId)
Intent(context, DetailsActivity::class.java)
.putExtra(MangaIntent.KEY_ID, mangaId)
}
}

@ -1,5 +1,6 @@
package org.koitharu.kotatsu.ui.details
package org.koitharu.kotatsu.details.ui
import android.os.Bundle
import android.text.Spanned
import android.view.View
import androidx.core.net.toUri
@ -10,37 +11,36 @@ import kotlinx.android.synthetic.main.fragment_details.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import moxy.ktx.moxyPresenter
import org.koin.android.viewmodel.ext.android.sharedViewModel
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.ui.base.BaseFragment
import org.koitharu.kotatsu.ui.list.favourites.categories.select.FavouriteCategoriesDialog
import org.koitharu.kotatsu.ui.reader.ReaderActivity
import org.koitharu.kotatsu.ui.search.MangaSearchSheet
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesDialog
import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.search.ui.MangaSearchSheet
import org.koitharu.kotatsu.utils.FileSizeUtils
import org.koitharu.kotatsu.utils.ext.*
import kotlin.math.roundToInt
class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetailsView,
View.OnClickListener,
class DetailsFragment : BaseFragment(R.layout.fragment_details), View.OnClickListener,
View.OnLongClickListener {
@Suppress("unused")
private val presenter by moxyPresenter {
MangaDetailsPresenter.getInstance(activity.hashCode())
}
private val viewModel by sharedViewModel<DetailsViewModel>()
private var manga: Manga? = null
private var history: MangaHistory? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.manga.observe(viewLifecycleOwner, ::onMangaUpdated)
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
viewModel.favouriteCategories.observe(viewLifecycleOwner, ::onFavouriteChanged)
viewModel.readingHistory.observe(viewLifecycleOwner, ::onHistoryChanged)
}
override fun onMangaUpdated(manga: Manga) {
this.manga = manga
private fun onMangaUpdated(manga: Manga) {
imageView_cover.newImageRequest(manga.largeCoverUrl ?: manga.coverUrl)
.fallback(R.drawable.ic_placeholder)
.crossfade(true)
.lifecycle(this)
.lifecycle(viewLifecycleOwner)
.enqueueWith(coil)
textView_title.text = manga.title
textView_subtitle.textAndVisible = manga.altTitle
@ -59,7 +59,7 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
text = it,
iconRes = R.drawable.ic_chip_user,
tag = it,
onClickListener = this@MangaDetailsFragment
onClickListener = this@DetailsFragment
)
}
}
@ -68,7 +68,7 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
text = it.title,
iconRes = R.drawable.ic_chip_tag,
tag = it,
onClickListener = this@MangaDetailsFragment
onClickListener = this@DetailsFragment
)
}
manga.url.toUri().toFileOrNull()?.let { f ->
@ -81,7 +81,7 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
text = FileSizeUtils.formatBytes(context, size),
iconRes = R.drawable.ic_chip_storage,
tag = it,
onClickListener = this@MangaDetailsFragment
onClickListener = this@DetailsFragment
)
}
}
@ -89,35 +89,37 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
imageView_favourite.setOnClickListener(this)
button_read.setOnClickListener(this)
button_read.setOnLongClickListener(this)
updateReadButton()
button_read.isEnabled = !manga.chapters.isNullOrEmpty()
}
override fun onHistoryChanged(history: MangaHistory?) {
this.history = history
updateReadButton()
private fun onHistoryChanged(history: MangaHistory?) {
with(button_read) {
if (history == null) {
setText(R.string.read)
setIconResource(R.drawable.ic_read)
} else {
setText(R.string._continue)
setIconResource(R.drawable.ic_play)
}
}
}
override fun onFavouriteChanged(categories: List<FavouriteCategory>) {
private fun onFavouriteChanged(isFavourite: Boolean) {
imageView_favourite.setImageResource(
if (categories.isEmpty()) {
R.drawable.ic_heart_outline
} else {
if (isFavourite) {
R.drawable.ic_heart
} else {
R.drawable.ic_heart_outline
}
)
}
override fun onLoadingStateChanged(isLoading: Boolean) {
private fun onLoadingStateChanged(isLoading: Boolean) {
progressBar.isVisible = isLoading
}
override fun onError(e: Throwable) = Unit //handled in activity
override fun onMangaRemoved(manga: Manga) = Unit //handled in activity
override fun onNewChaptersChanged(newChapters: Int) = Unit
override fun onClick(v: View) {
val manga = viewModel.manga.value
when {
v.id == R.id.imageView_favourite -> {
FavouriteCategoriesDialog.show(childFragmentManager, manga ?: return)
@ -127,7 +129,7 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
ReaderActivity.newIntent(
context ?: return,
manga ?: return,
history
viewModel.readingHistory.value
)
)
}
@ -146,7 +148,7 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
override fun onLongClick(v: View): Boolean {
when (v.id) {
R.id.button_read -> {
if (history == null) {
if (viewModel.readingHistory.value == null) {
return false
}
v.showPopupMenu(R.menu.popup_read) {
@ -155,7 +157,7 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
startActivity(
ReaderActivity.newIntent(
context ?: return@showPopupMenu false,
manga ?: return@showPopupMenu false
viewModel.manga.value ?: return@showPopupMenu false
)
)
true
@ -168,19 +170,4 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
else -> return false
}
}
private fun updateReadButton() {
if (manga?.chapters.isNullOrEmpty()) {
button_read.isEnabled = false
} else {
button_read.isEnabled = true
if (history == null) {
button_read.setText(R.string.read)
button_read.setIconResource(R.drawable.ic_read)
} else {
button_read.setText(R.string._continue)
button_read.setIconResource(R.drawable.ic_play)
}
}
}
}

@ -0,0 +1,101 @@
package org.koitharu.kotatsu.details.ui
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.base.domain.MangaIntent
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.exceptions.MangaNotFoundException
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.details.ui.model.toListItem
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.history.domain.ChapterExtra
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.safe
import java.io.IOException
class DetailsViewModel(
intent: MangaIntent,
private val historyRepository: HistoryRepository,
private val favouritesRepository: FavouritesRepository,
private val localMangaRepository: LocalMangaRepository,
private val trackingRepository: TrackingRepository,
private val mangaDataRepository: MangaDataRepository
) : BaseViewModel() {
private val mangaData = MutableStateFlow<Manga?>(intent.manga)
private val history = mangaData.mapNotNull { it?.id }
.distinctUntilChanged()
.flatMapLatest { mangaId ->
historyRepository.observeOne(mangaId)
}.stateIn(viewModelScope, SharingStarted.Eagerly, null)
private val favourite = mangaData.mapNotNull { it?.id }
.distinctUntilChanged()
.flatMapLatest { mangaId ->
favouritesRepository.observeCategoriesIds(mangaId).map { it.isNotEmpty() }
}.stateIn(viewModelScope, SharingStarted.Eagerly, false)
private val newChapters = mangaData.mapNotNull { it?.id }
.distinctUntilChanged()
.mapLatest { mangaId ->
trackingRepository.getNewChaptersCount(mangaId)
}.stateIn(viewModelScope, SharingStarted.Eagerly, 0)
val manga = mangaData.filterNotNull()
.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
val favouriteCategories = favourite
.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
val newChaptersCount = newChapters
.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
val readingHistory = history
.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
val onMangaRemoved = SingleLiveEvent<Manga>()
val chapters = combine(
mangaData.map { it?.chapters.orEmpty() },
history.map { it?.chapterId },
newChapters
) { chapters, currentId, newCount ->
val currentIndex = chapters.indexOfFirst { it.id == currentId }
val firstNewIndex = chapters.size - newCount
chapters.mapIndexed { index, chapter ->
chapter.toListItem(
when {
index >= firstNewIndex -> ChapterExtra.NEW
index == currentIndex -> ChapterExtra.CURRENT
index < currentIndex -> ChapterExtra.READ
else -> ChapterExtra.UNREAD
}
)
}
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
init {
launchLoadingJob(Dispatchers.Default) {
var manga = mangaDataRepository.resolveIntent(intent)
?: throw MangaNotFoundException("Cannot find manga")
mangaData.value = manga
manga = manga.source.repository.getDetails(manga)
mangaData.value = manga
}
}
fun deleteLocal(manga: Manga) {
launchLoadingJob(Dispatchers.Default) {
val original = localMangaRepository.getRemoteManga(manga)
localMangaRepository.delete(manga) || throw IOException("Unable to delete file")
safe {
historyRepository.deleteOrSwap(manga, original)
}
onMangaRemoved.postCall(manga)
}
}
}

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.details
package org.koitharu.kotatsu.details.ui
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
@ -6,12 +6,11 @@ import androidx.viewpager2.adapter.FragmentStateAdapter
class MangaDetailsAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
override fun getItemCount() = 3
override fun getItemCount() = 2
override fun createFragment(position: Int): Fragment = when (position) {
0 -> MangaDetailsFragment()
0 -> DetailsFragment()
1 -> ChaptersFragment()
2 -> RelatedMangaFragment()
else -> throw IndexOutOfBoundsException("No fragment for position $position")
}
}

@ -1,23 +1,29 @@
package org.koitharu.kotatsu.ui.details
package org.koitharu.kotatsu.details.ui.adapter
import android.graphics.Color
import android.view.ViewGroup
import androidx.core.view.isVisible
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateLayoutContainer
import kotlinx.android.synthetic.main.item_chapter.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.domain.history.ChapterExtra
import org.koitharu.kotatsu.ui.base.list.BaseViewHolder
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.history.domain.ChapterExtra
import org.koitharu.kotatsu.utils.ext.getThemeColor
class ChapterHolder(parent: ViewGroup) :
BaseViewHolder<MangaChapter, ChapterExtra>(parent, R.layout.item_chapter) {
fun chapterListItemAD(
clickListener: OnListItemClickListener<MangaChapter>
) = adapterDelegateLayoutContainer<ChapterListItem, ChapterListItem>(R.layout.item_chapter) {
override fun onBind(data: MangaChapter, extra: ChapterExtra) {
textView_title.text = data.name
textView_number.text = data.number.toString()
imageView_check.isVisible = extra == ChapterExtra.CHECKED
when (extra) {
itemView.setOnClickListener {
clickListener.onItemClick(item.chapter, it)
}
itemView.setOnLongClickListener {
clickListener.onItemLongClick(item.chapter, it)
}
bind { payload ->
textView_title.text = item.chapter.name
textView_number.text = item.chapter.number.toString()
when (item.extra) {
ChapterExtra.UNREAD -> {
textView_number.setBackgroundResource(R.drawable.bg_badge_default)
textView_number.setTextColor(context.getThemeColor(android.R.attr.textColorSecondaryInverse))
@ -34,10 +40,6 @@ class ChapterHolder(parent: ViewGroup) :
textView_number.setBackgroundResource(R.drawable.bg_badge_accent)
textView_number.setTextColor(context.getThemeColor(android.R.attr.textColorPrimaryInverse))
}
ChapterExtra.CHECKED -> {
textView_number.background = null
textView_number.setTextColor(Color.TRANSPARENT)
}
}
}
}

@ -0,0 +1,40 @@
package org.koitharu.kotatsu.details.ui.adapter
import androidx.recyclerview.widget.DiffUtil
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import kotlin.jvm.internal.Intrinsics
class ChaptersAdapter(
onItemClickListener: OnListItemClickListener<MangaChapter>
) : AsyncListDifferDelegationAdapter<ChapterListItem>(DiffCallback()) {
init {
setHasStableIds(true)
delegatesManager.addDelegate(chapterListItemAD(onItemClickListener))
}
override fun getItemId(position: Int): Long {
return items[position].chapter.id
}
private class DiffCallback : DiffUtil.ItemCallback<ChapterListItem>() {
override fun areItemsTheSame(oldItem: ChapterListItem, newItem: ChapterListItem): Boolean {
return oldItem.chapter.id == newItem.chapter.id
}
override fun areContentsTheSame(oldItem: ChapterListItem, newItem: ChapterListItem): Boolean {
return Intrinsics.areEqual(oldItem, newItem)
}
override fun getChangePayload(oldItem: ChapterListItem, newItem: ChapterListItem): Any? {
if (oldItem.extra != newItem.extra) {
return newItem.extra
}
return null
}
}
}

@ -0,0 +1,108 @@
package org.koitharu.kotatsu.details.ui.adapter
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import androidx.collection.ArraySet
import androidx.core.content.ContextCompat
import androidx.core.view.children
import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.utils.ext.getThemeColor
import org.koitharu.kotatsu.utils.ext.resolveDp
class ChaptersSelectionDecoration(context: Context) : RecyclerView.ItemDecoration() {
private val icon = ContextCompat.getDrawable(context, R.drawable.ic_check)
private val padding = context.resources.resolveDp(16)
private val bounds = Rect()
private val selection = ArraySet<Long>()
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
init {
paint.color = context.getThemeColor(com.google.android.material.R.attr.scrimBackground)
paint.style = Paint.Style.FILL
}
val checkedItemsCount: Int
get() = selection.size
val checkedItemsIds: Set<Long>
get() = selection
fun toggleItemChecked(id: Long) {
if (!selection.remove(id)) {
selection.add(id)
}
}
fun setItemIsChecked(id: Long, isChecked: Boolean) {
if (isChecked) {
selection.add(id)
} else {
selection.remove(id)
}
}
fun checkAll(ids: Collection<Long>) {
selection.addAll(ids)
}
fun clearSelection() {
selection.clear()
}
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
icon ?: return
canvas.save()
if (parent.clipToPadding) {
canvas.clipRect(
parent.paddingLeft, parent.paddingTop, parent.width - parent.paddingRight,
parent.height - parent.paddingBottom
)
}
for (child in parent.children) {
val itemId = parent.getChildItemId(child)
if (itemId in selection) {
parent.getDecoratedBoundsWithMargins(child, bounds)
bounds.offset(child.translationX.toInt(), child.translationY.toInt())
canvas.drawRect(bounds, paint)
}
}
canvas.restore()
}
override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
icon ?: return
canvas.save()
val left: Int
val right: Int
if (parent.clipToPadding) {
left = parent.paddingLeft
right = parent.width - parent.paddingRight
canvas.clipRect(
left, parent.paddingTop, right,
parent.height - parent.paddingBottom
)
} else {
left = 0
right = parent.width
}
for (child in parent.children) {
val itemId = parent.getChildItemId(child)
if (itemId in selection) {
parent.getDecoratedBoundsWithMargins(child, bounds)
bounds.offset(child.translationX.toInt(), child.translationY.toInt())
val hh = (bounds.height() - icon.intrinsicHeight) / 2
val top: Int = bounds.top + hh
val bottom: Int = bounds.bottom - hh
icon.setBounds(right - icon.intrinsicWidth - padding, top, right - padding, bottom)
icon.draw(canvas)
}
}
canvas.restore()
}
}

@ -0,0 +1,9 @@
package org.koitharu.kotatsu.details.ui.model
import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.history.domain.ChapterExtra
data class ChapterListItem(
val chapter: MangaChapter,
val extra: ChapterExtra
)

@ -0,0 +1,9 @@
package org.koitharu.kotatsu.details.ui.model
import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.history.domain.ChapterExtra
fun MangaChapter.toListItem(extra: ChapterExtra) = ChapterListItem(
chapter = this,
extra = extra
)

@ -1,6 +0,0 @@
package org.koitharu.kotatsu.domain.history
enum class ChapterExtra {
READ, CURRENT, UNREAD, NEW, CHECKED
}

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.download
package org.koitharu.kotatsu.download
import android.app.Notification
import android.app.NotificationChannel
@ -12,7 +12,7 @@ import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.ui.details.MangaDetailsActivity
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import kotlin.math.roundToInt
@ -24,7 +24,8 @@ class DownloadNotification(private val context: Context) {
init {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
&& manager.getNotificationChannel(CHANNEL_ID) == null) {
&& manager.getNotificationChannel(CHANNEL_ID) == null
) {
val channel = NotificationChannel(
CHANNEL_ID,
context.getString(R.string.downloads),
@ -145,7 +146,7 @@ class DownloadNotification(private val context: Context) {
private fun createIntent(context: Context, manga: Manga) = PendingIntent.getActivity(
context,
manga.hashCode(),
MangaDetailsActivity.newIntent(context, manga),
DetailsActivity.newIntent(context, manga),
PendingIntent.FLAG_CANCEL_CURRENT
)
}

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.download
package org.koitharu.kotatsu.download
import android.content.Context
import android.content.Intent
@ -7,6 +7,7 @@ import android.os.PowerManager
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import coil.ImageLoader
import coil.request.ImageRequest
import kotlinx.coroutines.*
@ -19,13 +20,13 @@ import org.koin.android.ext.android.inject
import org.koin.core.context.GlobalContext
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.local.PagesCache
import org.koitharu.kotatsu.base.ui.BaseService
import org.koitharu.kotatsu.base.ui.dialog.CheckBoxAlertDialog
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.parser.LocalMangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.domain.local.MangaZip
import org.koitharu.kotatsu.ui.base.BaseService
import org.koitharu.kotatsu.ui.base.dialog.CheckBoxAlertDialog
import org.koitharu.kotatsu.local.data.MangaZip
import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.utils.CacheUtils
import org.koitharu.kotatsu.utils.ext.*
import java.io.File
@ -54,8 +55,9 @@ class DownloadService : BaseService() {
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "kotatsu:downloading")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
when (intent.action) {
ACTION_DOWNLOAD_START -> {
val manga = intent.getParcelableExtra<Manga>(EXTRA_MANGA)
val chapters = intent.getLongArrayExtra(EXTRA_CHAPTERS_IDS)?.toArraySet()
@ -77,7 +79,7 @@ class DownloadService : BaseService() {
}
private fun downloadManga(manga: Manga, chaptersIds: Set<Long>?, startId: Int): Job {
return serviceScope.launch(Dispatchers.Default) {
return lifecycleScope.launch(Dispatchers.Default) {
mutex.lock()
wakeLock.acquire(TimeUnit.HOURS.toMillis(1))
notification.fillFrom(manga)

@ -0,0 +1,23 @@
package org.koitharu.kotatsu.favourites
import org.koin.android.viewmodel.dsl.viewModel
import org.koin.dsl.module
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.favourites.ui.categories.FavouritesCategoriesViewModel
import org.koitharu.kotatsu.favourites.ui.categories.select.MangaCategoriesViewModel
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListViewModel
val favouritesModule
get() = module {
single { FavouritesRepository(get()) }
viewModel { (categoryId: Long) ->
FavouritesListViewModel(categoryId, get(), get())
}
viewModel { FavouritesCategoriesViewModel(get()) }
viewModel { (manga: Manga) ->
MangaCategoriesViewModel(manga, get())
}
}

@ -1,7 +1,8 @@
package org.koitharu.kotatsu.core.db.dao
package org.koitharu.kotatsu.favourites.data
import androidx.room.*
import org.koitharu.kotatsu.core.db.entity.FavouriteCategoryEntity
import kotlinx.coroutines.flow.Flow
import org.koitharu.kotatsu.core.model.FavouriteCategory
@Dao
abstract class FavouriteCategoriesDao {
@ -9,6 +10,9 @@ abstract class FavouriteCategoriesDao {
@Query("SELECT * FROM favourite_categories ORDER BY sort_key")
abstract suspend fun findAll(): List<FavouriteCategoryEntity>
@Query("SELECT * FROM favourite_categories ORDER BY sort_key")
abstract fun observeAll(): Flow<List<FavouriteCategoryEntity>>
@Insert(onConflict = OnConflictStrategy.ABORT)
abstract suspend fun insert(category: FavouriteCategoryEntity): Long

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.core.db.entity
package org.koitharu.kotatsu.favourites.data
import androidx.room.ColumnInfo
import androidx.room.Entity

@ -1,8 +1,9 @@
package org.koitharu.kotatsu.core.db.entity
package org.koitharu.kotatsu.favourites.data
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import org.koitharu.kotatsu.core.db.entity.MangaEntity
@Entity(
tableName = "favourites", primaryKeys = ["manga_id", "category_id"], foreignKeys = [

@ -1,8 +1,11 @@
package org.koitharu.kotatsu.core.db.entity
package org.koitharu.kotatsu.favourites.data
import androidx.room.Embedded
import androidx.room.Junction
import androidx.room.Relation
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
data class FavouriteManga(
@Embedded val favourite: FavouriteEntity,

@ -1,8 +1,7 @@
package org.koitharu.kotatsu.core.db.dao
package org.koitharu.kotatsu.favourites.data
import androidx.room.*
import org.koitharu.kotatsu.core.db.entity.FavouriteEntity
import org.koitharu.kotatsu.core.db.entity.FavouriteManga
import kotlinx.coroutines.flow.Flow
import org.koitharu.kotatsu.core.db.entity.MangaEntity
@Dao
@ -12,6 +11,10 @@ abstract class FavouritesDao {
@Query("SELECT * FROM favourites GROUP BY manga_id ORDER BY created_at")
abstract suspend fun findAll(): List<FavouriteManga>
@Transaction
@Query("SELECT * FROM favourites GROUP BY manga_id ORDER BY created_at")
abstract fun observeAll(): Flow<List<FavouriteManga>>
@Transaction
@Query("SELECT * FROM favourites GROUP BY manga_id ORDER BY created_at LIMIT :limit OFFSET :offset")
abstract suspend fun findAll(offset: Int, limit: Int): List<FavouriteManga>
@ -20,6 +23,10 @@ abstract class FavouritesDao {
@Query("SELECT * FROM favourites WHERE category_id = :categoryId GROUP BY manga_id ORDER BY created_at")
abstract suspend fun findAll(categoryId: Long): List<FavouriteManga>
@Transaction
@Query("SELECT * FROM favourites WHERE category_id = :categoryId GROUP BY manga_id ORDER BY created_at")
abstract fun observeAll(categoryId: Long): Flow<List<FavouriteManga>>
@Transaction
@Query("SELECT * FROM favourites WHERE category_id = :categoryId GROUP BY manga_id ORDER BY created_at LIMIT :limit OFFSET :offset")
abstract suspend fun findAll(categoryId: Long, offset: Int, limit: Int): List<FavouriteManga>
@ -31,6 +38,13 @@ abstract class FavouritesDao {
@Query("SELECT * FROM favourites WHERE manga_id = :id GROUP BY manga_id")
abstract suspend fun find(id: Long): FavouriteManga?
@Transaction
@Query("SELECT * FROM favourites WHERE manga_id = :id GROUP BY manga_id")
abstract fun observe(id: Long): Flow<FavouriteManga?>
@Query("SELECT DISTINCT category_id FROM favourites WHERE manga_id = :id")
abstract fun observeIds(id: Long): Flow<List<Long>>
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract suspend fun insert(favourite: FavouriteEntity)

@ -1,16 +1,19 @@
package org.koitharu.kotatsu.domain.favourites
package org.koitharu.kotatsu.favourites.domain
import androidx.collection.ArraySet
import androidx.room.withTransaction
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.FavouriteCategoryEntity
import org.koitharu.kotatsu.core.db.entity.FavouriteEntity
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.utils.ext.mapItems
import org.koitharu.kotatsu.utils.ext.mapToSet
class FavouritesRepository(private val db: MangaDatabase) {
@ -20,6 +23,11 @@ class FavouritesRepository(private val db: MangaDatabase) {
return entities.map { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) }
}
fun observeAll(): Flow<List<Manga>> {
return db.favouritesDao.observeAll()
.mapItems { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) }
}
suspend fun getAllManga(offset: Int): List<Manga> {
val entities = db.favouritesDao.findAll(offset, 20)
return entities.map { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) }
@ -30,6 +38,11 @@ class FavouritesRepository(private val db: MangaDatabase) {
return entities.map { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) }
}
fun observeAll(categoryId: Long): Flow<List<Manga>> {
return db.favouritesDao.observeAll(categoryId)
.mapItems { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) }
}
suspend fun getManga(categoryId: Long, offset: Int): List<Manga> {
val entities = db.favouritesDao.findAll(categoryId, offset, 20)
return entities.map { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) }
@ -45,6 +58,22 @@ class FavouritesRepository(private val db: MangaDatabase) {
return entities?.map { it.toFavouriteCategory() }.orEmpty()
}
fun observeCategories(): Flow<List<FavouriteCategory>> {
return db.favouriteCategoriesDao.observeAll().mapItems {
it.toFavouriteCategory()
}
}
fun observeCategories(mangaId: Long): Flow<List<FavouriteCategory>> {
return db.favouritesDao.observe(mangaId).map { entity ->
entity?.categories?.map { it.toFavouriteCategory() }.orEmpty()
}
}
fun observeCategoriesIds(mangaId: Long): Flow<List<Long>> {
return db.favouritesDao.observeIds(mangaId)
}
suspend fun addCategory(title: String): FavouriteCategory {
val entity = FavouriteCategoryEntity(
title = title,

@ -1,5 +1,6 @@
package org.koitharu.kotatsu.domain.favourites
package org.koitharu.kotatsu.favourites.domain
@Deprecated("Use flow")
fun interface OnFavouritesChangeListener {
fun onFavouritesChanged(mangaId: Long)

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.list.favourites
package org.koitharu.kotatsu.favourites.ui
import android.os.Bundle
import android.view.Menu
@ -8,25 +8,22 @@ import android.view.View
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.android.synthetic.main.fragment_favourites.*
import moxy.ktx.moxyPresenter
import org.koin.android.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.domain.favourites.FavouritesRepository
import org.koitharu.kotatsu.domain.favourites.OnFavouritesChangeListener
import org.koitharu.kotatsu.ui.base.BaseFragment
import org.koitharu.kotatsu.ui.list.favourites.categories.CategoriesActivity
import org.koitharu.kotatsu.ui.list.favourites.categories.CategoriesEditDelegate
import org.koitharu.kotatsu.ui.list.favourites.categories.FavouriteCategoriesPresenter
import org.koitharu.kotatsu.ui.list.favourites.categories.FavouriteCategoriesView
import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity
import org.koitharu.kotatsu.favourites.ui.categories.CategoriesEditDelegate
import org.koitharu.kotatsu.favourites.ui.categories.FavouritesCategoriesViewModel
import org.koitharu.kotatsu.utils.ext.showPopupMenu
import java.util.*
import kotlin.collections.ArrayList
class FavouritesContainerFragment : BaseFragment(R.layout.fragment_favourites),
FavouriteCategoriesView, OnFavouritesChangeListener, FavouritesTabLongClickListener,
CategoriesEditDelegate.CategoriesEditCallback {
FavouritesTabLongClickListener, CategoriesEditDelegate.CategoriesEditCallback {
private val viewModel by viewModel<FavouritesCategoriesViewModel>()
private val presenter by moxyPresenter(factory = ::FavouriteCategoriesPresenter)
private val editDelegate by lazy(LazyThreadSafetyMode.NONE) {
CategoriesEditDelegate(requireContext(), this)
}
@ -41,15 +38,12 @@ class FavouritesContainerFragment : BaseFragment(R.layout.fragment_favourites),
val adapter = FavouritesPagerAdapter(this, this)
pager.adapter = adapter
TabLayoutMediator(tabs, pager, adapter).attach()
FavouritesRepository.subscribe(this)
}
override fun onDestroyView() {
FavouritesRepository.unsubscribe(this)
super.onDestroyView()
viewModel.categories.observe(viewLifecycleOwner, ::onCategoriesChanged)
viewModel.onError.observe(viewLifecycleOwner, ::onError)
}
override fun onCategoriesChanged(categories: List<FavouriteCategory>) {
private fun onCategoriesChanged(categories: List<FavouriteCategory>) {
val data = ArrayList<FavouriteCategory>(categories.size + 1)
data += FavouriteCategory(0L, getString(R.string.all_favourites), -1, Date())
data += categories
@ -72,21 +66,13 @@ class FavouritesContainerFragment : BaseFragment(R.layout.fragment_favourites),
}
override fun getTitle(): CharSequence? {
return getString(R.string.favourites)
return context?.getString(R.string.favourites)
}
override fun onCheckedCategoriesChanged(checkedIds: Set<Int>) = Unit
override fun onError(e: Throwable) {
private fun onError(e: Throwable) {
Snackbar.make(pager, e.message ?: return, Snackbar.LENGTH_LONG).show()
}
override fun onFavouritesChanged(mangaId: Long) = Unit
override fun onCategoriesChanged() {
presenter.loadAllCategories()
}
override fun onTabLongClick(tabView: View, category: FavouriteCategory): Boolean {
val menuRes = if (category.id == 0L) R.menu.popup_category_empty else R.menu.popup_category
tabView.showPopupMenu(menuRes) {
@ -101,15 +87,15 @@ class FavouritesContainerFragment : BaseFragment(R.layout.fragment_favourites),
}
override fun onDeleteCategory(category: FavouriteCategory) {
presenter.deleteCategory(category.id)
viewModel.deleteCategory(category.id)
}
override fun onRenameCategory(category: FavouriteCategory, newName: String) {
presenter.renameCategory(category.id, newName)
viewModel.renameCategory(category.id, newName)
}
override fun onCreateCategory(name: String) {
presenter.createCategory(name)
viewModel.createCategory(name)
}
companion object {

@ -1,12 +1,13 @@
package org.koitharu.kotatsu.ui.list.favourites
package org.koitharu.kotatsu.favourites.ui
import android.view.View
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import org.koitharu.kotatsu.base.ui.list.AdapterUpdater
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.ui.base.list.AdapterUpdater
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment
import org.koitharu.kotatsu.utils.ext.replaceWith
class FavouritesPagerAdapter(

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.list.favourites
package org.koitharu.kotatsu.favourites.ui
import android.view.View
import org.koitharu.kotatsu.core.model.FavouriteCategory

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.list.favourites.categories
package org.koitharu.kotatsu.favourites.ui.categories
import android.content.Context
import android.content.Intent
@ -12,18 +12,18 @@ import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_categories.*
import moxy.ktx.moxyPresenter
import org.koin.android.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.ui.base.BaseActivity
import org.koitharu.kotatsu.ui.base.list.OnRecyclerItemClickListener
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.showPopupMenu
class CategoriesActivity : BaseActivity(), OnRecyclerItemClickListener<FavouriteCategory>,
FavouriteCategoriesView, View.OnClickListener, CategoriesEditDelegate.CategoriesEditCallback {
class CategoriesActivity : BaseActivity(), OnListItemClickListener<FavouriteCategory>,
View.OnClickListener, CategoriesEditDelegate.CategoriesEditCallback {
private val presenter by moxyPresenter(factory = ::FavouriteCategoriesPresenter)
private val viewModel by viewModel<FavouritesCategoriesViewModel>()
private lateinit var adapter: CategoriesAdapter
private lateinit var reorderHelper: ItemTouchHelper
@ -41,6 +41,9 @@ class CategoriesActivity : BaseActivity(), OnRecyclerItemClickListener<Favourite
fab_add.setOnClickListener(this)
reorderHelper = ItemTouchHelper(ReorderHelperCallback())
reorderHelper.attachToRecyclerView(recyclerView)
viewModel.categories.observe(this, ::onCategoriesChanged)
viewModel.onError.observe(this, ::onError)
}
override fun onClick(v: View) {
@ -49,7 +52,7 @@ class CategoriesActivity : BaseActivity(), OnRecyclerItemClickListener<Favourite
}
}
override fun onItemClick(item: FavouriteCategory, position: Int, view: View) {
override fun onItemClick(item: FavouriteCategory, view: View) {
view.showPopupMenu(R.menu.popup_category) {
when (it.itemId) {
R.id.action_remove -> editDelegate.deleteCategory(item)
@ -59,35 +62,33 @@ class CategoriesActivity : BaseActivity(), OnRecyclerItemClickListener<Favourite
}
}
override fun onItemLongClick(item: FavouriteCategory, position: Int, view: View): Boolean {
override fun onItemLongClick(item: FavouriteCategory, view: View): Boolean {
reorderHelper.startDrag(
recyclerView.findViewHolderForAdapterPosition(position) ?: return false
recyclerView.findContainingViewHolder(view) ?: return false
)
return true
}
override fun onCategoriesChanged(categories: List<FavouriteCategory>) {
adapter.replaceData(categories)
private fun onCategoriesChanged(categories: List<FavouriteCategory>) {
adapter.items = categories
textView_holder.isVisible = categories.isEmpty()
}
override fun onCheckedCategoriesChanged(checkedIds: Set<Int>) = Unit
override fun onError(e: Throwable) {
private fun onError(e: Throwable) {
Snackbar.make(recyclerView, e.getDisplayMessage(resources), Snackbar.LENGTH_LONG)
.show()
}
override fun onDeleteCategory(category: FavouriteCategory) {
presenter.deleteCategory(category.id)
viewModel.deleteCategory(category.id)
}
override fun onRenameCategory(category: FavouriteCategory, newName: String) {
presenter.renameCategory(category.id, newName)
viewModel.renameCategory(category.id, newName)
}
override fun onCreateCategory(name: String) {
presenter.createCategory(name)
viewModel.createCategory(name)
}
private inner class ReorderHelperCallback : ItemTouchHelper.SimpleCallback(
@ -101,8 +102,7 @@ class CategoriesActivity : BaseActivity(), OnRecyclerItemClickListener<Favourite
): Boolean {
val oldPos = viewHolder.bindingAdapterPosition
val newPos = target.bindingAdapterPosition
adapter.moveItem(oldPos, newPos)
presenter.storeCategoriesOrder(adapter.items.map { it.id })
viewModel.reorderCategories(oldPos, newPos)
return true
}

@ -0,0 +1,27 @@
package org.koitharu.kotatsu.favourites.ui.categories
import androidx.recyclerview.widget.DiffUtil
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.FavouriteCategory
import kotlin.jvm.internal.Intrinsics
class CategoriesAdapter(
onItemClickListener: OnListItemClickListener<FavouriteCategory>
) : AsyncListDifferDelegationAdapter<FavouriteCategory>(DiffCallback()) {
init {
delegatesManager.addDelegate(categoryAD(onItemClickListener))
}
private class DiffCallback : DiffUtil.ItemCallback<FavouriteCategory>() {
override fun areItemsTheSame(oldItem: FavouriteCategory, newItem: FavouriteCategory): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: FavouriteCategory, newItem: FavouriteCategory): Boolean {
return Intrinsics.areEqual(oldItem, newItem)
}
}
}

@ -1,11 +1,11 @@
package org.koitharu.kotatsu.ui.list.favourites.categories
package org.koitharu.kotatsu.favourites.ui.categories
import android.content.Context
import android.text.InputType
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import androidx.appcompat.app.AlertDialog
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.dialog.TextInputDialog
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.ui.base.dialog.TextInputDialog
class CategoriesEditDelegate(
private val context: Context,
@ -13,7 +13,7 @@ class CategoriesEditDelegate(
) {
fun deleteCategory(category: FavouriteCategory) {
MaterialAlertDialogBuilder(context)
AlertDialog.Builder(context)
.setMessage(context.getString(R.string.category_delete_confirm, category.title))
.setTitle(R.string.remove_category)
.setNegativeButton(android.R.string.cancel, null)

@ -0,0 +1,29 @@
package org.koitharu.kotatsu.favourites.ui.categories
import android.view.MotionEvent
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateLayoutContainer
import kotlinx.android.synthetic.main.item_category.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.FavouriteCategory
fun categoryAD(
clickListener: OnListItemClickListener<FavouriteCategory>
) = adapterDelegateLayoutContainer<FavouriteCategory, FavouriteCategory>(R.layout.item_category) {
imageView_more.setOnClickListener {
clickListener.onItemClick(item, it)
}
@Suppress("ClickableViewAccessibility")
imageView_handle.setOnTouchListener { v, event ->
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
clickListener.onItemLongClick(item, itemView)
} else {
false
}
}
bind {
textView_title.text = item.title
}
}

@ -0,0 +1,48 @@
package org.koitharu.kotatsu.favourites.ui.categories
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
class FavouritesCategoriesViewModel(
private val repository: FavouritesRepository
) : BaseViewModel() {
private var reorderJob: Job? = null
val categories = repository.observeCategories()
.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
fun createCategory(name: String) {
launchJob(Dispatchers.Default) {
repository.addCategory(name)
}
}
fun renameCategory(id: Long, name: String) {
launchJob(Dispatchers.Default) {
repository.renameCategory(id, name)
}
}
fun deleteCategory(id: Long) {
launchJob(Dispatchers.Default) {
repository.removeCategory(id)
}
}
fun reorderCategories(oldPos: Int, newPos: Int) {
val prevJob = reorderJob
reorderJob = launchJob(Dispatchers.Default) {
prevJob?.join()
val items = categories.value ?: error("This should not happen")
val ids = items.mapTo(ArrayList(items.size)) { it.id }
val item = ids.removeAt(oldPos)
ids.add(newPos, item)
repository.reorderCategories(ids)
}
}
}

@ -0,0 +1,85 @@
package org.koitharu.kotatsu.favourites.ui.categories.select
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.fragment.app.FragmentManager
import kotlinx.android.synthetic.main.dialog_favorite_categories.*
import org.koin.android.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.BaseBottomSheet
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.favourites.ui.categories.CategoriesEditDelegate
import org.koitharu.kotatsu.favourites.ui.categories.select.adapter.MangaCategoriesAdapter
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.withArgs
class FavouriteCategoriesDialog : BaseBottomSheet(R.layout.dialog_favorite_categories),
OnListItemClickListener<MangaCategoryItem>, CategoriesEditDelegate.CategoriesEditCallback,
View.OnClickListener {
private val viewModel by viewModel<MangaCategoriesViewModel> {
parametersOf(requireNotNull(arguments?.getParcelable<Manga>(MangaIntent.KEY_MANGA)))
}
private var adapter: MangaCategoriesAdapter? = null
private val editDelegate by lazy(LazyThreadSafetyMode.NONE) {
CategoriesEditDelegate(requireContext(), this@FavouriteCategoriesDialog)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
adapter = MangaCategoriesAdapter(this)
recyclerView_categories.adapter = adapter
textView_add.setOnClickListener(this)
viewModel.content.observe(viewLifecycleOwner, this::onContentChanged)
viewModel.onError.observe(viewLifecycleOwner, ::onError)
}
override fun onDestroyView() {
adapter = null
super.onDestroyView()
}
override fun onClick(v: View) {
when (v.id) {
R.id.textView_add -> editDelegate.createCategory()
}
}
override fun onItemClick(item: MangaCategoryItem, view: View) {
viewModel.setChecked(item.id, !item.isChecked)
}
override fun onDeleteCategory(category: FavouriteCategory) = Unit
override fun onRenameCategory(category: FavouriteCategory, newName: String) = Unit
override fun onCreateCategory(name: String) {
viewModel.createCategory(name)
}
private fun onContentChanged(categories: List<MangaCategoryItem>) {
adapter?.items = categories
}
private fun onError(e: Throwable) {
Toast.makeText(context ?: return, e.getDisplayMessage(resources), Toast.LENGTH_SHORT).show()
}
companion object {
private const val TAG = "FavouriteCategoriesDialog"
fun show(fm: FragmentManager, manga: Manga) = FavouriteCategoriesDialog()
.withArgs(1) {
putParcelable(MangaIntent.KEY_MANGA, manga)
}.show(fm, TAG)
}
}

@ -0,0 +1,45 @@
package org.koitharu.kotatsu.favourites.ui.categories.select
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
class MangaCategoriesViewModel(
private val manga: Manga,
private val favouritesRepository: FavouritesRepository
) : BaseViewModel() {
val content = combine(
favouritesRepository.observeCategories(),
favouritesRepository.observeCategoriesIds(manga.id)
) { all, checked ->
all.map {
MangaCategoryItem(
id = it.id,
name = it.title,
isChecked = it.id in checked
)
}
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
fun setChecked(categoryId: Long, isChecked: Boolean) {
launchJob(Dispatchers.Default) {
if (isChecked) {
favouritesRepository.addToCategory(manga, categoryId)
} else {
favouritesRepository.removeFromCategory(manga, categoryId)
}
}
}
fun createCategory(name: String) {
launchJob(Dispatchers.Default) {
favouritesRepository.addCategory(name)
}
}
}

@ -0,0 +1,23 @@
package org.koitharu.kotatsu.favourites.ui.categories.select.adapter
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateLayoutContainer
import kotlinx.android.synthetic.main.item_category_checkable.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
fun mangaCategoryAD(
clickListener: OnListItemClickListener<MangaCategoryItem>
) = adapterDelegateLayoutContainer<MangaCategoryItem, MangaCategoryItem>(
R.layout.item_category_checkable
) {
itemView.setOnClickListener {
clickListener.onItemClick(item, itemView)
}
bind {
checkedTextView.text = item.name
checkedTextView.isChecked = item.isChecked
}
}

@ -0,0 +1,37 @@
package org.koitharu.kotatsu.favourites.ui.categories.select.adapter
import androidx.recyclerview.widget.DiffUtil
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
class MangaCategoriesAdapter(
clickListener: OnListItemClickListener<MangaCategoryItem>
) : AsyncListDifferDelegationAdapter<MangaCategoryItem>(DiffCallback()) {
init {
delegatesManager.addDelegate(mangaCategoryAD(clickListener))
}
private class DiffCallback : DiffUtil.ItemCallback<MangaCategoryItem>() {
override fun areItemsTheSame(
oldItem: MangaCategoryItem,
newItem: MangaCategoryItem
): Boolean = oldItem.id == newItem.id
override fun areContentsTheSame(
oldItem: MangaCategoryItem,
newItem: MangaCategoryItem
): Boolean = oldItem == newItem
override fun getChangePayload(
oldItem: MangaCategoryItem,
newItem: MangaCategoryItem
): Any? {
if (oldItem.isChecked != newItem.isChecked) {
return newItem.isChecked
}
return super.getChangePayload(oldItem, newItem)
}
}
}

@ -0,0 +1,7 @@
package org.koitharu.kotatsu.favourites.ui.categories.select.model
data class MangaCategoryItem(
val id: Long,
val name: String,
val isChecked: Boolean
)

@ -1,29 +1,28 @@
package org.koitharu.kotatsu.ui.list.favourites
package org.koitharu.kotatsu.favourites.ui.list
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import kotlinx.android.synthetic.main.fragment_list.*
import moxy.ktx.moxyPresenter
import org.koin.android.ext.android.get
import org.koin.android.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.ui.list.MangaListFragment
import org.koitharu.kotatsu.ui.list.MangaListView
import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.utils.ext.withArgs
class FavouritesListFragment : MangaListFragment<Unit>(), MangaListView<Unit> {
class FavouritesListFragment : MangaListFragment() {
private val presenter by moxyPresenter {
FavouritesListPresenter(categoryId, get())
override val viewModel by viewModel<FavouritesListViewModel> {
parametersOf(categoryId)
}
private val categoryId: Long
get() = arguments?.getLong(ARG_CATEGORY_ID) ?: 0L
override fun onRequestMoreItems(offset: Int) {
presenter.loadList(offset)
}
override val isSwipeRefreshEnabled = false
override fun onScrolledToEnd() = Unit
override fun setUpEmptyListHolder() {
textView_holder.setText(
@ -43,7 +42,7 @@ class FavouritesListFragment : MangaListFragment<Unit>(), MangaListView<Unit> {
override fun onPopupMenuItemSelected(item: MenuItem, data: Manga) = when (item.itemId) {
R.id.action_remove -> {
presenter.removeFromFavourites(data)
viewModel.removeFromFavourites(data)
true
}
else -> super.onPopupMenuItemSelected(item, data)

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

Loading…
Cancel
Save