Merge branch 'feature/mvvm' into devel
This commit is contained in:
12
.travis.yml
12
.travis.yml
@@ -1,15 +1,9 @@
|
||||
language: android
|
||||
dist: trusty
|
||||
jdk:
|
||||
- oraclejdk8
|
||||
android:
|
||||
components:
|
||||
- tools
|
||||
- platform-tools-30.0.3
|
||||
- build-tools-30.0.2
|
||||
- android-30
|
||||
licenses:
|
||||
- android-sdk-preview-license-.+
|
||||
- android-sdk-license-.+
|
||||
- google-gdk-license-.+
|
||||
- build-tools-30.0.2
|
||||
- platform-tools-30.0.3
|
||||
- tools
|
||||
script: ./gradlew -Dorg.gradle.jvmargs=-Xmx1536m assembleDebug lintDebug
|
||||
@@ -3,6 +3,7 @@ plugins {
|
||||
id 'kotlin-android'
|
||||
id 'kotlin-android-extensions'
|
||||
id 'kotlin-kapt'
|
||||
// TODO id 'kotlin-parcelize'
|
||||
}
|
||||
|
||||
def gitCommits = 'git rev-list --count HEAD'.execute([], rootDir).text.trim().toInteger()
|
||||
@@ -70,6 +71,7 @@ dependencies {
|
||||
implementation 'androidx.activity:activity-ktx:1.2.0-beta01'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.3.0-beta01'
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-beta01'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.0-beta01'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha06'
|
||||
@@ -84,24 +86,22 @@ dependencies {
|
||||
implementation 'androidx.room:room-ktx:2.2.5'
|
||||
kapt 'androidx.room:room-compiler:2.2.5'
|
||||
|
||||
implementation 'com.github.moxy-community:moxy:2.2.0'
|
||||
implementation 'com.github.moxy-community:moxy-androidx:2.2.0'
|
||||
implementation 'com.github.moxy-community:moxy-material:2.2.0'
|
||||
implementation 'com.github.moxy-community:moxy-ktx:2.2.0'
|
||||
kapt 'com.github.moxy-community:moxy-compiler:2.2.0'
|
||||
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
|
||||
implementation 'com.squareup.okio:okio:2.9.0'
|
||||
implementation 'org.jsoup:jsoup:1.13.1'
|
||||
|
||||
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.0'
|
||||
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-layoutcontainer:4.3.0'
|
||||
|
||||
implementation 'org.koin:koin-android:2.2.0'
|
||||
implementation 'io.coil-kt:coil-base:1.0.0'
|
||||
implementation 'org.koin:koin-android-viewmodel:2.2.0'
|
||||
implementation 'io.coil-kt:coil-base:1.1.0'
|
||||
implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0'
|
||||
implementation 'com.tomclaw.cache:cache:1.0'
|
||||
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.5'
|
||||
|
||||
testImplementation 'junit:junit:4.13.1'
|
||||
testImplementation 'org.json:json:20200518'
|
||||
testImplementation 'org.json:json:20201115'
|
||||
testImplementation 'org.koin:koin-test:2.2.0-rc-2'
|
||||
}
|
||||
@@ -23,7 +23,7 @@
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="UnusedAttribute">
|
||||
<activity android:name=".ui.list.MainActivity">
|
||||
<activity android:name="org.koitharu.kotatsu.main.ui.MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
@@ -32,62 +32,62 @@
|
||||
android:name="android.app.default_searchable"
|
||||
android:value=".ui.search.SearchActivity" />
|
||||
</activity>
|
||||
<activity android:name=".ui.details.MangaDetailsActivity">
|
||||
<activity android:name="org.koitharu.kotatsu.details.ui.DetailsActivity">
|
||||
<intent-filter>
|
||||
<action android:name="${applicationId}.action.VIEW_MANGA" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".ui.reader.ReaderActivity" />
|
||||
<activity android:name="org.koitharu.kotatsu.reader.ui.ReaderActivity" />
|
||||
<activity
|
||||
android:name=".ui.search.SearchActivity"
|
||||
android:name="org.koitharu.kotatsu.search.ui.SearchActivity"
|
||||
android:label="@string/search" />
|
||||
<activity
|
||||
android:name=".ui.settings.SettingsActivity"
|
||||
android:name="org.koitharu.kotatsu.settings.SettingsActivity"
|
||||
android:label="@string/settings" />
|
||||
<activity
|
||||
android:name=".ui.reader.SimpleSettingsActivity"
|
||||
android:name="org.koitharu.kotatsu.reader.ui.SimpleSettingsActivity"
|
||||
android:label="@string/settings">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MANAGE_NETWORK_USAGE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".ui.browser.BrowserActivity" />
|
||||
<activity android:name="org.koitharu.kotatsu.browser.BrowserActivity" />
|
||||
<activity
|
||||
android:name=".ui.utils.CrashActivity"
|
||||
android:name="org.koitharu.kotatsu.core.ui.CrashActivity"
|
||||
android:label="@string/error_occurred"
|
||||
android:theme="@android:style/Theme.DeviceDefault"
|
||||
android:windowSoftInputMode="stateAlwaysHidden" />
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.ui.list.favourites.categories.CategoriesActivity"
|
||||
android:name="org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity"
|
||||
android:label="@string/favourites_categories"
|
||||
android:windowSoftInputMode="stateAlwaysHidden" />
|
||||
<activity
|
||||
android:name=".ui.widget.shelf.ShelfConfigActivity"
|
||||
android:name="org.koitharu.kotatsu.widget.shelf.ShelfConfigActivity"
|
||||
android:label="@string/manga_shelf">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.search.global.GlobalSearchActivity"
|
||||
android:name="org.koitharu.kotatsu.search.ui.global.GlobalSearchActivity"
|
||||
android:label="@string/search" />
|
||||
<activity
|
||||
android:name=".ui.utils.protect.ProtectActivity"
|
||||
android:name="org.koitharu.kotatsu.main.ui.protect.ProtectActivity"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
|
||||
<service
|
||||
android:name=".ui.download.DownloadService"
|
||||
android:name="org.koitharu.kotatsu.download.DownloadService"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
<service
|
||||
android:name=".ui.widget.shelf.ShelfWidgetService"
|
||||
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetService"
|
||||
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
||||
<service
|
||||
android:name=".ui.widget.recent.RecentWidgetService"
|
||||
android:name="org.koitharu.kotatsu.widget.recent.RecentWidgetService"
|
||||
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
||||
|
||||
<provider
|
||||
android:name=".ui.search.MangaSuggestionsProvider"
|
||||
android:name="org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider"
|
||||
android:authorities="${applicationId}.MangaSuggestionsProvider"
|
||||
android:exported="false" />
|
||||
<provider
|
||||
@@ -101,7 +101,7 @@
|
||||
</provider>
|
||||
|
||||
<receiver
|
||||
android:name=".ui.widget.shelf.ShelfWidgetProvider"
|
||||
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetProvider"
|
||||
android:label="@string/manga_shelf">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
@@ -111,7 +111,7 @@
|
||||
android:resource="@xml/widget_shelf" />
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".ui.widget.recent.RecentWidgetProvider"
|
||||
android:name="org.koitharu.kotatsu.widget.recent.RecentWidgetProvider"
|
||||
android:label="@string/recent_manga">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
|
||||
@@ -6,25 +6,30 @@ import androidx.appcompat.app.AppCompatDelegate
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.context.startKoin
|
||||
import org.koin.dsl.module
|
||||
import org.koitharu.kotatsu.core.backup.BackupRepository
|
||||
import org.koitharu.kotatsu.core.backup.RestoreRepository
|
||||
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.core.db.databaseModule
|
||||
import org.koitharu.kotatsu.core.github.githubModule
|
||||
import org.koitharu.kotatsu.core.local.PagesCache
|
||||
import org.koitharu.kotatsu.core.network.networkModule
|
||||
import org.koitharu.kotatsu.core.parser.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.core.parser.parserModule
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.domain.MangaDataRepository
|
||||
import org.koitharu.kotatsu.domain.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.domain.MangaSearchRepository
|
||||
import org.koitharu.kotatsu.domain.favourites.FavouritesRepository
|
||||
import org.koitharu.kotatsu.domain.history.HistoryRepository
|
||||
import org.koitharu.kotatsu.domain.tracking.TrackingRepository
|
||||
import org.koitharu.kotatsu.ui.base.uiModule
|
||||
import org.koitharu.kotatsu.ui.utils.AppCrashHandler
|
||||
import org.koitharu.kotatsu.ui.widget.WidgetUpdater
|
||||
import org.koitharu.kotatsu.core.ui.AppCrashHandler
|
||||
import org.koitharu.kotatsu.core.ui.uiModule
|
||||
import org.koitharu.kotatsu.details.detailsModule
|
||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||
import org.koitharu.kotatsu.favourites.favouritesModule
|
||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
import org.koitharu.kotatsu.history.historyModule
|
||||
import org.koitharu.kotatsu.local.data.PagesCache
|
||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.local.localModule
|
||||
import org.koitharu.kotatsu.main.mainModule
|
||||
import org.koitharu.kotatsu.reader.readerModule
|
||||
import org.koitharu.kotatsu.remotelist.remoteListModule
|
||||
import org.koitharu.kotatsu.search.searchModule
|
||||
import org.koitharu.kotatsu.settings.settingsModule
|
||||
import org.koitharu.kotatsu.tracker.trackerModule
|
||||
import org.koitharu.kotatsu.widget.WidgetUpdater
|
||||
import org.koitharu.kotatsu.widget.appWidgetModule
|
||||
|
||||
class KotatsuApp : Application() {
|
||||
|
||||
@@ -62,20 +67,19 @@ class KotatsuApp : Application() {
|
||||
networkModule,
|
||||
databaseModule,
|
||||
githubModule,
|
||||
parserModule,
|
||||
uiModule,
|
||||
module {
|
||||
single { FavouritesRepository(get()) }
|
||||
single { HistoryRepository(get()) }
|
||||
single { TrackingRepository(get(), get()) }
|
||||
single { MangaDataRepository(get()) }
|
||||
single { BackupRepository(get()) }
|
||||
single { RestoreRepository(get()) }
|
||||
single { MangaSearchRepository() }
|
||||
single { MangaLoaderContext() }
|
||||
single { AppSettings(get()) }
|
||||
single { PagesCache(get()) }
|
||||
}
|
||||
parserModule,
|
||||
mainModule,
|
||||
searchModule,
|
||||
localModule,
|
||||
favouritesModule,
|
||||
historyModule,
|
||||
remoteListModule,
|
||||
detailsModule,
|
||||
trackerModule,
|
||||
settingsModule,
|
||||
readerModule,
|
||||
appWidgetModule
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.domain
|
||||
package org.koitharu.kotatsu.base.domain
|
||||
|
||||
import androidx.room.withTransaction
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
@@ -32,6 +32,12 @@ class MangaDataRepository(private val db: MangaDatabase) {
|
||||
return db.mangaDao.find(mangaId)?.toManga()
|
||||
}
|
||||
|
||||
suspend fun resolveIntent(intent: MangaIntent): Manga? = when {
|
||||
intent.manga != null -> intent.manga
|
||||
intent.mangaId != MangaIntent.ID_NONE -> db.mangaDao.find(intent.mangaId)?.toManga()
|
||||
else -> null // TODO resolve uri
|
||||
}
|
||||
|
||||
suspend fun storeManga(manga: Manga) {
|
||||
val tags = manga.tags.map(TagEntity.Companion::fromMangaTag)
|
||||
db.withTransaction {
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.koitharu.kotatsu.base.domain
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
|
||||
data class MangaIntent(
|
||||
val manga: Manga?,
|
||||
val mangaId: Long,
|
||||
val uri: Uri?
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
||||
fun from(intent: Intent?) = MangaIntent(
|
||||
manga = intent?.getParcelableExtra(KEY_MANGA),
|
||||
mangaId = intent?.getLongExtra(KEY_ID, ID_NONE) ?: ID_NONE,
|
||||
uri = intent?.data
|
||||
)
|
||||
|
||||
fun from(args: Bundle?) = MangaIntent(
|
||||
manga = args?.getParcelable(KEY_MANGA),
|
||||
mangaId = args?.getLong(KEY_ID, ID_NONE) ?: ID_NONE,
|
||||
uri = null
|
||||
)
|
||||
|
||||
const val ID_NONE = 0L
|
||||
|
||||
const val KEY_MANGA = "manga"
|
||||
const val KEY_ID = "id"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.domain
|
||||
package org.koitharu.kotatsu.base.domain
|
||||
|
||||
import okhttp3.*
|
||||
import org.koin.core.component.KoinComponent
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.domain
|
||||
package org.koitharu.kotatsu.base.domain
|
||||
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.domain
|
||||
package org.koitharu.kotatsu.base.domain
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.base
|
||||
package org.koitharu.kotatsu.base.ui
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
@@ -6,11 +6,11 @@ import android.view.View
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import moxy.MvpAppCompatDialogFragment
|
||||
import androidx.fragment.app.DialogFragment
|
||||
|
||||
abstract class AlertDialogFragment(
|
||||
@LayoutRes private val layoutResId: Int
|
||||
) : MvpAppCompatDialogFragment() {
|
||||
) : DialogFragment() {
|
||||
|
||||
private var rootView: View? = null
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
package org.koitharu.kotatsu.ui.base
|
||||
package org.koitharu.kotatsu.base.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import moxy.MvpAppCompatActivity
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
|
||||
abstract class BaseActivity : MvpAppCompatActivity() {
|
||||
abstract class BaseActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
if (get<AppSettings>().isAmoledTheme) {
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.base
|
||||
package org.koitharu.kotatsu.base.ui
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
@@ -7,11 +7,11 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.appcompat.app.AppCompatDialog
|
||||
import moxy.MvpBottomSheetDialogFragment
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import org.koitharu.kotatsu.utils.UiUtils
|
||||
|
||||
abstract class BaseBottomSheet(@LayoutRes private val layoutResId: Int) :
|
||||
MvpBottomSheetDialogFragment() {
|
||||
BottomSheetDialogFragment() {
|
||||
|
||||
final override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
@@ -1,14 +1,14 @@
|
||||
package org.koitharu.kotatsu.ui.base
|
||||
package org.koitharu.kotatsu.base.ui
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.fragment.app.Fragment
|
||||
import coil.ImageLoader
|
||||
import moxy.MvpAppCompatFragment
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
abstract class BaseFragment(
|
||||
@LayoutRes contentLayoutId: Int
|
||||
) : MvpAppCompatFragment(contentLayoutId) {
|
||||
) : Fragment(contentLayoutId) {
|
||||
|
||||
protected val coil by inject<ImageLoader>()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.base
|
||||
package org.koitharu.kotatsu.base.ui
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.base
|
||||
package org.koitharu.kotatsu.base.ui
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.koitharu.kotatsu.base.ui
|
||||
|
||||
import androidx.lifecycle.LifecycleService
|
||||
|
||||
abstract class BaseService : LifecycleService()
|
||||
@@ -1,35 +1,35 @@
|
||||
package org.koitharu.kotatsu.ui.base
|
||||
package org.koitharu.kotatsu.base.ui
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.*
|
||||
import moxy.MvpPresenter
|
||||
import moxy.presenterScope
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
abstract class BasePresenter<V : BaseMvpView> : MvpPresenter<V>(), KoinComponent {
|
||||
abstract class BaseViewModel : ViewModel() {
|
||||
|
||||
val onError = SingleLiveEvent<Throwable>()
|
||||
val isLoading = MutableLiveData(false)
|
||||
|
||||
protected fun launchJob(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
block: suspend CoroutineScope.() -> Unit
|
||||
) {
|
||||
presenterScope.launch(context + createErrorHandler(), start, block)
|
||||
}
|
||||
): Job = viewModelScope.launch(context + createErrorHandler(), start, block)
|
||||
|
||||
protected fun launchLoadingJob(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
block: suspend CoroutineScope.() -> Unit
|
||||
) {
|
||||
presenterScope.launch(context + createErrorHandler(), start) {
|
||||
viewState.onLoadingStateChanged(isLoading = true)
|
||||
try {
|
||||
block()
|
||||
} finally {
|
||||
viewState.onLoadingStateChanged(isLoading = false)
|
||||
}
|
||||
): Job = viewModelScope.launch(context + createErrorHandler(), start) {
|
||||
isLoading.postValue(true)
|
||||
try {
|
||||
block()
|
||||
} finally {
|
||||
isLoading.postValue(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ abstract class BasePresenter<V : BaseMvpView> : MvpPresenter<V>(), KoinComponent
|
||||
throwable.printStackTrace()
|
||||
}
|
||||
if (throwable !is CancellationException) {
|
||||
viewState.onError(throwable)
|
||||
onError.postCall(throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.base.dialog
|
||||
package org.koitharu.kotatsu.base.ui.dialog
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
@@ -8,7 +8,6 @@ import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.checkbox.MaterialCheckBox
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.koitharu.kotatsu.R
|
||||
|
||||
class CheckBoxAlertDialog private constructor(private val delegate: AlertDialog) :
|
||||
@@ -23,7 +22,7 @@ class CheckBoxAlertDialog private constructor(private val delegate: AlertDialog)
|
||||
.inflate(R.layout.dialog_checkbox, null, false)
|
||||
private val checkBox = view.findViewById<MaterialCheckBox>(android.R.id.checkbox)
|
||||
|
||||
private val delegate = MaterialAlertDialogBuilder(context)
|
||||
private val delegate = AlertDialog.Builder(context)
|
||||
.setView(view)
|
||||
|
||||
fun setTitle(@StringRes titleResId: Int): Builder {
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.base.dialog
|
||||
package org.koitharu.kotatsu.base.ui.dialog
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
@@ -7,10 +7,9 @@ import android.view.ViewGroup
|
||||
import android.widget.BaseAdapter
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.android.synthetic.main.item_storage.view.*
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.parser.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.utils.ext.getStorageName
|
||||
import org.koitharu.kotatsu.utils.ext.inflate
|
||||
import org.koitharu.kotatsu.utils.ext.longHashCode
|
||||
@@ -24,7 +23,7 @@ class StorageSelectDialog private constructor(private val delegate: AlertDialog)
|
||||
class Builder(context: Context, defaultValue: File?, listener: OnStorageSelectListener) {
|
||||
|
||||
private val adapter = VolumesAdapter(context)
|
||||
private val delegate = MaterialAlertDialogBuilder(context)
|
||||
private val delegate = AlertDialog.Builder(context)
|
||||
|
||||
init {
|
||||
if (adapter.isEmpty) {
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.base.dialog
|
||||
package org.koitharu.kotatsu.base.ui.dialog
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
@@ -7,7 +7,6 @@ import android.text.InputFilter
|
||||
import android.view.LayoutInflater
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.android.synthetic.main.dialog_input.view.*
|
||||
import org.koitharu.kotatsu.R
|
||||
|
||||
@@ -23,7 +22,7 @@ class TextInputDialog private constructor(
|
||||
private val view = LayoutInflater.from(context)
|
||||
.inflate(R.layout.dialog_input, null, false)
|
||||
|
||||
private val delegate = MaterialAlertDialogBuilder(context)
|
||||
private val delegate = AlertDialog.Builder(context)
|
||||
.setView(view)
|
||||
|
||||
fun setTitle(@StringRes titleResId: Int): Builder {
|
||||
@@ -1,9 +1,10 @@
|
||||
package org.koitharu.kotatsu.ui.base.list
|
||||
package org.koitharu.kotatsu.base.ui.list
|
||||
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import java.util.*
|
||||
|
||||
@Deprecated("")
|
||||
class AdapterUpdater<T>(oldList: List<T>, newList: List<T>, getId: (T) -> Long) {
|
||||
|
||||
private val diff = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.base.list
|
||||
package org.koitharu.kotatsu.base.ui.list
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -30,25 +30,7 @@ abstract class BaseViewHolder<T, E> protected constructor(view: View) :
|
||||
return boundData ?: throw IllegalStateException("Calling requireData() before bind()")
|
||||
}
|
||||
|
||||
fun setOnItemClickListener(listener: OnRecyclerItemClickListener<T>?) {
|
||||
val listenersAdapter = listener?.let { HolderListenersAdapter(it) }
|
||||
itemView.setOnClickListener(listenersAdapter)
|
||||
itemView.setOnLongClickListener(listenersAdapter)
|
||||
}
|
||||
|
||||
open fun onRecycled() = Unit
|
||||
|
||||
abstract fun onBind(data: T, extra: E)
|
||||
|
||||
private inner class HolderListenersAdapter(private val listener: OnRecyclerItemClickListener<T>) :
|
||||
View.OnClickListener, View.OnLongClickListener {
|
||||
|
||||
override fun onClick(v: View) {
|
||||
listener.onItemClick(boundData ?: return, bindingAdapterPosition, v)
|
||||
}
|
||||
|
||||
override fun onLongClick(v: View): Boolean {
|
||||
return listener.onItemLongClick(boundData ?: return false, bindingAdapterPosition, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.base.list
|
||||
package org.koitharu.kotatsu.base.ui.list
|
||||
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.koitharu.kotatsu.base.ui.list
|
||||
|
||||
import android.view.View
|
||||
|
||||
interface OnListItemClickListener<I> {
|
||||
|
||||
fun onItemClick(item: I, view: View)
|
||||
|
||||
fun onItemLongClick(item: I, view: View) = false
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.koitharu.kotatsu.base.ui.list
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class PaginationScrollListener(offset: Int, private val callback: Callback) :
|
||||
BoundsScrollListener(0, offset) {
|
||||
|
||||
override fun onScrolledToStart(recyclerView: RecyclerView) = Unit
|
||||
|
||||
override fun onScrolledToEnd(recyclerView: RecyclerView) {
|
||||
callback.onScrolledToEnd()
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
|
||||
fun onScrolledToEnd()
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.base.list
|
||||
package org.koitharu.kotatsu.base.ui.list
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.base.list.decor
|
||||
package org.koitharu.kotatsu.base.ui.list.decor
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.base.list.decor
|
||||
package org.koitharu.kotatsu.base.ui.list.decor
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.base.list.decor
|
||||
package org.koitharu.kotatsu.base.ui.list.decor
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.view.View
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.base.widgets
|
||||
package org.koitharu.kotatsu.base.ui.widgets
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.base.widgets
|
||||
package org.koitharu.kotatsu.base.ui.widgets
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.base.widgets
|
||||
package org.koitharu.kotatsu.base.ui.widgets
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.browser
|
||||
package org.koitharu.kotatsu.browser
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ActivityNotFoundException
|
||||
@@ -11,7 +11,7 @@ import android.view.MenuItem
|
||||
import androidx.core.view.isVisible
|
||||
import kotlinx.android.synthetic.main.activity_browser.*
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.ui.base.BaseActivity
|
||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
class BrowserActivity : BaseActivity(), BrowserCallback {
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.browser
|
||||
package org.koitharu.kotatsu.browser
|
||||
|
||||
interface BrowserCallback {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.browser
|
||||
package org.koitharu.kotatsu.browser
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.webkit.WebResourceRequest
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.utils.cloudflare
|
||||
package org.koitharu.kotatsu.browser.cloudflare
|
||||
|
||||
interface CloudFlareCallback {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.utils.cloudflare
|
||||
package org.koitharu.kotatsu.browser.cloudflare
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.webkit.CookieManager
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.utils.cloudflare
|
||||
package org.koitharu.kotatsu.browser.cloudflare
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
@@ -10,8 +10,8 @@ import androidx.core.view.isInvisible
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import kotlinx.android.synthetic.main.fragment_cloudflare.*
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
|
||||
import org.koitharu.kotatsu.core.network.UserAgentInterceptor
|
||||
import org.koitharu.kotatsu.ui.base.AlertDialogFragment
|
||||
import org.koitharu.kotatsu.utils.ext.stringArgument
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
|
||||
@@ -4,7 +4,11 @@ import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.db.entity.*
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
|
||||
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
|
||||
import org.koitharu.kotatsu.history.data.HistoryEntity
|
||||
|
||||
class BackupRepository(private val db: MangaDatabase) {
|
||||
|
||||
|
||||
@@ -3,7 +3,11 @@ package org.koitharu.kotatsu.core.backup
|
||||
import androidx.room.withTransaction
|
||||
import org.json.JSONObject
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.db.entity.*
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
|
||||
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
|
||||
import org.koitharu.kotatsu.history.data.HistoryEntity
|
||||
import org.koitharu.kotatsu.utils.ext.getStringOrNull
|
||||
import org.koitharu.kotatsu.utils.ext.iterator
|
||||
import org.koitharu.kotatsu.utils.ext.map
|
||||
|
||||
@@ -4,6 +4,12 @@ import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import org.koitharu.kotatsu.core.db.dao.*
|
||||
import org.koitharu.kotatsu.core.db.entity.*
|
||||
import org.koitharu.kotatsu.favourites.data.FavouriteCategoriesDao
|
||||
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
|
||||
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
|
||||
import org.koitharu.kotatsu.favourites.data.FavouritesDao
|
||||
import org.koitharu.kotatsu.history.data.HistoryDao
|
||||
import org.koitharu.kotatsu.history.data.HistoryEntity
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
|
||||
@@ -15,7 +15,7 @@ import androidx.room.PrimaryKey
|
||||
)
|
||||
]
|
||||
)
|
||||
data class TrackEntity (
|
||||
data class TrackEntity(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
@ColumnInfo(name = "manga_id") val mangaId: Long,
|
||||
@ColumnInfo(name = "chapters_total") val totalChapters: Int,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
package org.koitharu.kotatsu.core.exceptions
|
||||
|
||||
class ParseException(message: String? = null, cause: Throwable? = null) : RuntimeException(message, cause)
|
||||
class ParseException(message: String? = null, cause: Throwable? = null) :
|
||||
RuntimeException(message, cause)
|
||||
@@ -4,9 +4,10 @@ import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import org.koin.core.context.GlobalContext
|
||||
import org.koin.core.error.NoBeanDefFoundException
|
||||
import org.koitharu.kotatsu.core.parser.LocalMangaRepository
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.parser.site.*
|
||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||
|
||||
@Suppress("SpellCheckingInspection")
|
||||
@Parcelize
|
||||
@@ -30,6 +31,7 @@ enum class MangaSource(
|
||||
// HENTAILIB("HentaiLib", "ru", HentaiLibRepository::class.java)
|
||||
|
||||
@get:Throws(NoBeanDefFoundException::class)
|
||||
@Deprecated("")
|
||||
val repository: MangaRepository
|
||||
get() = GlobalContext.get().get(cls.kotlin)
|
||||
get() = GlobalContext.get().get(named(this))
|
||||
}
|
||||
@@ -5,10 +5,10 @@ import kotlinx.android.parcel.Parcelize
|
||||
import java.util.*
|
||||
|
||||
@Parcelize
|
||||
data class MangaTracking (
|
||||
data class MangaTracking(
|
||||
val manga: Manga,
|
||||
val knownChaptersCount: Int,
|
||||
val lastChapterId: Long,
|
||||
val lastNotifiedChapterId: Long,
|
||||
val lastCheck: Date?
|
||||
): Parcelable
|
||||
) : Parcelable
|
||||
@@ -5,9 +5,9 @@ import kotlinx.android.parcel.Parcelize
|
||||
import java.util.*
|
||||
|
||||
@Parcelize
|
||||
data class TrackingLogItem (
|
||||
data class TrackingLogItem(
|
||||
val id: Long,
|
||||
val manga: Manga,
|
||||
val chapters: List<String>,
|
||||
val createdAt: Date
|
||||
): Parcelable
|
||||
) : Parcelable
|
||||
@@ -6,13 +6,18 @@ interface MangaRepository {
|
||||
|
||||
val sortOrders: Set<SortOrder>
|
||||
|
||||
suspend fun getList(offset: Int, query: String? = null, sortOrder: SortOrder? = null, tag: MangaTag? = null): List<Manga>
|
||||
suspend fun getList(
|
||||
offset: Int,
|
||||
query: String? = null,
|
||||
sortOrder: SortOrder? = null,
|
||||
tag: MangaTag? = null
|
||||
): List<Manga>
|
||||
|
||||
suspend fun getDetails(manga: Manga) : Manga
|
||||
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>
|
||||
}
|
||||
@@ -1,22 +1,25 @@
|
||||
package org.koitharu.kotatsu.core.parser
|
||||
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.parser.site.*
|
||||
|
||||
val parserModule
|
||||
get() = module {
|
||||
single { LocalMangaRepository(get()) } bind MangaRepository::class
|
||||
|
||||
factory { ReadmangaRepository(get()) } bind MangaRepository::class
|
||||
factory { MintMangaRepository(get()) } bind MangaRepository::class
|
||||
factory { SelfMangaRepository(get()) } bind MangaRepository::class
|
||||
factory { MangaChanRepository(get()) } bind MangaRepository::class
|
||||
factory { DesuMeRepository(get()) } bind MangaRepository::class
|
||||
factory { HenChanRepository(get()) } bind MangaRepository::class
|
||||
factory { YaoiChanRepository(get()) } bind MangaRepository::class
|
||||
factory { MangaTownRepository(get()) } bind MangaRepository::class
|
||||
factory { MangaLibRepository(get()) } bind MangaRepository::class
|
||||
factory { NudeMoonRepository(get()) } bind MangaRepository::class
|
||||
factory { MangareadRepository(get()) } bind MangaRepository::class
|
||||
single { MangaLoaderContext() }
|
||||
|
||||
factory<MangaRepository>(named(MangaSource.READMANGA_RU)) { ReadmangaRepository(get()) }
|
||||
factory<MangaRepository>(named(MangaSource.MINTMANGA)) { MintMangaRepository(get()) }
|
||||
factory<MangaRepository>(named(MangaSource.SELFMANGA)) { SelfMangaRepository(get()) }
|
||||
factory<MangaRepository>(named(MangaSource.MANGACHAN)) { MangaChanRepository(get()) }
|
||||
factory<MangaRepository>(named(MangaSource.DESUME)) { DesuMeRepository(get()) }
|
||||
factory<MangaRepository>(named(MangaSource.HENCHAN)) { HenChanRepository(get()) }
|
||||
factory<MangaRepository>(named(MangaSource.YAOICHAN)) { YaoiChanRepository(get()) }
|
||||
factory<MangaRepository>(named(MangaSource.MANGATOWN)) { MangaTownRepository(get()) }
|
||||
factory<MangaRepository>(named(MangaSource.MANGALIB)) { MangaLibRepository(get()) }
|
||||
factory<MangaRepository>(named(MangaSource.NUDEMOON)) { NudeMoonRepository(get()) }
|
||||
factory<MangaRepository>(named(MangaSource.MANGAREAD)) { MangareadRepository(get()) }
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package org.koitharu.kotatsu.core.parser
|
||||
|
||||
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.model.MangaTag
|
||||
import org.koitharu.kotatsu.core.model.SortOrder
|
||||
import org.koitharu.kotatsu.domain.MangaLoaderContext
|
||||
|
||||
abstract class RemoteMangaRepository(
|
||||
protected val loaderContext: MangaLoaderContext
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package org.koitharu.kotatsu.core.parser.site
|
||||
|
||||
import androidx.collection.arraySetOf
|
||||
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.core.exceptions.ParseException
|
||||
import org.koitharu.kotatsu.core.model.*
|
||||
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.SourceSettings
|
||||
import org.koitharu.kotatsu.domain.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.utils.ext.*
|
||||
import java.util.*
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package org.koitharu.kotatsu.core.parser.site
|
||||
|
||||
import androidx.collection.arraySetOf
|
||||
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.core.exceptions.ParseException
|
||||
import org.koitharu.kotatsu.core.model.*
|
||||
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.SourceSettings
|
||||
import org.koitharu.kotatsu.domain.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.utils.ext.*
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
@@ -2,11 +2,11 @@ package org.koitharu.kotatsu.core.parser.site
|
||||
|
||||
import androidx.collection.arraySetOf
|
||||
import androidx.core.net.toUri
|
||||
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.core.exceptions.ParseException
|
||||
import org.koitharu.kotatsu.core.model.*
|
||||
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.SourceSettings
|
||||
import org.koitharu.kotatsu.domain.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.utils.ext.*
|
||||
import java.util.*
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package org.koitharu.kotatsu.core.parser.site
|
||||
|
||||
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.core.exceptions.ParseException
|
||||
import org.koitharu.kotatsu.core.model.*
|
||||
import org.koitharu.kotatsu.domain.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.utils.ext.longHashCode
|
||||
import org.koitharu.kotatsu.utils.ext.mapToSet
|
||||
import org.koitharu.kotatsu.utils.ext.parseHtml
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.koitharu.kotatsu.core.parser.site
|
||||
|
||||
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.domain.MangaLoaderContext
|
||||
|
||||
class MangaChanRepository(loaderContext: MangaLoaderContext) : ChanRepository(loaderContext) {
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@ import androidx.collection.ArraySet
|
||||
import androidx.collection.arraySetOf
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.core.exceptions.ParseException
|
||||
import org.koitharu.kotatsu.core.model.*
|
||||
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.SourceSettings
|
||||
import org.koitharu.kotatsu.domain.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.utils.ext.*
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
@@ -2,11 +2,11 @@ package org.koitharu.kotatsu.core.parser.site
|
||||
|
||||
import androidx.collection.arraySetOf
|
||||
import org.intellij.lang.annotations.Language
|
||||
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.core.exceptions.ParseException
|
||||
import org.koitharu.kotatsu.core.model.*
|
||||
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.SourceSettings
|
||||
import org.koitharu.kotatsu.domain.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.utils.ext.*
|
||||
import java.util.*
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package org.koitharu.kotatsu.core.parser.site
|
||||
|
||||
import androidx.collection.arraySetOf
|
||||
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.core.exceptions.ParseException
|
||||
import org.koitharu.kotatsu.core.model.*
|
||||
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.SourceSettings
|
||||
import org.koitharu.kotatsu.domain.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.utils.ext.*
|
||||
import java.util.*
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.koitharu.kotatsu.core.parser.site
|
||||
|
||||
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.domain.MangaLoaderContext
|
||||
|
||||
class MintMangaRepository(loaderContext: MangaLoaderContext) : GroupleRepository(loaderContext) {
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package org.koitharu.kotatsu.core.parser.site
|
||||
|
||||
import androidx.collection.arraySetOf
|
||||
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.core.exceptions.ParseException
|
||||
import org.koitharu.kotatsu.core.model.*
|
||||
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.SourceSettings
|
||||
import org.koitharu.kotatsu.domain.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.utils.ext.*
|
||||
import java.util.*
|
||||
import java.util.regex.Pattern
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.koitharu.kotatsu.core.parser.site
|
||||
|
||||
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.domain.MangaLoaderContext
|
||||
|
||||
class ReadmangaRepository(loaderContext: MangaLoaderContext) : GroupleRepository(loaderContext) {
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.koitharu.kotatsu.core.parser.site
|
||||
|
||||
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.domain.MangaLoaderContext
|
||||
|
||||
class SelfMangaRepository(loaderContext: MangaLoaderContext) : GroupleRepository(loaderContext) {
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package org.koitharu.kotatsu.core.parser.site
|
||||
|
||||
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.core.exceptions.ParseException
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.MangaChapter
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.domain.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.utils.ext.longHashCode
|
||||
import org.koitharu.kotatsu.utils.ext.parseHtml
|
||||
import org.koitharu.kotatsu.utils.ext.withDomain
|
||||
|
||||
@@ -7,8 +7,11 @@ import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.collection.arraySetOf
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.channels.sendBlocking
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import org.koitharu.kotatsu.core.model.ZoomMode
|
||||
import org.koitharu.kotatsu.core.parser.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.utils.delegates.prefs.*
|
||||
import java.io.File
|
||||
|
||||
@@ -19,7 +22,7 @@ class AppSettings private constructor(private val prefs: SharedPreferences) :
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
)
|
||||
|
||||
var listMode by IntEnumPreferenceDelegate(
|
||||
var listMode by EnumPreferenceDelegate(
|
||||
ListMode::class.java,
|
||||
KEY_LIST_MODE,
|
||||
ListMode.DETAILED_LIST
|
||||
@@ -38,7 +41,7 @@ class AppSettings private constructor(private val prefs: SharedPreferences) :
|
||||
|
||||
val isAmoledTheme by BoolPreferenceDelegate(KEY_THEME_AMOLED, defaultValue = false)
|
||||
|
||||
val gridSize by IntPreferenceDelegate(KEY_GRID_SIZE, defaultValue = 100)
|
||||
var gridSize by IntPreferenceDelegate(KEY_GRID_SIZE, defaultValue = 100)
|
||||
|
||||
val readerPageSwitch by StringSetPreferenceDelegate(
|
||||
KEY_READER_SWITCHERS,
|
||||
@@ -117,6 +120,16 @@ class AppSettings private constructor(private val prefs: SharedPreferences) :
|
||||
prefs.unregisterOnSharedPreferenceChangeListener(listener)
|
||||
}
|
||||
|
||||
fun observe() = callbackFlow<String> {
|
||||
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
||||
sendBlocking(key)
|
||||
}
|
||||
prefs.registerOnSharedPreferenceChangeListener(listener)
|
||||
awaitClose {
|
||||
prefs.unregisterOnSharedPreferenceChangeListener(listener)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val PAGE_SWITCH_TAPS = "taps"
|
||||
@@ -125,7 +138,7 @@ class AppSettings private constructor(private val prefs: SharedPreferences) :
|
||||
const val TRACK_HISTORY = "history"
|
||||
const val TRACK_FAVOURITES = "favourites"
|
||||
|
||||
const val KEY_LIST_MODE = "list_mode"
|
||||
const val KEY_LIST_MODE = "list_mode_2"
|
||||
const val KEY_APP_SECTION = "app_section"
|
||||
const val KEY_THEME = "theme"
|
||||
const val KEY_THEME_AMOLED = "amoled_theme"
|
||||
|
||||
@@ -7,7 +7,7 @@ import org.koitharu.kotatsu.utils.delegates.prefs.LongPreferenceDelegate
|
||||
class AppWidgetConfig private constructor(
|
||||
private val prefs: SharedPreferences,
|
||||
val widgetId: Int
|
||||
) : SharedPreferences by prefs {
|
||||
) : SharedPreferences by prefs {
|
||||
|
||||
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.Intent
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.base
|
||||
package org.koitharu.kotatsu.core.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.utils
|
||||
package org.koitharu.kotatsu.core.ui
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
@@ -10,7 +10,7 @@ import android.view.MenuItem
|
||||
import android.view.View
|
||||
import kotlinx.android.synthetic.main.activity_crash.*
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.ui.list.MainActivity
|
||||
import org.koitharu.kotatsu.main.ui.MainActivity
|
||||
import org.koitharu.kotatsu.utils.ShareHelper
|
||||
|
||||
class CrashActivity : Activity(), View.OnClickListener {
|
||||
@@ -1,11 +1,11 @@
|
||||
package org.koitharu.kotatsu.ui.base
|
||||
package org.koitharu.kotatsu.core.ui
|
||||
|
||||
import coil.ComponentRegistry
|
||||
import coil.ImageLoader
|
||||
import okhttp3.OkHttpClient
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.dsl.module
|
||||
import org.koitharu.kotatsu.core.local.CbzFetcher
|
||||
import org.koitharu.kotatsu.local.data.CbzFetcher
|
||||
|
||||
val uiModule
|
||||
get() = module {
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.koitharu.kotatsu.details
|
||||
|
||||
import org.koin.android.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
import org.koitharu.kotatsu.base.domain.MangaIntent
|
||||
import org.koitharu.kotatsu.details.ui.DetailsViewModel
|
||||
|
||||
val detailsModule
|
||||
get() = module {
|
||||
|
||||
viewModel { (intent: MangaIntent) ->
|
||||
DetailsViewModel(intent, get(), get(), get(), get(), get())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package org.koitharu.kotatsu.details.ui
|
||||
|
||||
import android.app.ActivityOptions
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.fragment_chapters.*
|
||||
import org.koin.android.viewmodel.ext.android.sharedViewModel
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.model.MangaChapter
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter
|
||||
import org.koitharu.kotatsu.details.ui.adapter.ChaptersSelectionDecoration
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||
import org.koitharu.kotatsu.download.DownloadService
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
|
||||
class ChaptersFragment : BaseFragment(R.layout.fragment_chapters),
|
||||
OnListItemClickListener<MangaChapter>, ActionMode.Callback {
|
||||
|
||||
private val viewModel by sharedViewModel<DetailsViewModel>()
|
||||
|
||||
private var chaptersAdapter: ChaptersAdapter? = null
|
||||
private var actionMode: ActionMode? = null
|
||||
private var selectionDecoration: ChaptersSelectionDecoration? = null
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
chaptersAdapter = ChaptersAdapter(this)
|
||||
selectionDecoration = ChaptersSelectionDecoration(view.context)
|
||||
with(recyclerView_chapters) {
|
||||
addItemDecoration(DividerItemDecoration(view.context, RecyclerView.VERTICAL))
|
||||
addItemDecoration(selectionDecoration!!)
|
||||
setHasFixedSize(true)
|
||||
adapter = chaptersAdapter
|
||||
}
|
||||
|
||||
viewModel.isLoading.observe(viewLifecycleOwner, this::onLoadingStateChanged)
|
||||
viewModel.chapters.observe(viewLifecycleOwner, this::onChaptersChanged)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
chaptersAdapter = null
|
||||
selectionDecoration = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun onChaptersChanged(list: List<ChapterListItem>) {
|
||||
chaptersAdapter?.items = list
|
||||
}
|
||||
|
||||
private fun onLoadingStateChanged(isLoading: Boolean) {
|
||||
progressBar.isVisible = isLoading
|
||||
}
|
||||
|
||||
override fun onItemClick(item: MangaChapter, view: View) {
|
||||
if (selectionDecoration?.checkedItemsCount != 0) {
|
||||
selectionDecoration?.toggleItemChecked(item.id)
|
||||
if (selectionDecoration?.checkedItemsCount == 0) {
|
||||
actionMode?.finish()
|
||||
} else {
|
||||
actionMode?.invalidate()
|
||||
recyclerView_chapters.invalidateItemDecorations()
|
||||
}
|
||||
return
|
||||
}
|
||||
val options = ActivityOptions.makeScaleUpAnimation(
|
||||
view,
|
||||
0,
|
||||
0,
|
||||
view.measuredWidth,
|
||||
view.measuredHeight
|
||||
)
|
||||
startActivity(
|
||||
ReaderActivity.newIntent(
|
||||
context ?: return,
|
||||
viewModel.manga.value ?: return,
|
||||
item.id
|
||||
), options.toBundle()
|
||||
)
|
||||
}
|
||||
|
||||
override fun onItemLongClick(item: MangaChapter, view: View): Boolean {
|
||||
if (actionMode == null) {
|
||||
actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this)
|
||||
}
|
||||
return actionMode?.also {
|
||||
selectionDecoration?.setItemIsChecked(item.id, true)
|
||||
recyclerView_chapters.invalidateItemDecorations()
|
||||
it.invalidate()
|
||||
} != null
|
||||
}
|
||||
|
||||
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.action_save -> {
|
||||
DownloadService.start(
|
||||
context ?: return false,
|
||||
viewModel.manga.value ?: return false,
|
||||
selectionDecoration?.checkedItemsIds
|
||||
)
|
||||
mode.finish()
|
||||
true
|
||||
}
|
||||
R.id.action_select_all -> {
|
||||
val ids = chaptersAdapter?.items?.map { it.chapter.id } ?: return false
|
||||
selectionDecoration?.checkAll(ids)
|
||||
recyclerView_chapters.invalidateItemDecorations()
|
||||
mode.invalidate()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
val manga = viewModel.manga.value
|
||||
mode.menuInflater.inflate(R.menu.mode_chapters, menu)
|
||||
menu.findItem(R.id.action_save).isVisible = manga?.source != MangaSource.LOCAL
|
||||
mode.title = manga?.title
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
val count = selectionDecoration?.checkedItemsCount ?: return false
|
||||
mode.subtitle = resources.getQuantityString(
|
||||
R.plurals.chapters_from_x,
|
||||
count,
|
||||
count,
|
||||
chaptersAdapter?.itemCount ?: 0
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode?) {
|
||||
selectionDecoration?.clearSelection()
|
||||
recyclerView_chapters.invalidateItemDecorations()
|
||||
actionMode = null
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.details
|
||||
package org.koitharu.kotatsu.details.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@@ -7,70 +7,58 @@ import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.net.toFile
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import kotlinx.android.synthetic.main.activity_details.*
|
||||
import kotlinx.coroutines.launch
|
||||
import moxy.MvpDelegate
|
||||
import moxy.ktx.moxyPresenter
|
||||
import org.koin.android.viewmodel.ext.android.viewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
import org.koitharu.kotatsu.base.domain.MangaIntent
|
||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.browser.BrowserActivity
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.ui.base.BaseActivity
|
||||
import org.koitharu.kotatsu.ui.browser.BrowserActivity
|
||||
import org.koitharu.kotatsu.ui.download.DownloadService
|
||||
import org.koitharu.kotatsu.download.DownloadService
|
||||
import org.koitharu.kotatsu.search.ui.global.GlobalSearchActivity
|
||||
import org.koitharu.kotatsu.utils.MangaShortcut
|
||||
import org.koitharu.kotatsu.utils.ShareHelper
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.utils.ext.getThemeColor
|
||||
|
||||
class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
|
||||
TabLayoutMediator.TabConfigurationStrategy {
|
||||
class DetailsActivity : BaseActivity(), TabLayoutMediator.TabConfigurationStrategy {
|
||||
|
||||
private val presenter by moxyPresenter {
|
||||
MangaDetailsPresenter.getInstance(hashCode())
|
||||
private val viewModel by viewModel<DetailsViewModel> {
|
||||
parametersOf(MangaIntent.from(intent))
|
||||
}
|
||||
|
||||
private var manga: Manga? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_details)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
pager.adapter = MangaDetailsAdapter(this)
|
||||
TabLayoutMediator(tabs, pager, this).attach()
|
||||
if (savedInstanceState?.containsKey(MvpDelegate.MOXY_DELEGATE_TAGS_KEY) != true) {
|
||||
intent?.getParcelableExtra<Manga>(EXTRA_MANGA)?.let {
|
||||
presenter.loadDetails(it, true)
|
||||
} ?: intent?.getLongExtra(EXTRA_MANGA_ID, 0)?.takeUnless { it == 0L }?.let {
|
||||
presenter.findMangaById(it)
|
||||
} ?: finishAfterTransition()
|
||||
}
|
||||
|
||||
viewModel.manga.observe(this, ::onMangaUpdated)
|
||||
viewModel.newChaptersCount.observe(this, ::onNewChaptersChanged)
|
||||
viewModel.onMangaRemoved.observe(this, ::onMangaRemoved)
|
||||
viewModel.onError.observe(this, ::onError)
|
||||
}
|
||||
|
||||
override fun onMangaUpdated(manga: Manga) {
|
||||
this.manga = manga
|
||||
private fun onMangaUpdated(manga: Manga) {
|
||||
title = manga.title
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
override fun onHistoryChanged(history: MangaHistory?) = Unit
|
||||
|
||||
override fun onFavouriteChanged(categories: List<FavouriteCategory>) = Unit
|
||||
|
||||
override fun onLoadingStateChanged(isLoading: Boolean) = Unit
|
||||
|
||||
override fun onMangaRemoved(manga: Manga) {
|
||||
private fun onMangaRemoved(manga: Manga) {
|
||||
Toast.makeText(
|
||||
this, getString(R.string._s_deleted_from_local_storage, manga.title),
|
||||
Toast.LENGTH_SHORT
|
||||
@@ -78,8 +66,8 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
|
||||
finishAfterTransition()
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
if (manga == null) {
|
||||
private fun onError(e: Throwable) {
|
||||
if (viewModel.manga.value == null) {
|
||||
Toast.makeText(this, e.getDisplayMessage(resources), Toast.LENGTH_LONG).show()
|
||||
finishAfterTransition()
|
||||
} else {
|
||||
@@ -87,7 +75,7 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewChaptersChanged(newChapters: Int) {
|
||||
private fun onNewChaptersChanged(newChapters: Int) {
|
||||
val tab = tabs.getTabAt(1) ?: return
|
||||
if (newChapters == 0) {
|
||||
tab.removeBadge()
|
||||
@@ -104,8 +92,9 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
||||
val manga = viewModel.manga.value
|
||||
menu.findItem(R.id.action_save).isVisible =
|
||||
manga?.source != null && manga?.source != MangaSource.LOCAL
|
||||
manga?.source != null && manga.source != MangaSource.LOCAL
|
||||
menu.findItem(R.id.action_delete).isVisible =
|
||||
manga?.source == MangaSource.LOCAL
|
||||
menu.findItem(R.id.action_browser).isVisible =
|
||||
@@ -117,7 +106,7 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
R.id.action_share -> {
|
||||
manga?.let {
|
||||
viewModel.manga.value?.let {
|
||||
if (it.source == MangaSource.LOCAL) {
|
||||
ShareHelper.shareCbz(this, Uri.parse(it.url).toFile())
|
||||
} else {
|
||||
@@ -127,12 +116,12 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
|
||||
true
|
||||
}
|
||||
R.id.action_delete -> {
|
||||
manga?.let { m ->
|
||||
MaterialAlertDialogBuilder(this)
|
||||
viewModel.manga.value?.let { m ->
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.delete_manga)
|
||||
.setMessage(getString(R.string.text_delete_local_manga, m.title))
|
||||
.setPositiveButton(R.string.delete) { _, _ ->
|
||||
presenter.deleteLocal(m)
|
||||
viewModel.deleteLocal(m)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
@@ -140,10 +129,10 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
|
||||
true
|
||||
}
|
||||
R.id.action_save -> {
|
||||
manga?.let {
|
||||
viewModel.manga.value?.let {
|
||||
val chaptersCount = it.chapters?.size ?: 0
|
||||
if (chaptersCount > 5) {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.save_manga)
|
||||
.setMessage(
|
||||
getString(
|
||||
@@ -166,15 +155,21 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
|
||||
true
|
||||
}
|
||||
R.id.action_browser -> {
|
||||
manga?.let {
|
||||
viewModel.manga.value?.let {
|
||||
startActivity(BrowserActivity.newIntent(this, it.url))
|
||||
}
|
||||
true
|
||||
}
|
||||
R.id.action_related -> {
|
||||
viewModel.manga.value?.let {
|
||||
startActivity(GlobalSearchActivity.newIntent(this, it.title))
|
||||
}
|
||||
true
|
||||
}
|
||||
R.id.action_shortcut -> {
|
||||
manga?.let {
|
||||
viewModel.manga.value?.let {
|
||||
lifecycleScope.launch {
|
||||
if (!MangaShortcut(it).requestPinShortcut(this@MangaDetailsActivity)) {
|
||||
if (!MangaShortcut(it).requestPinShortcut(this@DetailsActivity)) {
|
||||
Snackbar.make(
|
||||
pager,
|
||||
R.string.operation_not_supported,
|
||||
@@ -192,7 +187,6 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
|
||||
tab.text = when (position) {
|
||||
0 -> getString(R.string.details)
|
||||
1 -> getString(R.string.chapters)
|
||||
2 -> getString(R.string.related)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -211,17 +205,14 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
|
||||
|
||||
companion object {
|
||||
|
||||
private const val EXTRA_MANGA = "manga"
|
||||
const val EXTRA_MANGA_ID = "manga_id"
|
||||
|
||||
const val ACTION_MANGA_VIEW = "${BuildConfig.APPLICATION_ID}.action.VIEW_MANGA"
|
||||
|
||||
fun newIntent(context: Context, manga: Manga) =
|
||||
Intent(context, MangaDetailsActivity::class.java)
|
||||
.putExtra(EXTRA_MANGA, manga)
|
||||
Intent(context, DetailsActivity::class.java)
|
||||
.putExtra(MangaIntent.KEY_MANGA, manga)
|
||||
|
||||
fun newIntent(context: Context, mangaId: Long) =
|
||||
Intent(context, MangaDetailsActivity::class.java)
|
||||
.putExtra(EXTRA_MANGA_ID, mangaId)
|
||||
Intent(context, DetailsActivity::class.java)
|
||||
.putExtra(MangaIntent.KEY_ID, mangaId)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.koitharu.kotatsu.ui.details
|
||||
package org.koitharu.kotatsu.details.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.Spanned
|
||||
import android.view.View
|
||||
import androidx.core.net.toUri
|
||||
@@ -10,37 +11,36 @@ import kotlinx.android.synthetic.main.fragment_details.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import moxy.ktx.moxyPresenter
|
||||
import org.koin.android.viewmodel.ext.android.sharedViewModel
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
import org.koitharu.kotatsu.base.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||
import org.koitharu.kotatsu.ui.base.BaseFragment
|
||||
import org.koitharu.kotatsu.ui.list.favourites.categories.select.FavouriteCategoriesDialog
|
||||
import org.koitharu.kotatsu.ui.reader.ReaderActivity
|
||||
import org.koitharu.kotatsu.ui.search.MangaSearchSheet
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesDialog
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
import org.koitharu.kotatsu.search.ui.MangaSearchSheet
|
||||
import org.koitharu.kotatsu.utils.FileSizeUtils
|
||||
import org.koitharu.kotatsu.utils.ext.*
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetailsView,
|
||||
View.OnClickListener,
|
||||
class DetailsFragment : BaseFragment(R.layout.fragment_details), View.OnClickListener,
|
||||
View.OnLongClickListener {
|
||||
|
||||
@Suppress("unused")
|
||||
private val presenter by moxyPresenter {
|
||||
MangaDetailsPresenter.getInstance(activity.hashCode())
|
||||
private val viewModel by sharedViewModel<DetailsViewModel>()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
viewModel.manga.observe(viewLifecycleOwner, ::onMangaUpdated)
|
||||
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
|
||||
viewModel.favouriteCategories.observe(viewLifecycleOwner, ::onFavouriteChanged)
|
||||
viewModel.readingHistory.observe(viewLifecycleOwner, ::onHistoryChanged)
|
||||
}
|
||||
|
||||
private var manga: Manga? = null
|
||||
private var history: MangaHistory? = null
|
||||
|
||||
override fun onMangaUpdated(manga: Manga) {
|
||||
this.manga = manga
|
||||
private fun onMangaUpdated(manga: Manga) {
|
||||
imageView_cover.newImageRequest(manga.largeCoverUrl ?: manga.coverUrl)
|
||||
.fallback(R.drawable.ic_placeholder)
|
||||
.crossfade(true)
|
||||
.lifecycle(this)
|
||||
.lifecycle(viewLifecycleOwner)
|
||||
.enqueueWith(coil)
|
||||
textView_title.text = manga.title
|
||||
textView_subtitle.textAndVisible = manga.altTitle
|
||||
@@ -59,7 +59,7 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
|
||||
text = it,
|
||||
iconRes = R.drawable.ic_chip_user,
|
||||
tag = it,
|
||||
onClickListener = this@MangaDetailsFragment
|
||||
onClickListener = this@DetailsFragment
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
|
||||
text = it.title,
|
||||
iconRes = R.drawable.ic_chip_tag,
|
||||
tag = it,
|
||||
onClickListener = this@MangaDetailsFragment
|
||||
onClickListener = this@DetailsFragment
|
||||
)
|
||||
}
|
||||
manga.url.toUri().toFileOrNull()?.let { f ->
|
||||
@@ -81,7 +81,7 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
|
||||
text = FileSizeUtils.formatBytes(context, size),
|
||||
iconRes = R.drawable.ic_chip_storage,
|
||||
tag = it,
|
||||
onClickListener = this@MangaDetailsFragment
|
||||
onClickListener = this@DetailsFragment
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -89,35 +89,37 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
|
||||
imageView_favourite.setOnClickListener(this)
|
||||
button_read.setOnClickListener(this)
|
||||
button_read.setOnLongClickListener(this)
|
||||
updateReadButton()
|
||||
button_read.isEnabled = !manga.chapters.isNullOrEmpty()
|
||||
}
|
||||
|
||||
override fun onHistoryChanged(history: MangaHistory?) {
|
||||
this.history = history
|
||||
updateReadButton()
|
||||
}
|
||||
|
||||
override fun onFavouriteChanged(categories: List<FavouriteCategory>) {
|
||||
imageView_favourite.setImageResource(
|
||||
if (categories.isEmpty()) {
|
||||
R.drawable.ic_heart_outline
|
||||
private fun onHistoryChanged(history: MangaHistory?) {
|
||||
with(button_read) {
|
||||
if (history == null) {
|
||||
setText(R.string.read)
|
||||
setIconResource(R.drawable.ic_read)
|
||||
} else {
|
||||
setText(R.string._continue)
|
||||
setIconResource(R.drawable.ic_play)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onFavouriteChanged(isFavourite: Boolean) {
|
||||
imageView_favourite.setImageResource(
|
||||
if (isFavourite) {
|
||||
R.drawable.ic_heart
|
||||
} else {
|
||||
R.drawable.ic_heart_outline
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onLoadingStateChanged(isLoading: Boolean) {
|
||||
private fun onLoadingStateChanged(isLoading: Boolean) {
|
||||
progressBar.isVisible = isLoading
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) = Unit //handled in activity
|
||||
|
||||
override fun onMangaRemoved(manga: Manga) = Unit //handled in activity
|
||||
|
||||
override fun onNewChaptersChanged(newChapters: Int) = Unit
|
||||
|
||||
override fun onClick(v: View) {
|
||||
val manga = viewModel.manga.value
|
||||
when {
|
||||
v.id == R.id.imageView_favourite -> {
|
||||
FavouriteCategoriesDialog.show(childFragmentManager, manga ?: return)
|
||||
@@ -127,7 +129,7 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
|
||||
ReaderActivity.newIntent(
|
||||
context ?: return,
|
||||
manga ?: return,
|
||||
history
|
||||
viewModel.readingHistory.value
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -146,7 +148,7 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
|
||||
override fun onLongClick(v: View): Boolean {
|
||||
when (v.id) {
|
||||
R.id.button_read -> {
|
||||
if (history == null) {
|
||||
if (viewModel.readingHistory.value == null) {
|
||||
return false
|
||||
}
|
||||
v.showPopupMenu(R.menu.popup_read) {
|
||||
@@ -155,7 +157,7 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
|
||||
startActivity(
|
||||
ReaderActivity.newIntent(
|
||||
context ?: return@showPopupMenu false,
|
||||
manga ?: return@showPopupMenu false
|
||||
viewModel.manga.value ?: return@showPopupMenu false
|
||||
)
|
||||
)
|
||||
true
|
||||
@@ -168,19 +170,4 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateReadButton() {
|
||||
if (manga?.chapters.isNullOrEmpty()) {
|
||||
button_read.isEnabled = false
|
||||
} else {
|
||||
button_read.isEnabled = true
|
||||
if (history == null) {
|
||||
button_read.setText(R.string.read)
|
||||
button_read.setIconResource(R.drawable.ic_read)
|
||||
} else {
|
||||
button_read.setText(R.string._continue)
|
||||
button_read.setIconResource(R.drawable.ic_play)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package org.koitharu.kotatsu.details.ui
|
||||
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.*
|
||||
import org.koitharu.kotatsu.base.domain.MangaDataRepository
|
||||
import org.koitharu.kotatsu.base.domain.MangaIntent
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.exceptions.MangaNotFoundException
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.details.ui.model.toListItem
|
||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||
import org.koitharu.kotatsu.history.domain.ChapterExtra
|
||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.ext.safe
|
||||
import java.io.IOException
|
||||
|
||||
class DetailsViewModel(
|
||||
intent: MangaIntent,
|
||||
private val historyRepository: HistoryRepository,
|
||||
private val favouritesRepository: FavouritesRepository,
|
||||
private val localMangaRepository: LocalMangaRepository,
|
||||
private val trackingRepository: TrackingRepository,
|
||||
private val mangaDataRepository: MangaDataRepository
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val mangaData = MutableStateFlow<Manga?>(intent.manga)
|
||||
|
||||
private val history = mangaData.mapNotNull { it?.id }
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest { mangaId ->
|
||||
historyRepository.observeOne(mangaId)
|
||||
}.stateIn(viewModelScope, SharingStarted.Eagerly, null)
|
||||
|
||||
private val favourite = mangaData.mapNotNull { it?.id }
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest { mangaId ->
|
||||
favouritesRepository.observeCategoriesIds(mangaId).map { it.isNotEmpty() }
|
||||
}.stateIn(viewModelScope, SharingStarted.Eagerly, false)
|
||||
|
||||
private val newChapters = mangaData.mapNotNull { it?.id }
|
||||
.distinctUntilChanged()
|
||||
.mapLatest { mangaId ->
|
||||
trackingRepository.getNewChaptersCount(mangaId)
|
||||
}.stateIn(viewModelScope, SharingStarted.Eagerly, 0)
|
||||
|
||||
val manga = mangaData.filterNotNull()
|
||||
.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
val favouriteCategories = favourite
|
||||
.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
val newChaptersCount = newChapters
|
||||
.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
val readingHistory = history
|
||||
.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
|
||||
val onMangaRemoved = SingleLiveEvent<Manga>()
|
||||
|
||||
val chapters = combine(
|
||||
mangaData.map { it?.chapters.orEmpty() },
|
||||
history.map { it?.chapterId },
|
||||
newChapters
|
||||
) { chapters, currentId, newCount ->
|
||||
val currentIndex = chapters.indexOfFirst { it.id == currentId }
|
||||
val firstNewIndex = chapters.size - newCount
|
||||
chapters.mapIndexed { index, chapter ->
|
||||
chapter.toListItem(
|
||||
when {
|
||||
index >= firstNewIndex -> ChapterExtra.NEW
|
||||
index == currentIndex -> ChapterExtra.CURRENT
|
||||
index < currentIndex -> ChapterExtra.READ
|
||||
else -> ChapterExtra.UNREAD
|
||||
}
|
||||
)
|
||||
}
|
||||
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
|
||||
init {
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
var manga = mangaDataRepository.resolveIntent(intent)
|
||||
?: throw MangaNotFoundException("Cannot find manga")
|
||||
mangaData.value = manga
|
||||
manga = manga.source.repository.getDetails(manga)
|
||||
mangaData.value = manga
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteLocal(manga: Manga) {
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
val original = localMangaRepository.getRemoteManga(manga)
|
||||
localMangaRepository.delete(manga) || throw IOException("Unable to delete file")
|
||||
safe {
|
||||
historyRepository.deleteOrSwap(manga, original)
|
||||
}
|
||||
onMangaRemoved.postCall(manga)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.details
|
||||
package org.koitharu.kotatsu.details.ui
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
@@ -6,12 +6,11 @@ import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
|
||||
class MangaDetailsAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
|
||||
|
||||
override fun getItemCount() = 3
|
||||
override fun getItemCount() = 2
|
||||
|
||||
override fun createFragment(position: Int): Fragment = when(position) {
|
||||
0 -> MangaDetailsFragment()
|
||||
override fun createFragment(position: Int): Fragment = when (position) {
|
||||
0 -> DetailsFragment()
|
||||
1 -> ChaptersFragment()
|
||||
2 -> RelatedMangaFragment()
|
||||
else -> throw IndexOutOfBoundsException("No fragment for position $position")
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,29 @@
|
||||
package org.koitharu.kotatsu.ui.details
|
||||
package org.koitharu.kotatsu.details.ui.adapter
|
||||
|
||||
import android.graphics.Color
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateLayoutContainer
|
||||
import kotlinx.android.synthetic.main.item_chapter.*
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.model.MangaChapter
|
||||
import org.koitharu.kotatsu.domain.history.ChapterExtra
|
||||
import org.koitharu.kotatsu.ui.base.list.BaseViewHolder
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||
import org.koitharu.kotatsu.history.domain.ChapterExtra
|
||||
import org.koitharu.kotatsu.utils.ext.getThemeColor
|
||||
|
||||
class ChapterHolder(parent: ViewGroup) :
|
||||
BaseViewHolder<MangaChapter, ChapterExtra>(parent, R.layout.item_chapter) {
|
||||
fun chapterListItemAD(
|
||||
clickListener: OnListItemClickListener<MangaChapter>
|
||||
) = adapterDelegateLayoutContainer<ChapterListItem, ChapterListItem>(R.layout.item_chapter) {
|
||||
|
||||
override fun onBind(data: MangaChapter, extra: ChapterExtra) {
|
||||
textView_title.text = data.name
|
||||
textView_number.text = data.number.toString()
|
||||
imageView_check.isVisible = extra == ChapterExtra.CHECKED
|
||||
when (extra) {
|
||||
itemView.setOnClickListener {
|
||||
clickListener.onItemClick(item.chapter, it)
|
||||
}
|
||||
itemView.setOnLongClickListener {
|
||||
clickListener.onItemLongClick(item.chapter, it)
|
||||
}
|
||||
|
||||
bind { payload ->
|
||||
textView_title.text = item.chapter.name
|
||||
textView_number.text = item.chapter.number.toString()
|
||||
when (item.extra) {
|
||||
ChapterExtra.UNREAD -> {
|
||||
textView_number.setBackgroundResource(R.drawable.bg_badge_default)
|
||||
textView_number.setTextColor(context.getThemeColor(android.R.attr.textColorSecondaryInverse))
|
||||
@@ -34,10 +40,6 @@ class ChapterHolder(parent: ViewGroup) :
|
||||
textView_number.setBackgroundResource(R.drawable.bg_badge_accent)
|
||||
textView_number.setTextColor(context.getThemeColor(android.R.attr.textColorPrimaryInverse))
|
||||
}
|
||||
ChapterExtra.CHECKED -> {
|
||||
textView_number.background = null
|
||||
textView_number.setTextColor(Color.TRANSPARENT)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.koitharu.kotatsu.details.ui.adapter
|
||||
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.model.MangaChapter
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||
import kotlin.jvm.internal.Intrinsics
|
||||
|
||||
class ChaptersAdapter(
|
||||
onItemClickListener: OnListItemClickListener<MangaChapter>
|
||||
) : AsyncListDifferDelegationAdapter<ChapterListItem>(DiffCallback()) {
|
||||
|
||||
init {
|
||||
setHasStableIds(true)
|
||||
delegatesManager.addDelegate(chapterListItemAD(onItemClickListener))
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return items[position].chapter.id
|
||||
}
|
||||
|
||||
private class DiffCallback : DiffUtil.ItemCallback<ChapterListItem>() {
|
||||
|
||||
override fun areItemsTheSame(oldItem: ChapterListItem, newItem: ChapterListItem): Boolean {
|
||||
return oldItem.chapter.id == newItem.chapter.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: ChapterListItem, newItem: ChapterListItem): Boolean {
|
||||
return Intrinsics.areEqual(oldItem, newItem)
|
||||
}
|
||||
|
||||
override fun getChangePayload(oldItem: ChapterListItem, newItem: ChapterListItem): Any? {
|
||||
if (oldItem.extra != newItem.extra) {
|
||||
return newItem.extra
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package org.koitharu.kotatsu.details.ui.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import androidx.collection.ArraySet
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.children
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.utils.ext.getThemeColor
|
||||
import org.koitharu.kotatsu.utils.ext.resolveDp
|
||||
|
||||
class ChaptersSelectionDecoration(context: Context) : RecyclerView.ItemDecoration() {
|
||||
|
||||
private val icon = ContextCompat.getDrawable(context, R.drawable.ic_check)
|
||||
private val padding = context.resources.resolveDp(16)
|
||||
private val bounds = Rect()
|
||||
private val selection = ArraySet<Long>()
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
|
||||
init {
|
||||
paint.color = context.getThemeColor(com.google.android.material.R.attr.scrimBackground)
|
||||
paint.style = Paint.Style.FILL
|
||||
}
|
||||
|
||||
val checkedItemsCount: Int
|
||||
get() = selection.size
|
||||
|
||||
val checkedItemsIds: Set<Long>
|
||||
get() = selection
|
||||
|
||||
fun toggleItemChecked(id: Long) {
|
||||
if (!selection.remove(id)) {
|
||||
selection.add(id)
|
||||
}
|
||||
}
|
||||
|
||||
fun setItemIsChecked(id: Long, isChecked: Boolean) {
|
||||
if (isChecked) {
|
||||
selection.add(id)
|
||||
} else {
|
||||
selection.remove(id)
|
||||
}
|
||||
}
|
||||
|
||||
fun checkAll(ids: Collection<Long>) {
|
||||
selection.addAll(ids)
|
||||
}
|
||||
|
||||
fun clearSelection() {
|
||||
selection.clear()
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||
icon ?: return
|
||||
canvas.save()
|
||||
if (parent.clipToPadding) {
|
||||
canvas.clipRect(
|
||||
parent.paddingLeft, parent.paddingTop, parent.width - parent.paddingRight,
|
||||
parent.height - parent.paddingBottom
|
||||
)
|
||||
}
|
||||
|
||||
for (child in parent.children) {
|
||||
val itemId = parent.getChildItemId(child)
|
||||
if (itemId in selection) {
|
||||
parent.getDecoratedBoundsWithMargins(child, bounds)
|
||||
bounds.offset(child.translationX.toInt(), child.translationY.toInt())
|
||||
canvas.drawRect(bounds, paint)
|
||||
}
|
||||
}
|
||||
canvas.restore()
|
||||
}
|
||||
|
||||
override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||
icon ?: return
|
||||
canvas.save()
|
||||
val left: Int
|
||||
val right: Int
|
||||
if (parent.clipToPadding) {
|
||||
left = parent.paddingLeft
|
||||
right = parent.width - parent.paddingRight
|
||||
canvas.clipRect(
|
||||
left, parent.paddingTop, right,
|
||||
parent.height - parent.paddingBottom
|
||||
)
|
||||
} else {
|
||||
left = 0
|
||||
right = parent.width
|
||||
}
|
||||
|
||||
for (child in parent.children) {
|
||||
val itemId = parent.getChildItemId(child)
|
||||
if (itemId in selection) {
|
||||
parent.getDecoratedBoundsWithMargins(child, bounds)
|
||||
bounds.offset(child.translationX.toInt(), child.translationY.toInt())
|
||||
val hh = (bounds.height() - icon.intrinsicHeight) / 2
|
||||
val top: Int = bounds.top + hh
|
||||
val bottom: Int = bounds.bottom - hh
|
||||
icon.setBounds(right - icon.intrinsicWidth - padding, top, right - padding, bottom)
|
||||
icon.draw(canvas)
|
||||
}
|
||||
}
|
||||
canvas.restore()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.koitharu.kotatsu.details.ui.model
|
||||
|
||||
import org.koitharu.kotatsu.core.model.MangaChapter
|
||||
import org.koitharu.kotatsu.history.domain.ChapterExtra
|
||||
|
||||
data class ChapterListItem(
|
||||
val chapter: MangaChapter,
|
||||
val extra: ChapterExtra
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.koitharu.kotatsu.details.ui.model
|
||||
|
||||
import org.koitharu.kotatsu.core.model.MangaChapter
|
||||
import org.koitharu.kotatsu.history.domain.ChapterExtra
|
||||
|
||||
fun MangaChapter.toListItem(extra: ChapterExtra) = ChapterListItem(
|
||||
chapter = this,
|
||||
extra = extra
|
||||
)
|
||||
@@ -1,6 +0,0 @@
|
||||
package org.koitharu.kotatsu.domain.history
|
||||
|
||||
enum class ChapterExtra {
|
||||
|
||||
READ, CURRENT, UNREAD, NEW, CHECKED
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.download
|
||||
package org.koitharu.kotatsu.download
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
@@ -12,7 +12,7 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.ui.details.MangaDetailsActivity
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@@ -24,7 +24,8 @@ class DownloadNotification(private val context: Context) {
|
||||
|
||||
init {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
|
||||
&& manager.getNotificationChannel(CHANNEL_ID) == null) {
|
||||
&& manager.getNotificationChannel(CHANNEL_ID) == null
|
||||
) {
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
context.getString(R.string.downloads),
|
||||
@@ -145,7 +146,7 @@ class DownloadNotification(private val context: Context) {
|
||||
private fun createIntent(context: Context, manga: Manga) = PendingIntent.getActivity(
|
||||
context,
|
||||
manga.hashCode(),
|
||||
MangaDetailsActivity.newIntent(context, manga),
|
||||
DetailsActivity.newIntent(context, manga),
|
||||
PendingIntent.FLAG_CANCEL_CURRENT
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.download
|
||||
package org.koitharu.kotatsu.download
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@@ -7,6 +7,7 @@ import android.os.PowerManager
|
||||
import android.webkit.MimeTypeMap
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import kotlinx.coroutines.*
|
||||
@@ -19,13 +20,13 @@ import org.koin.android.ext.android.inject
|
||||
import org.koin.core.context.GlobalContext
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.local.PagesCache
|
||||
import org.koitharu.kotatsu.base.ui.BaseService
|
||||
import org.koitharu.kotatsu.base.ui.dialog.CheckBoxAlertDialog
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.parser.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.domain.local.MangaZip
|
||||
import org.koitharu.kotatsu.ui.base.BaseService
|
||||
import org.koitharu.kotatsu.ui.base.dialog.CheckBoxAlertDialog
|
||||
import org.koitharu.kotatsu.local.data.MangaZip
|
||||
import org.koitharu.kotatsu.local.data.PagesCache
|
||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.utils.CacheUtils
|
||||
import org.koitharu.kotatsu.utils.ext.*
|
||||
import java.io.File
|
||||
@@ -54,8 +55,9 @@ class DownloadService : BaseService() {
|
||||
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "kotatsu:downloading")
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
when (intent?.action) {
|
||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
when (intent.action) {
|
||||
ACTION_DOWNLOAD_START -> {
|
||||
val manga = intent.getParcelableExtra<Manga>(EXTRA_MANGA)
|
||||
val chapters = intent.getLongArrayExtra(EXTRA_CHAPTERS_IDS)?.toArraySet()
|
||||
@@ -77,7 +79,7 @@ class DownloadService : BaseService() {
|
||||
}
|
||||
|
||||
private fun downloadManga(manga: Manga, chaptersIds: Set<Long>?, startId: Int): Job {
|
||||
return serviceScope.launch(Dispatchers.Default) {
|
||||
return lifecycleScope.launch(Dispatchers.Default) {
|
||||
mutex.lock()
|
||||
wakeLock.acquire(TimeUnit.HOURS.toMillis(1))
|
||||
notification.fillFrom(manga)
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.koitharu.kotatsu.favourites
|
||||
|
||||
import org.koin.android.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.FavouritesCategoriesViewModel
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.MangaCategoriesViewModel
|
||||
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListViewModel
|
||||
|
||||
val favouritesModule
|
||||
get() = module {
|
||||
|
||||
single { FavouritesRepository(get()) }
|
||||
|
||||
viewModel { (categoryId: Long) ->
|
||||
FavouritesListViewModel(categoryId, get(), get())
|
||||
}
|
||||
viewModel { FavouritesCategoriesViewModel(get()) }
|
||||
viewModel { (manga: Manga) ->
|
||||
MangaCategoriesViewModel(manga, get())
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
package org.koitharu.kotatsu.core.db.dao
|
||||
package org.koitharu.kotatsu.favourites.data
|
||||
|
||||
import androidx.room.*
|
||||
import org.koitharu.kotatsu.core.db.entity.FavouriteCategoryEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
|
||||
@Dao
|
||||
abstract class FavouriteCategoriesDao {
|
||||
@@ -9,6 +10,9 @@ abstract class FavouriteCategoriesDao {
|
||||
@Query("SELECT * FROM favourite_categories ORDER BY sort_key")
|
||||
abstract suspend fun findAll(): List<FavouriteCategoryEntity>
|
||||
|
||||
@Query("SELECT * FROM favourite_categories ORDER BY sort_key")
|
||||
abstract fun observeAll(): Flow<List<FavouriteCategoryEntity>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.ABORT)
|
||||
abstract suspend fun insert(category: FavouriteCategoryEntity): Long
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.core.db.entity
|
||||
package org.koitharu.kotatsu.favourites.data
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
@@ -1,8 +1,9 @@
|
||||
package org.koitharu.kotatsu.core.db.entity
|
||||
package org.koitharu.kotatsu.favourites.data
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
|
||||
@Entity(
|
||||
tableName = "favourites", primaryKeys = ["manga_id", "category_id"], foreignKeys = [
|
||||
@@ -1,8 +1,11 @@
|
||||
package org.koitharu.kotatsu.core.db.entity
|
||||
package org.koitharu.kotatsu.favourites.data
|
||||
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Junction
|
||||
import androidx.room.Relation
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||
|
||||
data class FavouriteManga(
|
||||
@Embedded val favourite: FavouriteEntity,
|
||||
@@ -1,8 +1,7 @@
|
||||
package org.koitharu.kotatsu.core.db.dao
|
||||
package org.koitharu.kotatsu.favourites.data
|
||||
|
||||
import androidx.room.*
|
||||
import org.koitharu.kotatsu.core.db.entity.FavouriteEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.FavouriteManga
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
|
||||
@Dao
|
||||
@@ -12,6 +11,10 @@ abstract class FavouritesDao {
|
||||
@Query("SELECT * FROM favourites GROUP BY manga_id ORDER BY created_at")
|
||||
abstract suspend fun findAll(): List<FavouriteManga>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM favourites GROUP BY manga_id ORDER BY created_at")
|
||||
abstract fun observeAll(): Flow<List<FavouriteManga>>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM favourites GROUP BY manga_id ORDER BY created_at LIMIT :limit OFFSET :offset")
|
||||
abstract suspend fun findAll(offset: Int, limit: Int): List<FavouriteManga>
|
||||
@@ -20,6 +23,10 @@ abstract class FavouritesDao {
|
||||
@Query("SELECT * FROM favourites WHERE category_id = :categoryId GROUP BY manga_id ORDER BY created_at")
|
||||
abstract suspend fun findAll(categoryId: Long): List<FavouriteManga>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM favourites WHERE category_id = :categoryId GROUP BY manga_id ORDER BY created_at")
|
||||
abstract fun observeAll(categoryId: Long): Flow<List<FavouriteManga>>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM favourites WHERE category_id = :categoryId GROUP BY manga_id ORDER BY created_at LIMIT :limit OFFSET :offset")
|
||||
abstract suspend fun findAll(categoryId: Long, offset: Int, limit: Int): List<FavouriteManga>
|
||||
@@ -31,6 +38,13 @@ abstract class FavouritesDao {
|
||||
@Query("SELECT * FROM favourites WHERE manga_id = :id GROUP BY manga_id")
|
||||
abstract suspend fun find(id: Long): FavouriteManga?
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM favourites WHERE manga_id = :id GROUP BY manga_id")
|
||||
abstract fun observe(id: Long): Flow<FavouriteManga?>
|
||||
|
||||
@Query("SELECT DISTINCT category_id FROM favourites WHERE manga_id = :id")
|
||||
abstract fun observeIds(id: Long): Flow<List<Long>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
abstract suspend fun insert(favourite: FavouriteEntity)
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
package org.koitharu.kotatsu.domain.favourites
|
||||
package org.koitharu.kotatsu.favourites.domain
|
||||
|
||||
import androidx.collection.ArraySet
|
||||
import androidx.room.withTransaction
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.db.entity.FavouriteCategoryEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.FavouriteEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
|
||||
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
|
||||
import org.koitharu.kotatsu.utils.ext.mapItems
|
||||
import org.koitharu.kotatsu.utils.ext.mapToSet
|
||||
|
||||
class FavouritesRepository(private val db: MangaDatabase) {
|
||||
@@ -20,6 +23,11 @@ class FavouritesRepository(private val db: MangaDatabase) {
|
||||
return entities.map { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) }
|
||||
}
|
||||
|
||||
fun observeAll(): Flow<List<Manga>> {
|
||||
return db.favouritesDao.observeAll()
|
||||
.mapItems { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) }
|
||||
}
|
||||
|
||||
suspend fun getAllManga(offset: Int): List<Manga> {
|
||||
val entities = db.favouritesDao.findAll(offset, 20)
|
||||
return entities.map { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) }
|
||||
@@ -30,6 +38,11 @@ class FavouritesRepository(private val db: MangaDatabase) {
|
||||
return entities.map { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) }
|
||||
}
|
||||
|
||||
fun observeAll(categoryId: Long): Flow<List<Manga>> {
|
||||
return db.favouritesDao.observeAll(categoryId)
|
||||
.mapItems { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) }
|
||||
}
|
||||
|
||||
suspend fun getManga(categoryId: Long, offset: Int): List<Manga> {
|
||||
val entities = db.favouritesDao.findAll(categoryId, offset, 20)
|
||||
return entities.map { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) }
|
||||
@@ -45,6 +58,22 @@ class FavouritesRepository(private val db: MangaDatabase) {
|
||||
return entities?.map { it.toFavouriteCategory() }.orEmpty()
|
||||
}
|
||||
|
||||
fun observeCategories(): Flow<List<FavouriteCategory>> {
|
||||
return db.favouriteCategoriesDao.observeAll().mapItems {
|
||||
it.toFavouriteCategory()
|
||||
}
|
||||
}
|
||||
|
||||
fun observeCategories(mangaId: Long): Flow<List<FavouriteCategory>> {
|
||||
return db.favouritesDao.observe(mangaId).map { entity ->
|
||||
entity?.categories?.map { it.toFavouriteCategory() }.orEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
fun observeCategoriesIds(mangaId: Long): Flow<List<Long>> {
|
||||
return db.favouritesDao.observeIds(mangaId)
|
||||
}
|
||||
|
||||
suspend fun addCategory(title: String): FavouriteCategory {
|
||||
val entity = FavouriteCategoryEntity(
|
||||
title = title,
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.koitharu.kotatsu.domain.favourites
|
||||
package org.koitharu.kotatsu.favourites.domain
|
||||
|
||||
@Deprecated("Use flow")
|
||||
fun interface OnFavouritesChangeListener {
|
||||
|
||||
fun onFavouritesChanged(mangaId: Long)
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.list.favourites
|
||||
package org.koitharu.kotatsu.favourites.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
@@ -8,25 +8,22 @@ import android.view.View
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import kotlinx.android.synthetic.main.fragment_favourites.*
|
||||
import moxy.ktx.moxyPresenter
|
||||
import org.koin.android.viewmodel.ext.android.viewModel
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
import org.koitharu.kotatsu.domain.favourites.FavouritesRepository
|
||||
import org.koitharu.kotatsu.domain.favourites.OnFavouritesChangeListener
|
||||
import org.koitharu.kotatsu.ui.base.BaseFragment
|
||||
import org.koitharu.kotatsu.ui.list.favourites.categories.CategoriesActivity
|
||||
import org.koitharu.kotatsu.ui.list.favourites.categories.CategoriesEditDelegate
|
||||
import org.koitharu.kotatsu.ui.list.favourites.categories.FavouriteCategoriesPresenter
|
||||
import org.koitharu.kotatsu.ui.list.favourites.categories.FavouriteCategoriesView
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.CategoriesEditDelegate
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.FavouritesCategoriesViewModel
|
||||
import org.koitharu.kotatsu.utils.ext.showPopupMenu
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
class FavouritesContainerFragment : BaseFragment(R.layout.fragment_favourites),
|
||||
FavouriteCategoriesView, OnFavouritesChangeListener, FavouritesTabLongClickListener,
|
||||
CategoriesEditDelegate.CategoriesEditCallback {
|
||||
FavouritesTabLongClickListener, CategoriesEditDelegate.CategoriesEditCallback {
|
||||
|
||||
private val viewModel by viewModel<FavouritesCategoriesViewModel>()
|
||||
|
||||
private val presenter by moxyPresenter(factory = ::FavouriteCategoriesPresenter)
|
||||
private val editDelegate by lazy(LazyThreadSafetyMode.NONE) {
|
||||
CategoriesEditDelegate(requireContext(), this)
|
||||
}
|
||||
@@ -41,15 +38,12 @@ class FavouritesContainerFragment : BaseFragment(R.layout.fragment_favourites),
|
||||
val adapter = FavouritesPagerAdapter(this, this)
|
||||
pager.adapter = adapter
|
||||
TabLayoutMediator(tabs, pager, adapter).attach()
|
||||
FavouritesRepository.subscribe(this)
|
||||
|
||||
viewModel.categories.observe(viewLifecycleOwner, ::onCategoriesChanged)
|
||||
viewModel.onError.observe(viewLifecycleOwner, ::onError)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
FavouritesRepository.unsubscribe(this)
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onCategoriesChanged(categories: List<FavouriteCategory>) {
|
||||
private fun onCategoriesChanged(categories: List<FavouriteCategory>) {
|
||||
val data = ArrayList<FavouriteCategory>(categories.size + 1)
|
||||
data += FavouriteCategory(0L, getString(R.string.all_favourites), -1, Date())
|
||||
data += categories
|
||||
@@ -72,21 +66,13 @@ class FavouritesContainerFragment : BaseFragment(R.layout.fragment_favourites),
|
||||
}
|
||||
|
||||
override fun getTitle(): CharSequence? {
|
||||
return getString(R.string.favourites)
|
||||
return context?.getString(R.string.favourites)
|
||||
}
|
||||
|
||||
override fun onCheckedCategoriesChanged(checkedIds: Set<Int>) = Unit
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
private fun onError(e: Throwable) {
|
||||
Snackbar.make(pager, e.message ?: return, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
override fun onFavouritesChanged(mangaId: Long) = Unit
|
||||
|
||||
override fun onCategoriesChanged() {
|
||||
presenter.loadAllCategories()
|
||||
}
|
||||
|
||||
override fun onTabLongClick(tabView: View, category: FavouriteCategory): Boolean {
|
||||
val menuRes = if (category.id == 0L) R.menu.popup_category_empty else R.menu.popup_category
|
||||
tabView.showPopupMenu(menuRes) {
|
||||
@@ -101,15 +87,15 @@ class FavouritesContainerFragment : BaseFragment(R.layout.fragment_favourites),
|
||||
}
|
||||
|
||||
override fun onDeleteCategory(category: FavouriteCategory) {
|
||||
presenter.deleteCategory(category.id)
|
||||
viewModel.deleteCategory(category.id)
|
||||
}
|
||||
|
||||
override fun onRenameCategory(category: FavouriteCategory, newName: String) {
|
||||
presenter.renameCategory(category.id, newName)
|
||||
viewModel.renameCategory(category.id, newName)
|
||||
}
|
||||
|
||||
override fun onCreateCategory(name: String) {
|
||||
presenter.createCategory(name)
|
||||
viewModel.createCategory(name)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -1,12 +1,13 @@
|
||||
package org.koitharu.kotatsu.ui.list.favourites
|
||||
package org.koitharu.kotatsu.favourites.ui
|
||||
|
||||
import android.view.View
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import org.koitharu.kotatsu.base.ui.list.AdapterUpdater
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
import org.koitharu.kotatsu.ui.base.list.AdapterUpdater
|
||||
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment
|
||||
import org.koitharu.kotatsu.utils.ext.replaceWith
|
||||
|
||||
class FavouritesPagerAdapter(
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.list.favourites
|
||||
package org.koitharu.kotatsu.favourites.ui
|
||||
|
||||
import android.view.View
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.list.favourites.categories
|
||||
package org.koitharu.kotatsu.favourites.ui.categories
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@@ -12,18 +12,18 @@ import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.android.synthetic.main.activity_categories.*
|
||||
import moxy.ktx.moxyPresenter
|
||||
import org.koin.android.viewmodel.ext.android.viewModel
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
import org.koitharu.kotatsu.ui.base.BaseActivity
|
||||
import org.koitharu.kotatsu.ui.base.list.OnRecyclerItemClickListener
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.utils.ext.showPopupMenu
|
||||
|
||||
class CategoriesActivity : BaseActivity(), OnRecyclerItemClickListener<FavouriteCategory>,
|
||||
FavouriteCategoriesView, View.OnClickListener, CategoriesEditDelegate.CategoriesEditCallback {
|
||||
class CategoriesActivity : BaseActivity(), OnListItemClickListener<FavouriteCategory>,
|
||||
View.OnClickListener, CategoriesEditDelegate.CategoriesEditCallback {
|
||||
|
||||
private val presenter by moxyPresenter(factory = ::FavouriteCategoriesPresenter)
|
||||
private val viewModel by viewModel<FavouritesCategoriesViewModel>()
|
||||
|
||||
private lateinit var adapter: CategoriesAdapter
|
||||
private lateinit var reorderHelper: ItemTouchHelper
|
||||
@@ -41,6 +41,9 @@ class CategoriesActivity : BaseActivity(), OnRecyclerItemClickListener<Favourite
|
||||
fab_add.setOnClickListener(this)
|
||||
reorderHelper = ItemTouchHelper(ReorderHelperCallback())
|
||||
reorderHelper.attachToRecyclerView(recyclerView)
|
||||
|
||||
viewModel.categories.observe(this, ::onCategoriesChanged)
|
||||
viewModel.onError.observe(this, ::onError)
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
@@ -49,7 +52,7 @@ class CategoriesActivity : BaseActivity(), OnRecyclerItemClickListener<Favourite
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemClick(item: FavouriteCategory, position: Int, view: View) {
|
||||
override fun onItemClick(item: FavouriteCategory, view: View) {
|
||||
view.showPopupMenu(R.menu.popup_category) {
|
||||
when (it.itemId) {
|
||||
R.id.action_remove -> editDelegate.deleteCategory(item)
|
||||
@@ -59,35 +62,33 @@ class CategoriesActivity : BaseActivity(), OnRecyclerItemClickListener<Favourite
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemLongClick(item: FavouriteCategory, position: Int, view: View): Boolean {
|
||||
override fun onItemLongClick(item: FavouriteCategory, view: View): Boolean {
|
||||
reorderHelper.startDrag(
|
||||
recyclerView.findViewHolderForAdapterPosition(position) ?: return false
|
||||
recyclerView.findContainingViewHolder(view) ?: return false
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onCategoriesChanged(categories: List<FavouriteCategory>) {
|
||||
adapter.replaceData(categories)
|
||||
private fun onCategoriesChanged(categories: List<FavouriteCategory>) {
|
||||
adapter.items = categories
|
||||
textView_holder.isVisible = categories.isEmpty()
|
||||
}
|
||||
|
||||
override fun onCheckedCategoriesChanged(checkedIds: Set<Int>) = Unit
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
private fun onError(e: Throwable) {
|
||||
Snackbar.make(recyclerView, e.getDisplayMessage(resources), Snackbar.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onDeleteCategory(category: FavouriteCategory) {
|
||||
presenter.deleteCategory(category.id)
|
||||
viewModel.deleteCategory(category.id)
|
||||
}
|
||||
|
||||
override fun onRenameCategory(category: FavouriteCategory, newName: String) {
|
||||
presenter.renameCategory(category.id, newName)
|
||||
viewModel.renameCategory(category.id, newName)
|
||||
}
|
||||
|
||||
override fun onCreateCategory(name: String) {
|
||||
presenter.createCategory(name)
|
||||
viewModel.createCategory(name)
|
||||
}
|
||||
|
||||
private inner class ReorderHelperCallback : ItemTouchHelper.SimpleCallback(
|
||||
@@ -101,8 +102,7 @@ class CategoriesActivity : BaseActivity(), OnRecyclerItemClickListener<Favourite
|
||||
): Boolean {
|
||||
val oldPos = viewHolder.bindingAdapterPosition
|
||||
val newPos = target.bindingAdapterPosition
|
||||
adapter.moveItem(oldPos, newPos)
|
||||
presenter.storeCategoriesOrder(adapter.items.map { it.id })
|
||||
viewModel.reorderCategories(oldPos, newPos)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.koitharu.kotatsu.favourites.ui.categories
|
||||
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
import kotlin.jvm.internal.Intrinsics
|
||||
|
||||
class CategoriesAdapter(
|
||||
onItemClickListener: OnListItemClickListener<FavouriteCategory>
|
||||
) : AsyncListDifferDelegationAdapter<FavouriteCategory>(DiffCallback()) {
|
||||
|
||||
init {
|
||||
delegatesManager.addDelegate(categoryAD(onItemClickListener))
|
||||
}
|
||||
|
||||
private class DiffCallback : DiffUtil.ItemCallback<FavouriteCategory>() {
|
||||
|
||||
override fun areItemsTheSame(oldItem: FavouriteCategory, newItem: FavouriteCategory): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: FavouriteCategory, newItem: FavouriteCategory): Boolean {
|
||||
return Intrinsics.areEqual(oldItem, newItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package org.koitharu.kotatsu.ui.list.favourites.categories
|
||||
package org.koitharu.kotatsu.favourites.ui.categories
|
||||
|
||||
import android.content.Context
|
||||
import android.text.InputType
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.dialog.TextInputDialog
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
import org.koitharu.kotatsu.ui.base.dialog.TextInputDialog
|
||||
|
||||
class CategoriesEditDelegate(
|
||||
private val context: Context,
|
||||
@@ -13,7 +13,7 @@ class CategoriesEditDelegate(
|
||||
) {
|
||||
|
||||
fun deleteCategory(category: FavouriteCategory) {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
AlertDialog.Builder(context)
|
||||
.setMessage(context.getString(R.string.category_delete_confirm, category.title))
|
||||
.setTitle(R.string.remove_category)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.koitharu.kotatsu.favourites.ui.categories
|
||||
|
||||
import android.view.MotionEvent
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateLayoutContainer
|
||||
import kotlinx.android.synthetic.main.item_category.*
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
|
||||
fun categoryAD(
|
||||
clickListener: OnListItemClickListener<FavouriteCategory>
|
||||
) = adapterDelegateLayoutContainer<FavouriteCategory, FavouriteCategory>(R.layout.item_category) {
|
||||
|
||||
imageView_more.setOnClickListener {
|
||||
clickListener.onItemClick(item, it)
|
||||
}
|
||||
@Suppress("ClickableViewAccessibility")
|
||||
imageView_handle.setOnTouchListener { v, event ->
|
||||
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||
clickListener.onItemLongClick(item, itemView)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
bind {
|
||||
textView_title.text = item.title
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.koitharu.kotatsu.favourites.ui.categories
|
||||
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||
|
||||
class FavouritesCategoriesViewModel(
|
||||
private val repository: FavouritesRepository
|
||||
) : BaseViewModel() {
|
||||
|
||||
private var reorderJob: Job? = null
|
||||
|
||||
val categories = repository.observeCategories()
|
||||
.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
|
||||
fun createCategory(name: String) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
repository.addCategory(name)
|
||||
}
|
||||
}
|
||||
|
||||
fun renameCategory(id: Long, name: String) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
repository.renameCategory(id, name)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteCategory(id: Long) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
repository.removeCategory(id)
|
||||
}
|
||||
}
|
||||
|
||||
fun reorderCategories(oldPos: Int, newPos: Int) {
|
||||
val prevJob = reorderJob
|
||||
reorderJob = launchJob(Dispatchers.Default) {
|
||||
prevJob?.join()
|
||||
val items = categories.value ?: error("This should not happen")
|
||||
val ids = items.mapTo(ArrayList(items.size)) { it.id }
|
||||
val item = ids.removeAt(oldPos)
|
||||
ids.add(newPos, item)
|
||||
repository.reorderCategories(ids)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package org.koitharu.kotatsu.favourites.ui.categories.select
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import kotlinx.android.synthetic.main.dialog_favorite_categories.*
|
||||
import org.koin.android.viewmodel.ext.android.viewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.domain.MangaIntent
|
||||
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.CategoriesEditDelegate
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.adapter.MangaCategoriesAdapter
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
|
||||
class FavouriteCategoriesDialog : BaseBottomSheet(R.layout.dialog_favorite_categories),
|
||||
OnListItemClickListener<MangaCategoryItem>, CategoriesEditDelegate.CategoriesEditCallback,
|
||||
View.OnClickListener {
|
||||
|
||||
private val viewModel by viewModel<MangaCategoriesViewModel> {
|
||||
parametersOf(requireNotNull(arguments?.getParcelable<Manga>(MangaIntent.KEY_MANGA)))
|
||||
}
|
||||
|
||||
private var adapter: MangaCategoriesAdapter? = null
|
||||
private val editDelegate by lazy(LazyThreadSafetyMode.NONE) {
|
||||
CategoriesEditDelegate(requireContext(), this@FavouriteCategoriesDialog)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
adapter = MangaCategoriesAdapter(this)
|
||||
recyclerView_categories.adapter = adapter
|
||||
textView_add.setOnClickListener(this)
|
||||
|
||||
viewModel.content.observe(viewLifecycleOwner, this::onContentChanged)
|
||||
viewModel.onError.observe(viewLifecycleOwner, ::onError)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
adapter = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.textView_add -> editDelegate.createCategory()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemClick(item: MangaCategoryItem, view: View) {
|
||||
viewModel.setChecked(item.id, !item.isChecked)
|
||||
}
|
||||
|
||||
override fun onDeleteCategory(category: FavouriteCategory) = Unit
|
||||
|
||||
override fun onRenameCategory(category: FavouriteCategory, newName: String) = Unit
|
||||
|
||||
override fun onCreateCategory(name: String) {
|
||||
viewModel.createCategory(name)
|
||||
}
|
||||
|
||||
private fun onContentChanged(categories: List<MangaCategoryItem>) {
|
||||
adapter?.items = categories
|
||||
}
|
||||
|
||||
private fun onError(e: Throwable) {
|
||||
Toast.makeText(context ?: return, e.getDisplayMessage(resources), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "FavouriteCategoriesDialog"
|
||||
|
||||
fun show(fm: FragmentManager, manga: Manga) = FavouriteCategoriesDialog()
|
||||
.withArgs(1) {
|
||||
putParcelable(MangaIntent.KEY_MANGA, manga)
|
||||
}.show(fm, TAG)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.koitharu.kotatsu.favourites.ui.categories.select
|
||||
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
|
||||
|
||||
class MangaCategoriesViewModel(
|
||||
private val manga: Manga,
|
||||
private val favouritesRepository: FavouritesRepository
|
||||
) : BaseViewModel() {
|
||||
|
||||
val content = combine(
|
||||
favouritesRepository.observeCategories(),
|
||||
favouritesRepository.observeCategoriesIds(manga.id)
|
||||
) { all, checked ->
|
||||
all.map {
|
||||
MangaCategoryItem(
|
||||
id = it.id,
|
||||
name = it.title,
|
||||
isChecked = it.id in checked
|
||||
)
|
||||
}
|
||||
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
|
||||
fun setChecked(categoryId: Long, isChecked: Boolean) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
if (isChecked) {
|
||||
favouritesRepository.addToCategory(manga, categoryId)
|
||||
} else {
|
||||
favouritesRepository.removeFromCategory(manga, categoryId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createCategory(name: String) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
favouritesRepository.addCategory(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user