Merge branch 'feature/mvvm' into devel

pull/26/head
Koitharu 5 years ago
commit b1be45af8b

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

@ -3,6 +3,7 @@ plugins {
id 'kotlin-android' id 'kotlin-android'
id 'kotlin-android-extensions' id 'kotlin-android-extensions'
id 'kotlin-kapt' id 'kotlin-kapt'
// TODO id 'kotlin-parcelize'
} }
def gitCommits = 'git rev-list --count HEAD'.execute([], rootDir).text.trim().toInteger() 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.activity:activity-ktx:1.2.0-beta01'
implementation 'androidx.fragment:fragment-ktx:1.3.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-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.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha06' implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha06'
@ -84,24 +86,22 @@ dependencies {
implementation 'androidx.room:room-ktx:2.2.5' implementation 'androidx.room:room-ktx:2.2.5'
kapt 'androidx.room:room-compiler: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.okhttp3:okhttp:4.9.0'
implementation 'com.squareup.okio:okio:2.9.0' implementation 'com.squareup.okio:okio:2.9.0'
implementation 'org.jsoup:jsoup:1.13.1' 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 '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.davemorrissey.labs:subsampling-scale-image-view:3.10.0'
implementation 'com.tomclaw.cache:cache:1.0' implementation 'com.tomclaw.cache:cache:1.0'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.5' debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.5'
testImplementation 'junit:junit:4.13.1' 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' testImplementation 'org.koin:koin-test:2.2.0-rc-2'
} }

@ -23,7 +23,7 @@
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
tools:ignore="UnusedAttribute"> tools:ignore="UnusedAttribute">
<activity android:name=".ui.list.MainActivity"> <activity android:name="org.koitharu.kotatsu.main.ui.MainActivity">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
@ -32,62 +32,62 @@
android:name="android.app.default_searchable" android:name="android.app.default_searchable"
android:value=".ui.search.SearchActivity" /> android:value=".ui.search.SearchActivity" />
</activity> </activity>
<activity android:name=".ui.details.MangaDetailsActivity"> <activity android:name="org.koitharu.kotatsu.details.ui.DetailsActivity">
<intent-filter> <intent-filter>
<action android:name="${applicationId}.action.VIEW_MANGA" /> <action android:name="${applicationId}.action.VIEW_MANGA" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".ui.reader.ReaderActivity" /> <activity android:name="org.koitharu.kotatsu.reader.ui.ReaderActivity" />
<activity <activity
android:name=".ui.search.SearchActivity" android:name="org.koitharu.kotatsu.search.ui.SearchActivity"
android:label="@string/search" /> android:label="@string/search" />
<activity <activity
android:name=".ui.settings.SettingsActivity" android:name="org.koitharu.kotatsu.settings.SettingsActivity"
android:label="@string/settings" /> android:label="@string/settings" />
<activity <activity
android:name=".ui.reader.SimpleSettingsActivity" android:name="org.koitharu.kotatsu.reader.ui.SimpleSettingsActivity"
android:label="@string/settings"> android:label="@string/settings">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MANAGE_NETWORK_USAGE" /> <action android:name="android.intent.action.MANAGE_NETWORK_USAGE" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".ui.browser.BrowserActivity" /> <activity android:name="org.koitharu.kotatsu.browser.BrowserActivity" />
<activity <activity
android:name=".ui.utils.CrashActivity" android:name="org.koitharu.kotatsu.core.ui.CrashActivity"
android:label="@string/error_occurred" android:label="@string/error_occurred"
android:theme="@android:style/Theme.DeviceDefault" android:theme="@android:style/Theme.DeviceDefault"
android:windowSoftInputMode="stateAlwaysHidden" /> android:windowSoftInputMode="stateAlwaysHidden" />
<activity <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:label="@string/favourites_categories"
android:windowSoftInputMode="stateAlwaysHidden" /> android:windowSoftInputMode="stateAlwaysHidden" />
<activity <activity
android:name=".ui.widget.shelf.ShelfConfigActivity" android:name="org.koitharu.kotatsu.widget.shelf.ShelfConfigActivity"
android:label="@string/manga_shelf"> android:label="@string/manga_shelf">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" /> <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name=".ui.search.global.GlobalSearchActivity" android:name="org.koitharu.kotatsu.search.ui.global.GlobalSearchActivity"
android:label="@string/search" /> android:label="@string/search" />
<activity <activity
android:name=".ui.utils.protect.ProtectActivity" android:name="org.koitharu.kotatsu.main.ui.protect.ProtectActivity"
android:windowSoftInputMode="adjustResize" /> android:windowSoftInputMode="adjustResize" />
<service <service
android:name=".ui.download.DownloadService" android:name="org.koitharu.kotatsu.download.DownloadService"
android:foregroundServiceType="dataSync" /> android:foregroundServiceType="dataSync" />
<service <service
android:name=".ui.widget.shelf.ShelfWidgetService" android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS" /> android:permission="android.permission.BIND_REMOTEVIEWS" />
<service <service
android:name=".ui.widget.recent.RecentWidgetService" android:name="org.koitharu.kotatsu.widget.recent.RecentWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS" /> android:permission="android.permission.BIND_REMOTEVIEWS" />
<provider <provider
android:name=".ui.search.MangaSuggestionsProvider" android:name="org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider"
android:authorities="${applicationId}.MangaSuggestionsProvider" android:authorities="${applicationId}.MangaSuggestionsProvider"
android:exported="false" /> android:exported="false" />
<provider <provider
@ -101,7 +101,7 @@
</provider> </provider>
<receiver <receiver
android:name=".ui.widget.shelf.ShelfWidgetProvider" android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetProvider"
android:label="@string/manga_shelf"> android:label="@string/manga_shelf">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@ -111,7 +111,7 @@
android:resource="@xml/widget_shelf" /> android:resource="@xml/widget_shelf" />
</receiver> </receiver>
<receiver <receiver
android:name=".ui.widget.recent.RecentWidgetProvider" android:name="org.koitharu.kotatsu.widget.recent.RecentWidgetProvider"
android:label="@string/recent_manga"> android:label="@string/recent_manga">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <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.android.get
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin import org.koin.core.context.startKoin
import org.koin.dsl.module import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.backup.BackupRepository
import org.koitharu.kotatsu.core.backup.RestoreRepository
import org.koitharu.kotatsu.core.db.databaseModule import org.koitharu.kotatsu.core.db.databaseModule
import org.koitharu.kotatsu.core.github.githubModule 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.network.networkModule
import org.koitharu.kotatsu.core.parser.LocalMangaRepository
import org.koitharu.kotatsu.core.parser.parserModule import org.koitharu.kotatsu.core.parser.parserModule
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.domain.MangaDataRepository import org.koitharu.kotatsu.core.ui.AppCrashHandler
import org.koitharu.kotatsu.domain.MangaLoaderContext import org.koitharu.kotatsu.core.ui.uiModule
import org.koitharu.kotatsu.domain.MangaSearchRepository import org.koitharu.kotatsu.details.detailsModule
import org.koitharu.kotatsu.domain.favourites.FavouritesRepository import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.domain.history.HistoryRepository import org.koitharu.kotatsu.favourites.favouritesModule
import org.koitharu.kotatsu.domain.tracking.TrackingRepository import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.ui.base.uiModule import org.koitharu.kotatsu.history.historyModule
import org.koitharu.kotatsu.ui.utils.AppCrashHandler import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.ui.widget.WidgetUpdater 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() { class KotatsuApp : Application() {
@ -62,20 +67,19 @@ class KotatsuApp : Application() {
networkModule, networkModule,
databaseModule, databaseModule,
githubModule, githubModule,
parserModule,
uiModule, uiModule,
module { parserModule,
single { FavouritesRepository(get()) } mainModule,
single { HistoryRepository(get()) } searchModule,
single { TrackingRepository(get(), get()) } localModule,
single { MangaDataRepository(get()) } favouritesModule,
single { BackupRepository(get()) } historyModule,
single { RestoreRepository(get()) } remoteListModule,
single { MangaSearchRepository() } detailsModule,
single { MangaLoaderContext() } trackerModule,
single { AppSettings(get()) } settingsModule,
single { PagesCache(get()) } readerModule,
} appWidgetModule
) )
} }
} }

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.domain package org.koitharu.kotatsu.base.domain
import androidx.room.withTransaction import androidx.room.withTransaction
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
@ -32,6 +32,12 @@ class MangaDataRepository(private val db: MangaDatabase) {
return db.mangaDao.find(mangaId)?.toManga() 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) { suspend fun storeManga(manga: Manga) {
val tags = manga.tags.map(TagEntity.Companion::fromMangaTag) val tags = manga.tags.map(TagEntity.Companion::fromMangaTag)
db.withTransaction { 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 okhttp3.*
import org.koin.core.component.KoinComponent 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.KoinComponent
import org.koin.core.component.get 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.graphics.BitmapFactory
import android.net.Uri 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.app.Dialog
import android.os.Bundle import android.os.Bundle
@ -6,11 +6,11 @@ import android.view.View
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.annotation.LayoutRes import androidx.annotation.LayoutRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import moxy.MvpAppCompatDialogFragment import androidx.fragment.app.DialogFragment
abstract class AlertDialogFragment( abstract class AlertDialogFragment(
@LayoutRes private val layoutResId: Int @LayoutRes private val layoutResId: Int
) : MvpAppCompatDialogFragment() { ) : DialogFragment() {
private var rootView: View? = null 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.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import moxy.MvpAppCompatActivity
import org.koin.android.ext.android.get import org.koin.android.ext.android.get
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
abstract class BaseActivity : MvpAppCompatActivity() { abstract class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
if (get<AppSettings>().isAmoledTheme) { 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.app.Dialog
import android.os.Bundle import android.os.Bundle
@ -7,11 +7,11 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.LayoutRes import androidx.annotation.LayoutRes
import androidx.appcompat.app.AppCompatDialog import androidx.appcompat.app.AppCompatDialog
import moxy.MvpBottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.koitharu.kotatsu.utils.UiUtils import org.koitharu.kotatsu.utils.UiUtils
abstract class BaseBottomSheet(@LayoutRes private val layoutResId: Int) : abstract class BaseBottomSheet(@LayoutRes private val layoutResId: Int) :
MvpBottomSheetDialogFragment() { BottomSheetDialogFragment() {
final override fun onCreateView( final override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,

@ -1,14 +1,14 @@
package org.koitharu.kotatsu.ui.base package org.koitharu.kotatsu.base.ui
import android.content.Context import android.content.Context
import androidx.annotation.LayoutRes import androidx.annotation.LayoutRes
import androidx.fragment.app.Fragment
import coil.ImageLoader import coil.ImageLoader
import moxy.MvpAppCompatFragment
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
abstract class BaseFragment( abstract class BaseFragment(
@LayoutRes contentLayoutId: Int @LayoutRes contentLayoutId: Int
) : MvpAppCompatFragment(contentLayoutId) { ) : Fragment(contentLayoutId) {
protected val coil by inject<ImageLoader>() 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.graphics.Color
import android.os.Build 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.annotation.StringRes
import androidx.preference.PreferenceFragmentCompat 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 kotlinx.coroutines.*
import moxy.MvpPresenter
import moxy.presenterScope
import org.koin.core.component.KoinComponent
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.utils.SingleLiveEvent
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext 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( protected fun launchJob(
context: CoroutineContext = EmptyCoroutineContext, context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT, start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit block: suspend CoroutineScope.() -> Unit
) { ): Job = viewModelScope.launch(context + createErrorHandler(), start, block)
presenterScope.launch(context + createErrorHandler(), start, block)
}
protected fun launchLoadingJob( protected fun launchLoadingJob(
context: CoroutineContext = EmptyCoroutineContext, context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT, start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit block: suspend CoroutineScope.() -> Unit
) { ): Job = viewModelScope.launch(context + createErrorHandler(), start) {
presenterScope.launch(context + createErrorHandler(), start) { isLoading.postValue(true)
viewState.onLoadingStateChanged(isLoading = true) try {
try { block()
block() } finally {
} finally { isLoading.postValue(false)
viewState.onLoadingStateChanged(isLoading = false)
}
} }
} }
@ -38,7 +38,7 @@ abstract class BasePresenter<V : BaseMvpView> : MvpPresenter<V>(), KoinComponent
throwable.printStackTrace() throwable.printStackTrace()
} }
if (throwable !is CancellationException) { 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.annotation.SuppressLint
import android.content.Context import android.content.Context
@ -8,7 +8,6 @@ import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.google.android.material.checkbox.MaterialCheckBox import com.google.android.material.checkbox.MaterialCheckBox
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
class CheckBoxAlertDialog private constructor(private val delegate: AlertDialog) : 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) .inflate(R.layout.dialog_checkbox, null, false)
private val checkBox = view.findViewById<MaterialCheckBox>(android.R.id.checkbox) private val checkBox = view.findViewById<MaterialCheckBox>(android.R.id.checkbox)
private val delegate = MaterialAlertDialogBuilder(context) private val delegate = AlertDialog.Builder(context)
.setView(view) .setView(view)
fun setTitle(@StringRes titleResId: Int): Builder { 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.Context
import android.content.DialogInterface import android.content.DialogInterface
@ -7,10 +7,9 @@ import android.view.ViewGroup
import android.widget.BaseAdapter import android.widget.BaseAdapter
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.android.synthetic.main.item_storage.view.* import kotlinx.android.synthetic.main.item_storage.view.*
import org.koitharu.kotatsu.R 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.getStorageName
import org.koitharu.kotatsu.utils.ext.inflate import org.koitharu.kotatsu.utils.ext.inflate
import org.koitharu.kotatsu.utils.ext.longHashCode 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) { class Builder(context: Context, defaultValue: File?, listener: OnStorageSelectListener) {
private val adapter = VolumesAdapter(context) private val adapter = VolumesAdapter(context)
private val delegate = MaterialAlertDialogBuilder(context) private val delegate = AlertDialog.Builder(context)
init { init {
if (adapter.isEmpty) { 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.annotation.SuppressLint
import android.content.Context import android.content.Context
@ -7,7 +7,6 @@ import android.text.InputFilter
import android.view.LayoutInflater import android.view.LayoutInflater
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.android.synthetic.main.dialog_input.view.* import kotlinx.android.synthetic.main.dialog_input.view.*
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
@ -23,7 +22,7 @@ class TextInputDialog private constructor(
private val view = LayoutInflater.from(context) private val view = LayoutInflater.from(context)
.inflate(R.layout.dialog_input, null, false) .inflate(R.layout.dialog_input, null, false)
private val delegate = MaterialAlertDialogBuilder(context) private val delegate = AlertDialog.Builder(context)
.setView(view) .setView(view)
fun setTitle(@StringRes titleResId: Int): Builder { 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.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import java.util.* import java.util.*
@Deprecated("")
class AdapterUpdater<T>(oldList: List<T>, newList: List<T>, getId: (T) -> Long) { class AdapterUpdater<T>(oldList: List<T>, newList: List<T>, getId: (T) -> Long) {
private val diff = DiffUtil.calculateDiff(object : DiffUtil.Callback() { 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.View
import android.view.ViewGroup 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()") 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 open fun onRecycled() = Unit
abstract fun onBind(data: T, extra: E) 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.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView 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.View
import android.view.ViewGroup 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.content.Context
import android.graphics.Canvas 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.Canvas
import android.graphics.Rect 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.graphics.Rect
import android.view.View 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.content.Context
import android.util.AttributeSet 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.content.Context
import android.util.AttributeSet 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.content.Context
import android.util.AttributeSet 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.annotation.SuppressLint
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
@ -11,7 +11,7 @@ import android.view.MenuItem
import androidx.core.view.isVisible import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.activity_browser.* import kotlinx.android.synthetic.main.activity_browser.*
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.ui.base.BaseActivity import org.koitharu.kotatsu.base.ui.BaseActivity
@SuppressLint("SetJavaScriptEnabled") @SuppressLint("SetJavaScriptEnabled")
class BrowserActivity : BaseActivity(), BrowserCallback { class BrowserActivity : BaseActivity(), BrowserCallback {

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

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

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.utils.cloudflare package org.koitharu.kotatsu.browser.cloudflare
interface CloudFlareCallback { 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.graphics.Bitmap
import android.webkit.CookieManager 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.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
@ -10,8 +10,8 @@ import androidx.core.view.isInvisible
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import kotlinx.android.synthetic.main.fragment_cloudflare.* import kotlinx.android.synthetic.main.fragment_cloudflare.*
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
import org.koitharu.kotatsu.core.network.UserAgentInterceptor 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.stringArgument
import org.koitharu.kotatsu.utils.ext.withArgs import org.koitharu.kotatsu.utils.ext.withArgs

@ -4,7 +4,11 @@ import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.* import org.koitharu.kotatsu.core.db.entity.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) { class BackupRepository(private val db: MangaDatabase) {

@ -3,7 +3,11 @@ package org.koitharu.kotatsu.core.backup
import androidx.room.withTransaction import androidx.room.withTransaction
import org.json.JSONObject import org.json.JSONObject
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.* import org.koitharu.kotatsu.core.db.entity.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.getStringOrNull
import org.koitharu.kotatsu.utils.ext.iterator import org.koitharu.kotatsu.utils.ext.iterator
import org.koitharu.kotatsu.utils.ext.map import org.koitharu.kotatsu.utils.ext.map

@ -4,6 +4,12 @@ import androidx.room.Database
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import org.koitharu.kotatsu.core.db.dao.* import org.koitharu.kotatsu.core.db.dao.*
import org.koitharu.kotatsu.core.db.entity.* 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( @Database(
entities = [ entities = [

@ -15,7 +15,7 @@ import androidx.room.PrimaryKey
) )
] ]
) )
data class TrackEntity ( data class TrackEntity(
@PrimaryKey(autoGenerate = false) @PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "manga_id") val mangaId: Long, @ColumnInfo(name = "manga_id") val mangaId: Long,
@ColumnInfo(name = "chapters_total") val totalChapters: Int, @ColumnInfo(name = "chapters_total") val totalChapters: Int,

@ -1,3 +1,4 @@
package org.koitharu.kotatsu.core.exceptions 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 kotlinx.android.parcel.Parcelize
import org.koin.core.context.GlobalContext import org.koin.core.context.GlobalContext
import org.koin.core.error.NoBeanDefFoundException 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.MangaRepository
import org.koitharu.kotatsu.core.parser.site.* import org.koitharu.kotatsu.core.parser.site.*
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
@Suppress("SpellCheckingInspection") @Suppress("SpellCheckingInspection")
@Parcelize @Parcelize
@ -30,6 +31,7 @@ enum class MangaSource(
// HENTAILIB("HentaiLib", "ru", HentaiLibRepository::class.java) // HENTAILIB("HentaiLib", "ru", HentaiLibRepository::class.java)
@get:Throws(NoBeanDefFoundException::class) @get:Throws(NoBeanDefFoundException::class)
@Deprecated("")
val repository: MangaRepository val repository: MangaRepository
get() = GlobalContext.get().get(cls.kotlin) get() = GlobalContext.get().get(named(this))
} }

@ -5,10 +5,10 @@ import kotlinx.android.parcel.Parcelize
import java.util.* import java.util.*
@Parcelize @Parcelize
data class MangaTracking ( data class MangaTracking(
val manga: Manga, val manga: Manga,
val knownChaptersCount: Int, val knownChaptersCount: Int,
val lastChapterId: Long, val lastChapterId: Long,
val lastNotifiedChapterId: Long, val lastNotifiedChapterId: Long,
val lastCheck: Date? val lastCheck: Date?
): Parcelable ) : Parcelable

@ -5,9 +5,9 @@ import kotlinx.android.parcel.Parcelize
import java.util.* import java.util.*
@Parcelize @Parcelize
data class TrackingLogItem ( data class TrackingLogItem(
val id: Long, val id: Long,
val manga: Manga, val manga: Manga,
val chapters: List<String>, val chapters: List<String>,
val createdAt: Date val createdAt: Date
): Parcelable ) : Parcelable

@ -6,13 +6,18 @@ interface MangaRepository {
val sortOrders: Set<SortOrder> 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 suspend fun getDetails(manga: Manga): Manga
suspend fun getPages(chapter: MangaChapter) : List<MangaPage> suspend fun getPages(chapter: MangaChapter): List<MangaPage>
suspend fun getPageFullUrl(page: MangaPage) : String suspend fun getPageFullUrl(page: MangaPage): String
suspend fun getTags(): Set<MangaTag> suspend fun getTags(): Set<MangaTag>
} }

@ -1,22 +1,25 @@
package org.koitharu.kotatsu.core.parser package org.koitharu.kotatsu.core.parser
import org.koin.dsl.bind import org.koin.core.qualifier.named
import org.koin.dsl.module 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.* import org.koitharu.kotatsu.core.parser.site.*
val parserModule val parserModule
get() = module { get() = module {
single { LocalMangaRepository(get()) } bind MangaRepository::class
factory { ReadmangaRepository(get()) } bind MangaRepository::class single { MangaLoaderContext() }
factory { MintMangaRepository(get()) } bind MangaRepository::class
factory { SelfMangaRepository(get()) } bind MangaRepository::class factory<MangaRepository>(named(MangaSource.READMANGA_RU)) { ReadmangaRepository(get()) }
factory { MangaChanRepository(get()) } bind MangaRepository::class factory<MangaRepository>(named(MangaSource.MINTMANGA)) { MintMangaRepository(get()) }
factory { DesuMeRepository(get()) } bind MangaRepository::class factory<MangaRepository>(named(MangaSource.SELFMANGA)) { SelfMangaRepository(get()) }
factory { HenChanRepository(get()) } bind MangaRepository::class factory<MangaRepository>(named(MangaSource.MANGACHAN)) { MangaChanRepository(get()) }
factory { YaoiChanRepository(get()) } bind MangaRepository::class factory<MangaRepository>(named(MangaSource.DESUME)) { DesuMeRepository(get()) }
factory { MangaTownRepository(get()) } bind MangaRepository::class factory<MangaRepository>(named(MangaSource.HENCHAN)) { HenChanRepository(get()) }
factory { MangaLibRepository(get()) } bind MangaRepository::class factory<MangaRepository>(named(MangaSource.YAOICHAN)) { YaoiChanRepository(get()) }
factory { NudeMoonRepository(get()) } bind MangaRepository::class factory<MangaRepository>(named(MangaSource.MANGATOWN)) { MangaTownRepository(get()) }
factory { MangareadRepository(get()) } bind MangaRepository::class 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 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.MangaPage
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.MangaTag import org.koitharu.kotatsu.core.model.MangaTag
import org.koitharu.kotatsu.core.model.SortOrder import org.koitharu.kotatsu.core.model.SortOrder
import org.koitharu.kotatsu.domain.MangaLoaderContext
abstract class RemoteMangaRepository( abstract class RemoteMangaRepository(
protected val loaderContext: MangaLoaderContext protected val loaderContext: MangaLoaderContext

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

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

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

@ -1,8 +1,8 @@
package org.koitharu.kotatsu.core.parser.site 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.exceptions.ParseException
import org.koitharu.kotatsu.core.model.* 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.longHashCode
import org.koitharu.kotatsu.utils.ext.mapToSet import org.koitharu.kotatsu.utils.ext.mapToSet
import org.koitharu.kotatsu.utils.ext.parseHtml import org.koitharu.kotatsu.utils.ext.parseHtml

@ -1,7 +1,7 @@
package org.koitharu.kotatsu.core.parser.site 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.core.model.MangaSource
import org.koitharu.kotatsu.domain.MangaLoaderContext
class MangaChanRepository(loaderContext: MangaLoaderContext) : ChanRepository(loaderContext) { class MangaChanRepository(loaderContext: MangaLoaderContext) : ChanRepository(loaderContext) {

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

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

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

@ -1,7 +1,7 @@
package org.koitharu.kotatsu.core.parser.site 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.core.model.MangaSource
import org.koitharu.kotatsu.domain.MangaLoaderContext
class MintMangaRepository(loaderContext: MangaLoaderContext) : GroupleRepository(loaderContext) { class MintMangaRepository(loaderContext: MangaLoaderContext) : GroupleRepository(loaderContext) {

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

@ -1,7 +1,7 @@
package org.koitharu.kotatsu.core.parser.site 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.core.model.MangaSource
import org.koitharu.kotatsu.domain.MangaLoaderContext
class ReadmangaRepository(loaderContext: MangaLoaderContext) : GroupleRepository(loaderContext) { class ReadmangaRepository(loaderContext: MangaLoaderContext) : GroupleRepository(loaderContext) {

@ -1,7 +1,7 @@
package org.koitharu.kotatsu.core.parser.site 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.core.model.MangaSource
import org.koitharu.kotatsu.domain.MangaLoaderContext
class SelfMangaRepository(loaderContext: MangaLoaderContext) : GroupleRepository(loaderContext) { class SelfMangaRepository(loaderContext: MangaLoaderContext) : GroupleRepository(loaderContext) {

@ -1,10 +1,10 @@
package org.koitharu.kotatsu.core.parser.site 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.exceptions.ParseException
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaChapter import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.core.model.MangaSource 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.longHashCode
import org.koitharu.kotatsu.utils.ext.parseHtml import org.koitharu.kotatsu.utils.ext.parseHtml
import org.koitharu.kotatsu.utils.ext.withDomain import org.koitharu.kotatsu.utils.ext.withDomain

@ -7,8 +7,11 @@ import androidx.appcompat.app.AppCompatDelegate
import androidx.collection.arraySetOf import androidx.collection.arraySetOf
import androidx.core.content.edit import androidx.core.content.edit
import androidx.preference.PreferenceManager 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.model.ZoomMode
import org.koitharu.kotatsu.core.parser.LocalMangaRepository import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.utils.delegates.prefs.* import org.koitharu.kotatsu.utils.delegates.prefs.*
import java.io.File import java.io.File
@ -19,7 +22,7 @@ class AppSettings private constructor(private val prefs: SharedPreferences) :
PreferenceManager.getDefaultSharedPreferences(context) PreferenceManager.getDefaultSharedPreferences(context)
) )
var listMode by IntEnumPreferenceDelegate( var listMode by EnumPreferenceDelegate(
ListMode::class.java, ListMode::class.java,
KEY_LIST_MODE, KEY_LIST_MODE,
ListMode.DETAILED_LIST 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 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( val readerPageSwitch by StringSetPreferenceDelegate(
KEY_READER_SWITCHERS, KEY_READER_SWITCHERS,
@ -117,6 +120,16 @@ class AppSettings private constructor(private val prefs: SharedPreferences) :
prefs.unregisterOnSharedPreferenceChangeListener(listener) prefs.unregisterOnSharedPreferenceChangeListener(listener)
} }
fun observe() = callbackFlow<String> {
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
sendBlocking(key)
}
prefs.registerOnSharedPreferenceChangeListener(listener)
awaitClose {
prefs.unregisterOnSharedPreferenceChangeListener(listener)
}
}
companion object { companion object {
const val PAGE_SWITCH_TAPS = "taps" 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_HISTORY = "history"
const val TRACK_FAVOURITES = "favourites" 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_APP_SECTION = "app_section"
const val KEY_THEME = "theme" const val KEY_THEME = "theme"
const val KEY_THEME_AMOLED = "amoled_theme" const val KEY_THEME_AMOLED = "amoled_theme"

@ -7,7 +7,7 @@ import org.koitharu.kotatsu.utils.delegates.prefs.LongPreferenceDelegate
class AppWidgetConfig private constructor( class AppWidgetConfig private constructor(
private val prefs: SharedPreferences, private val prefs: SharedPreferences,
val widgetId: Int val widgetId: Int
) : SharedPreferences by prefs { ) : SharedPreferences by prefs {
var categoryId by LongPreferenceDelegate(CATEGORY_ID, 0L) var categoryId by LongPreferenceDelegate(CATEGORY_ID, 0L)

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.utils package org.koitharu.kotatsu.core.ui
import android.content.Context import android.content.Context
import android.content.Intent 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.content.Context
import android.view.View 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.app.Activity
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
@ -10,7 +10,7 @@ import android.view.MenuItem
import android.view.View import android.view.View
import kotlinx.android.synthetic.main.activity_crash.* import kotlinx.android.synthetic.main.activity_crash.*
import org.koitharu.kotatsu.R 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 import org.koitharu.kotatsu.utils.ShareHelper
class CrashActivity : Activity(), View.OnClickListener { 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.ComponentRegistry
import coil.ImageLoader import coil.ImageLoader
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module import org.koin.dsl.module
import org.koitharu.kotatsu.core.local.CbzFetcher import org.koitharu.kotatsu.local.data.CbzFetcher
val uiModule val uiModule
get() = module { 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.Context
import android.content.Intent import android.content.Intent
@ -7,70 +7,58 @@ import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.net.toFile import androidx.core.net.toFile
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.android.synthetic.main.activity_details.* import kotlinx.android.synthetic.main.activity_details.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import moxy.MvpDelegate import org.koin.android.viewmodel.ext.android.viewModel
import moxy.ktx.moxyPresenter import org.koin.core.parameter.parametersOf
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.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.Manga
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.ui.base.BaseActivity import org.koitharu.kotatsu.download.DownloadService
import org.koitharu.kotatsu.ui.browser.BrowserActivity import org.koitharu.kotatsu.search.ui.global.GlobalSearchActivity
import org.koitharu.kotatsu.ui.download.DownloadService
import org.koitharu.kotatsu.utils.MangaShortcut import org.koitharu.kotatsu.utils.MangaShortcut
import org.koitharu.kotatsu.utils.ShareHelper import org.koitharu.kotatsu.utils.ShareHelper
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.getThemeColor import org.koitharu.kotatsu.utils.ext.getThemeColor
class MangaDetailsActivity : BaseActivity(), MangaDetailsView, class DetailsActivity : BaseActivity(), TabLayoutMediator.TabConfigurationStrategy {
TabLayoutMediator.TabConfigurationStrategy {
private val presenter by moxyPresenter { private val viewModel by viewModel<DetailsViewModel> {
MangaDetailsPresenter.getInstance(hashCode()) parametersOf(MangaIntent.from(intent))
} }
private var manga: Manga? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_details) setContentView(R.layout.activity_details)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
pager.adapter = MangaDetailsAdapter(this) pager.adapter = MangaDetailsAdapter(this)
TabLayoutMediator(tabs, pager, this).attach() TabLayoutMediator(tabs, pager, this).attach()
if (savedInstanceState?.containsKey(MvpDelegate.MOXY_DELEGATE_TAGS_KEY) != true) {
intent?.getParcelableExtra<Manga>(EXTRA_MANGA)?.let { viewModel.manga.observe(this, ::onMangaUpdated)
presenter.loadDetails(it, true) viewModel.newChaptersCount.observe(this, ::onNewChaptersChanged)
} ?: intent?.getLongExtra(EXTRA_MANGA_ID, 0)?.takeUnless { it == 0L }?.let { viewModel.onMangaRemoved.observe(this, ::onMangaRemoved)
presenter.findMangaById(it) viewModel.onError.observe(this, ::onError)
} ?: finishAfterTransition()
}
} }
override fun onMangaUpdated(manga: Manga) { private fun onMangaUpdated(manga: Manga) {
this.manga = manga
title = manga.title title = manga.title
invalidateOptionsMenu() invalidateOptionsMenu()
} }
override fun onHistoryChanged(history: MangaHistory?) = Unit private fun onMangaRemoved(manga: Manga) {
override fun onFavouriteChanged(categories: List<FavouriteCategory>) = Unit
override fun onLoadingStateChanged(isLoading: Boolean) = Unit
override fun onMangaRemoved(manga: Manga) {
Toast.makeText( Toast.makeText(
this, getString(R.string._s_deleted_from_local_storage, manga.title), this, getString(R.string._s_deleted_from_local_storage, manga.title),
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
@ -78,8 +66,8 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
finishAfterTransition() finishAfterTransition()
} }
override fun onError(e: Throwable) { private fun onError(e: Throwable) {
if (manga == null) { if (viewModel.manga.value == null) {
Toast.makeText(this, e.getDisplayMessage(resources), Toast.LENGTH_LONG).show() Toast.makeText(this, e.getDisplayMessage(resources), Toast.LENGTH_LONG).show()
finishAfterTransition() finishAfterTransition()
} else { } 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 val tab = tabs.getTabAt(1) ?: return
if (newChapters == 0) { if (newChapters == 0) {
tab.removeBadge() tab.removeBadge()
@ -104,8 +92,9 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
} }
override fun onPrepareOptionsMenu(menu: Menu): Boolean { override fun onPrepareOptionsMenu(menu: Menu): Boolean {
val manga = viewModel.manga.value
menu.findItem(R.id.action_save).isVisible = 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 = menu.findItem(R.id.action_delete).isVisible =
manga?.source == MangaSource.LOCAL manga?.source == MangaSource.LOCAL
menu.findItem(R.id.action_browser).isVisible = menu.findItem(R.id.action_browser).isVisible =
@ -117,7 +106,7 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.action_share -> { R.id.action_share -> {
manga?.let { viewModel.manga.value?.let {
if (it.source == MangaSource.LOCAL) { if (it.source == MangaSource.LOCAL) {
ShareHelper.shareCbz(this, Uri.parse(it.url).toFile()) ShareHelper.shareCbz(this, Uri.parse(it.url).toFile())
} else { } else {
@ -127,12 +116,12 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
true true
} }
R.id.action_delete -> { R.id.action_delete -> {
manga?.let { m -> viewModel.manga.value?.let { m ->
MaterialAlertDialogBuilder(this) AlertDialog.Builder(this)
.setTitle(R.string.delete_manga) .setTitle(R.string.delete_manga)
.setMessage(getString(R.string.text_delete_local_manga, m.title)) .setMessage(getString(R.string.text_delete_local_manga, m.title))
.setPositiveButton(R.string.delete) { _, _ -> .setPositiveButton(R.string.delete) { _, _ ->
presenter.deleteLocal(m) viewModel.deleteLocal(m)
} }
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.show() .show()
@ -140,10 +129,10 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
true true
} }
R.id.action_save -> { R.id.action_save -> {
manga?.let { viewModel.manga.value?.let {
val chaptersCount = it.chapters?.size ?: 0 val chaptersCount = it.chapters?.size ?: 0
if (chaptersCount > 5) { if (chaptersCount > 5) {
MaterialAlertDialogBuilder(this) AlertDialog.Builder(this)
.setTitle(R.string.save_manga) .setTitle(R.string.save_manga)
.setMessage( .setMessage(
getString( getString(
@ -166,15 +155,21 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
true true
} }
R.id.action_browser -> { R.id.action_browser -> {
manga?.let { viewModel.manga.value?.let {
startActivity(BrowserActivity.newIntent(this, it.url)) startActivity(BrowserActivity.newIntent(this, it.url))
} }
true true
} }
R.id.action_related -> {
viewModel.manga.value?.let {
startActivity(GlobalSearchActivity.newIntent(this, it.title))
}
true
}
R.id.action_shortcut -> { R.id.action_shortcut -> {
manga?.let { viewModel.manga.value?.let {
lifecycleScope.launch { lifecycleScope.launch {
if (!MangaShortcut(it).requestPinShortcut(this@MangaDetailsActivity)) { if (!MangaShortcut(it).requestPinShortcut(this@DetailsActivity)) {
Snackbar.make( Snackbar.make(
pager, pager,
R.string.operation_not_supported, R.string.operation_not_supported,
@ -192,7 +187,6 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
tab.text = when (position) { tab.text = when (position) {
0 -> getString(R.string.details) 0 -> getString(R.string.details)
1 -> getString(R.string.chapters) 1 -> getString(R.string.chapters)
2 -> getString(R.string.related)
else -> null else -> null
} }
} }
@ -211,17 +205,14 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
companion object { 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" const val ACTION_MANGA_VIEW = "${BuildConfig.APPLICATION_ID}.action.VIEW_MANGA"
fun newIntent(context: Context, manga: Manga) = fun newIntent(context: Context, manga: Manga) =
Intent(context, MangaDetailsActivity::class.java) Intent(context, DetailsActivity::class.java)
.putExtra(EXTRA_MANGA, manga) .putExtra(MangaIntent.KEY_MANGA, manga)
fun newIntent(context: Context, mangaId: Long) = fun newIntent(context: Context, mangaId: Long) =
Intent(context, MangaDetailsActivity::class.java) Intent(context, DetailsActivity::class.java)
.putExtra(EXTRA_MANGA_ID, mangaId) .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.text.Spanned
import android.view.View import android.view.View
import androidx.core.net.toUri import androidx.core.net.toUri
@ -10,37 +11,36 @@ import kotlinx.android.synthetic.main.fragment_details.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import moxy.ktx.moxyPresenter import org.koin.android.viewmodel.ext.android.sharedViewModel
import org.koitharu.kotatsu.R 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.Manga
import org.koitharu.kotatsu.core.model.MangaHistory import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.ui.base.BaseFragment import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesDialog
import org.koitharu.kotatsu.ui.list.favourites.categories.select.FavouriteCategoriesDialog import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.ui.reader.ReaderActivity import org.koitharu.kotatsu.search.ui.MangaSearchSheet
import org.koitharu.kotatsu.ui.search.MangaSearchSheet
import org.koitharu.kotatsu.utils.FileSizeUtils import org.koitharu.kotatsu.utils.FileSizeUtils
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
import kotlin.math.roundToInt import kotlin.math.roundToInt
class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetailsView, class DetailsFragment : BaseFragment(R.layout.fragment_details), View.OnClickListener,
View.OnClickListener,
View.OnLongClickListener { View.OnLongClickListener {
@Suppress("unused") private val viewModel by sharedViewModel<DetailsViewModel>()
private val presenter by moxyPresenter {
MangaDetailsPresenter.getInstance(activity.hashCode())
}
private var manga: Manga? = null override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
private var history: MangaHistory? = null 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) { private fun onMangaUpdated(manga: Manga) {
this.manga = manga
imageView_cover.newImageRequest(manga.largeCoverUrl ?: manga.coverUrl) imageView_cover.newImageRequest(manga.largeCoverUrl ?: manga.coverUrl)
.fallback(R.drawable.ic_placeholder) .fallback(R.drawable.ic_placeholder)
.crossfade(true) .crossfade(true)
.lifecycle(this) .lifecycle(viewLifecycleOwner)
.enqueueWith(coil) .enqueueWith(coil)
textView_title.text = manga.title textView_title.text = manga.title
textView_subtitle.textAndVisible = manga.altTitle textView_subtitle.textAndVisible = manga.altTitle
@ -59,7 +59,7 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
text = it, text = it,
iconRes = R.drawable.ic_chip_user, iconRes = R.drawable.ic_chip_user,
tag = it, tag = it,
onClickListener = this@MangaDetailsFragment onClickListener = this@DetailsFragment
) )
} }
} }
@ -68,7 +68,7 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
text = it.title, text = it.title,
iconRes = R.drawable.ic_chip_tag, iconRes = R.drawable.ic_chip_tag,
tag = it, tag = it,
onClickListener = this@MangaDetailsFragment onClickListener = this@DetailsFragment
) )
} }
manga.url.toUri().toFileOrNull()?.let { f -> manga.url.toUri().toFileOrNull()?.let { f ->
@ -81,7 +81,7 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
text = FileSizeUtils.formatBytes(context, size), text = FileSizeUtils.formatBytes(context, size),
iconRes = R.drawable.ic_chip_storage, iconRes = R.drawable.ic_chip_storage,
tag = it, tag = it,
onClickListener = this@MangaDetailsFragment onClickListener = this@DetailsFragment
) )
} }
} }
@ -89,35 +89,37 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
imageView_favourite.setOnClickListener(this) imageView_favourite.setOnClickListener(this)
button_read.setOnClickListener(this) button_read.setOnClickListener(this)
button_read.setOnLongClickListener(this) button_read.setOnLongClickListener(this)
updateReadButton() button_read.isEnabled = !manga.chapters.isNullOrEmpty()
} }
override fun onHistoryChanged(history: MangaHistory?) { private fun onHistoryChanged(history: MangaHistory?) {
this.history = history with(button_read) {
updateReadButton() 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( imageView_favourite.setImageResource(
if (categories.isEmpty()) { if (isFavourite) {
R.drawable.ic_heart_outline
} else {
R.drawable.ic_heart R.drawable.ic_heart
} else {
R.drawable.ic_heart_outline
} }
) )
} }
override fun onLoadingStateChanged(isLoading: Boolean) { private fun onLoadingStateChanged(isLoading: Boolean) {
progressBar.isVisible = isLoading 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) { override fun onClick(v: View) {
val manga = viewModel.manga.value
when { when {
v.id == R.id.imageView_favourite -> { v.id == R.id.imageView_favourite -> {
FavouriteCategoriesDialog.show(childFragmentManager, manga ?: return) FavouriteCategoriesDialog.show(childFragmentManager, manga ?: return)
@ -127,7 +129,7 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
ReaderActivity.newIntent( ReaderActivity.newIntent(
context ?: return, context ?: return,
manga ?: 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 { override fun onLongClick(v: View): Boolean {
when (v.id) { when (v.id) {
R.id.button_read -> { R.id.button_read -> {
if (history == null) { if (viewModel.readingHistory.value == null) {
return false return false
} }
v.showPopupMenu(R.menu.popup_read) { v.showPopupMenu(R.menu.popup_read) {
@ -155,7 +157,7 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
startActivity( startActivity(
ReaderActivity.newIntent( ReaderActivity.newIntent(
context ?: return@showPopupMenu false, context ?: return@showPopupMenu false,
manga ?: return@showPopupMenu false viewModel.manga.value ?: return@showPopupMenu false
) )
) )
true true
@ -168,19 +170,4 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
else -> return false 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.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
@ -6,12 +6,11 @@ import androidx.viewpager2.adapter.FragmentStateAdapter
class MangaDetailsAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) { class MangaDetailsAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
override fun getItemCount() = 3 override fun getItemCount() = 2
override fun createFragment(position: Int): Fragment = when(position) { override fun createFragment(position: Int): Fragment = when (position) {
0 -> MangaDetailsFragment() 0 -> DetailsFragment()
1 -> ChaptersFragment() 1 -> ChaptersFragment()
2 -> RelatedMangaFragment()
else -> throw IndexOutOfBoundsException("No fragment for position $position") 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 com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateLayoutContainer
import android.view.ViewGroup
import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.item_chapter.* import kotlinx.android.synthetic.main.item_chapter.*
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.MangaChapter import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.domain.history.ChapterExtra import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.ui.base.list.BaseViewHolder import org.koitharu.kotatsu.history.domain.ChapterExtra
import org.koitharu.kotatsu.utils.ext.getThemeColor import org.koitharu.kotatsu.utils.ext.getThemeColor
class ChapterHolder(parent: ViewGroup) : fun chapterListItemAD(
BaseViewHolder<MangaChapter, ChapterExtra>(parent, R.layout.item_chapter) { clickListener: OnListItemClickListener<MangaChapter>
) = adapterDelegateLayoutContainer<ChapterListItem, ChapterListItem>(R.layout.item_chapter) {
override fun onBind(data: MangaChapter, extra: ChapterExtra) { itemView.setOnClickListener {
textView_title.text = data.name clickListener.onItemClick(item.chapter, it)
textView_number.text = data.number.toString() }
imageView_check.isVisible = extra == ChapterExtra.CHECKED itemView.setOnLongClickListener {
when (extra) { 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 -> { ChapterExtra.UNREAD -> {
textView_number.setBackgroundResource(R.drawable.bg_badge_default) textView_number.setBackgroundResource(R.drawable.bg_badge_default)
textView_number.setTextColor(context.getThemeColor(android.R.attr.textColorSecondaryInverse)) 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.setBackgroundResource(R.drawable.bg_badge_accent)
textView_number.setTextColor(context.getThemeColor(android.R.attr.textColorPrimaryInverse)) 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.Notification
import android.app.NotificationChannel import android.app.NotificationChannel
@ -12,7 +12,7 @@ import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga 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 org.koitharu.kotatsu.utils.ext.getDisplayMessage
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -24,7 +24,8 @@ class DownloadNotification(private val context: Context) {
init { init {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
&& manager.getNotificationChannel(CHANNEL_ID) == null) { && manager.getNotificationChannel(CHANNEL_ID) == null
) {
val channel = NotificationChannel( val channel = NotificationChannel(
CHANNEL_ID, CHANNEL_ID,
context.getString(R.string.downloads), context.getString(R.string.downloads),
@ -145,7 +146,7 @@ class DownloadNotification(private val context: Context) {
private fun createIntent(context: Context, manga: Manga) = PendingIntent.getActivity( private fun createIntent(context: Context, manga: Manga) = PendingIntent.getActivity(
context, context,
manga.hashCode(), manga.hashCode(),
MangaDetailsActivity.newIntent(context, manga), DetailsActivity.newIntent(context, manga),
PendingIntent.FLAG_CANCEL_CURRENT 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.Context
import android.content.Intent import android.content.Intent
@ -7,6 +7,7 @@ import android.os.PowerManager
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import coil.ImageLoader import coil.ImageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import kotlinx.coroutines.* import kotlinx.coroutines.*
@ -19,13 +20,13 @@ import org.koin.android.ext.android.inject
import org.koin.core.context.GlobalContext import org.koin.core.context.GlobalContext
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.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.model.Manga
import org.koitharu.kotatsu.core.parser.LocalMangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.domain.local.MangaZip import org.koitharu.kotatsu.local.data.MangaZip
import org.koitharu.kotatsu.ui.base.BaseService import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.ui.base.dialog.CheckBoxAlertDialog import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.utils.CacheUtils import org.koitharu.kotatsu.utils.CacheUtils
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
import java.io.File import java.io.File
@ -54,8 +55,9 @@ class DownloadService : BaseService() {
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "kotatsu:downloading") .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "kotatsu:downloading")
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
when (intent?.action) { super.onStartCommand(intent, flags, startId)
when (intent.action) {
ACTION_DOWNLOAD_START -> { ACTION_DOWNLOAD_START -> {
val manga = intent.getParcelableExtra<Manga>(EXTRA_MANGA) val manga = intent.getParcelableExtra<Manga>(EXTRA_MANGA)
val chapters = intent.getLongArrayExtra(EXTRA_CHAPTERS_IDS)?.toArraySet() 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 { private fun downloadManga(manga: Manga, chaptersIds: Set<Long>?, startId: Int): Job {
return serviceScope.launch(Dispatchers.Default) { return lifecycleScope.launch(Dispatchers.Default) {
mutex.lock() mutex.lock()
wakeLock.acquire(TimeUnit.HOURS.toMillis(1)) wakeLock.acquire(TimeUnit.HOURS.toMillis(1))
notification.fillFrom(manga) 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 androidx.room.*
import org.koitharu.kotatsu.core.db.entity.FavouriteCategoryEntity import kotlinx.coroutines.flow.Flow
import org.koitharu.kotatsu.core.model.FavouriteCategory
@Dao @Dao
abstract class FavouriteCategoriesDao { abstract class FavouriteCategoriesDao {
@ -9,6 +10,9 @@ abstract class FavouriteCategoriesDao {
@Query("SELECT * FROM favourite_categories ORDER BY sort_key") @Query("SELECT * FROM favourite_categories ORDER BY sort_key")
abstract suspend fun findAll(): List<FavouriteCategoryEntity> 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) @Insert(onConflict = OnConflictStrategy.ABORT)
abstract suspend fun insert(category: FavouriteCategoryEntity): Long 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.ColumnInfo
import androidx.room.Entity 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.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey import androidx.room.ForeignKey
import org.koitharu.kotatsu.core.db.entity.MangaEntity
@Entity( @Entity(
tableName = "favourites", primaryKeys = ["manga_id", "category_id"], foreignKeys = [ 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.Embedded
import androidx.room.Junction import androidx.room.Junction
import androidx.room.Relation 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( data class FavouriteManga(
@Embedded val favourite: FavouriteEntity, @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 androidx.room.*
import org.koitharu.kotatsu.core.db.entity.FavouriteEntity import kotlinx.coroutines.flow.Flow
import org.koitharu.kotatsu.core.db.entity.FavouriteManga
import org.koitharu.kotatsu.core.db.entity.MangaEntity import org.koitharu.kotatsu.core.db.entity.MangaEntity
@Dao @Dao
@ -12,6 +11,10 @@ abstract class FavouritesDao {
@Query("SELECT * FROM favourites GROUP BY manga_id ORDER BY created_at") @Query("SELECT * FROM favourites GROUP BY manga_id ORDER BY created_at")
abstract suspend fun findAll(): List<FavouriteManga> 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 @Transaction
@Query("SELECT * FROM favourites GROUP BY manga_id ORDER BY created_at LIMIT :limit OFFSET :offset") @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> 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") @Query("SELECT * FROM favourites WHERE category_id = :categoryId GROUP BY manga_id ORDER BY created_at")
abstract suspend fun findAll(categoryId: Long): List<FavouriteManga> 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 @Transaction
@Query("SELECT * FROM favourites WHERE category_id = :categoryId GROUP BY manga_id ORDER BY created_at LIMIT :limit OFFSET :offset") @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> 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") @Query("SELECT * FROM favourites WHERE manga_id = :id GROUP BY manga_id")
abstract suspend fun find(id: Long): FavouriteManga? 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) @Insert(onConflict = OnConflictStrategy.IGNORE)
abstract suspend fun insert(favourite: FavouriteEntity) 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.collection.ArraySet
import androidx.room.withTransaction import androidx.room.withTransaction
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.core.db.MangaDatabase 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.MangaEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.Manga 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 import org.koitharu.kotatsu.utils.ext.mapToSet
class FavouritesRepository(private val db: MangaDatabase) { 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)) } 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> { suspend fun getAllManga(offset: Int): List<Manga> {
val entities = db.favouritesDao.findAll(offset, 20) val entities = db.favouritesDao.findAll(offset, 20)
return entities.map { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) } 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)) } 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> { suspend fun getManga(categoryId: Long, offset: Int): List<Manga> {
val entities = db.favouritesDao.findAll(categoryId, offset, 20) val entities = db.favouritesDao.findAll(categoryId, offset, 20)
return entities.map { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) } 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() 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 { suspend fun addCategory(title: String): FavouriteCategory {
val entity = FavouriteCategoryEntity( val entity = FavouriteCategoryEntity(
title = title, 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 interface OnFavouritesChangeListener {
fun onFavouritesChanged(mangaId: Long) 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.os.Bundle
import android.view.Menu import android.view.Menu
@ -8,25 +8,22 @@ import android.view.View
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.android.synthetic.main.fragment_favourites.* 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.R
import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.domain.favourites.FavouritesRepository import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity
import org.koitharu.kotatsu.domain.favourites.OnFavouritesChangeListener import org.koitharu.kotatsu.favourites.ui.categories.CategoriesEditDelegate
import org.koitharu.kotatsu.ui.base.BaseFragment import org.koitharu.kotatsu.favourites.ui.categories.FavouritesCategoriesViewModel
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.utils.ext.showPopupMenu import org.koitharu.kotatsu.utils.ext.showPopupMenu
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
class FavouritesContainerFragment : BaseFragment(R.layout.fragment_favourites), class FavouritesContainerFragment : BaseFragment(R.layout.fragment_favourites),
FavouriteCategoriesView, OnFavouritesChangeListener, FavouritesTabLongClickListener, FavouritesTabLongClickListener, CategoriesEditDelegate.CategoriesEditCallback {
CategoriesEditDelegate.CategoriesEditCallback {
private val viewModel by viewModel<FavouritesCategoriesViewModel>()
private val presenter by moxyPresenter(factory = ::FavouriteCategoriesPresenter)
private val editDelegate by lazy(LazyThreadSafetyMode.NONE) { private val editDelegate by lazy(LazyThreadSafetyMode.NONE) {
CategoriesEditDelegate(requireContext(), this) CategoriesEditDelegate(requireContext(), this)
} }
@ -41,15 +38,12 @@ class FavouritesContainerFragment : BaseFragment(R.layout.fragment_favourites),
val adapter = FavouritesPagerAdapter(this, this) val adapter = FavouritesPagerAdapter(this, this)
pager.adapter = adapter pager.adapter = adapter
TabLayoutMediator(tabs, pager, adapter).attach() TabLayoutMediator(tabs, pager, adapter).attach()
FavouritesRepository.subscribe(this)
}
override fun onDestroyView() { viewModel.categories.observe(viewLifecycleOwner, ::onCategoriesChanged)
FavouritesRepository.unsubscribe(this) viewModel.onError.observe(viewLifecycleOwner, ::onError)
super.onDestroyView()
} }
override fun onCategoriesChanged(categories: List<FavouriteCategory>) { private fun onCategoriesChanged(categories: List<FavouriteCategory>) {
val data = ArrayList<FavouriteCategory>(categories.size + 1) val data = ArrayList<FavouriteCategory>(categories.size + 1)
data += FavouriteCategory(0L, getString(R.string.all_favourites), -1, Date()) data += FavouriteCategory(0L, getString(R.string.all_favourites), -1, Date())
data += categories data += categories
@ -72,21 +66,13 @@ class FavouritesContainerFragment : BaseFragment(R.layout.fragment_favourites),
} }
override fun getTitle(): CharSequence? { override fun getTitle(): CharSequence? {
return getString(R.string.favourites) return context?.getString(R.string.favourites)
} }
override fun onCheckedCategoriesChanged(checkedIds: Set<Int>) = Unit private fun onError(e: Throwable) {
override fun onError(e: Throwable) {
Snackbar.make(pager, e.message ?: return, Snackbar.LENGTH_LONG).show() 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 { override fun onTabLongClick(tabView: View, category: FavouriteCategory): Boolean {
val menuRes = if (category.id == 0L) R.menu.popup_category_empty else R.menu.popup_category val menuRes = if (category.id == 0L) R.menu.popup_category_empty else R.menu.popup_category
tabView.showPopupMenu(menuRes) { tabView.showPopupMenu(menuRes) {
@ -101,15 +87,15 @@ class FavouritesContainerFragment : BaseFragment(R.layout.fragment_favourites),
} }
override fun onDeleteCategory(category: FavouriteCategory) { override fun onDeleteCategory(category: FavouriteCategory) {
presenter.deleteCategory(category.id) viewModel.deleteCategory(category.id)
} }
override fun onRenameCategory(category: FavouriteCategory, newName: String) { override fun onRenameCategory(category: FavouriteCategory, newName: String) {
presenter.renameCategory(category.id, newName) viewModel.renameCategory(category.id, newName)
} }
override fun onCreateCategory(name: String) { override fun onCreateCategory(name: String) {
presenter.createCategory(name) viewModel.createCategory(name)
} }
companion object { companion object {

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

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.list.favourites package org.koitharu.kotatsu.favourites.ui
import android.view.View import android.view.View
import org.koitharu.kotatsu.core.model.FavouriteCategory 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.Context
import android.content.Intent import android.content.Intent
@ -12,18 +12,18 @@ import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_categories.* 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.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.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.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.showPopupMenu import org.koitharu.kotatsu.utils.ext.showPopupMenu
class CategoriesActivity : BaseActivity(), OnRecyclerItemClickListener<FavouriteCategory>, class CategoriesActivity : BaseActivity(), OnListItemClickListener<FavouriteCategory>,
FavouriteCategoriesView, View.OnClickListener, CategoriesEditDelegate.CategoriesEditCallback { 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 adapter: CategoriesAdapter
private lateinit var reorderHelper: ItemTouchHelper private lateinit var reorderHelper: ItemTouchHelper
@ -41,6 +41,9 @@ class CategoriesActivity : BaseActivity(), OnRecyclerItemClickListener<Favourite
fab_add.setOnClickListener(this) fab_add.setOnClickListener(this)
reorderHelper = ItemTouchHelper(ReorderHelperCallback()) reorderHelper = ItemTouchHelper(ReorderHelperCallback())
reorderHelper.attachToRecyclerView(recyclerView) reorderHelper.attachToRecyclerView(recyclerView)
viewModel.categories.observe(this, ::onCategoriesChanged)
viewModel.onError.observe(this, ::onError)
} }
override fun onClick(v: View) { 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) { view.showPopupMenu(R.menu.popup_category) {
when (it.itemId) { when (it.itemId) {
R.id.action_remove -> editDelegate.deleteCategory(item) 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( reorderHelper.startDrag(
recyclerView.findViewHolderForAdapterPosition(position) ?: return false recyclerView.findContainingViewHolder(view) ?: return false
) )
return true return true
} }
override fun onCategoriesChanged(categories: List<FavouriteCategory>) { private fun onCategoriesChanged(categories: List<FavouriteCategory>) {
adapter.replaceData(categories) adapter.items = categories
textView_holder.isVisible = categories.isEmpty() textView_holder.isVisible = categories.isEmpty()
} }
override fun onCheckedCategoriesChanged(checkedIds: Set<Int>) = Unit private fun onError(e: Throwable) {
override fun onError(e: Throwable) {
Snackbar.make(recyclerView, e.getDisplayMessage(resources), Snackbar.LENGTH_LONG) Snackbar.make(recyclerView, e.getDisplayMessage(resources), Snackbar.LENGTH_LONG)
.show() .show()
} }
override fun onDeleteCategory(category: FavouriteCategory) { override fun onDeleteCategory(category: FavouriteCategory) {
presenter.deleteCategory(category.id) viewModel.deleteCategory(category.id)
} }
override fun onRenameCategory(category: FavouriteCategory, newName: String) { override fun onRenameCategory(category: FavouriteCategory, newName: String) {
presenter.renameCategory(category.id, newName) viewModel.renameCategory(category.id, newName)
} }
override fun onCreateCategory(name: String) { override fun onCreateCategory(name: String) {
presenter.createCategory(name) viewModel.createCategory(name)
} }
private inner class ReorderHelperCallback : ItemTouchHelper.SimpleCallback( private inner class ReorderHelperCallback : ItemTouchHelper.SimpleCallback(
@ -101,8 +102,7 @@ class CategoriesActivity : BaseActivity(), OnRecyclerItemClickListener<Favourite
): Boolean { ): Boolean {
val oldPos = viewHolder.bindingAdapterPosition val oldPos = viewHolder.bindingAdapterPosition
val newPos = target.bindingAdapterPosition val newPos = target.bindingAdapterPosition
adapter.moveItem(oldPos, newPos) viewModel.reorderCategories(oldPos, newPos)
presenter.storeCategoriesOrder(adapter.items.map { it.id })
return true 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.content.Context
import android.text.InputType 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.R
import org.koitharu.kotatsu.base.ui.dialog.TextInputDialog
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.ui.base.dialog.TextInputDialog
class CategoriesEditDelegate( class CategoriesEditDelegate(
private val context: Context, private val context: Context,
@ -13,7 +13,7 @@ class CategoriesEditDelegate(
) { ) {
fun deleteCategory(category: FavouriteCategory) { fun deleteCategory(category: FavouriteCategory) {
MaterialAlertDialogBuilder(context) AlertDialog.Builder(context)
.setMessage(context.getString(R.string.category_delete_confirm, category.title)) .setMessage(context.getString(R.string.category_delete_confirm, category.title))
.setTitle(R.string.remove_category) .setTitle(R.string.remove_category)
.setNegativeButton(android.R.string.cancel, null) .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)
}
}
}

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

Loading…
Cancel
Save