Scrobblers config activity
@@ -85,16 +85,7 @@
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.settings.SettingsActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/settings">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="kotatsu" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
android:label="@string/settings" />
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.browser.BrowserActivity"
|
||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
||||
@@ -140,6 +131,22 @@
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.shelf.ui.config.ShelfSettingsActivity"
|
||||
android:label="@string/settings" />
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.scrobbling.common.ui.config.ScrobblerConfigActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/settings"
|
||||
android:launchMode="singleTop">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="kotatsu" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name="org.koitharu.kotatsu.download.ui.service.DownloadService"
|
||||
|
||||
@@ -4,11 +4,6 @@ import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.util.Size
|
||||
import androidx.room.withTransaction
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.util.zip.ZipFile
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.roundToInt
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
@@ -17,7 +12,11 @@ import kotlinx.coroutines.runInterruptible
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.db.entity.*
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.toEntities
|
||||
import org.koitharu.kotatsu.core.db.entity.toEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.toManga
|
||||
import org.koitharu.kotatsu.core.db.entity.toMangaTags
|
||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||
@@ -27,6 +26,11 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.util.await
|
||||
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.util.zip.ZipFile
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private const val MIN_WEBTOON_RATIO = 2
|
||||
|
||||
@@ -121,7 +125,7 @@ class MangaDataRepository @Inject constructor(
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.get()
|
||||
.header(CommonHeaders.REFERER, page.referer)
|
||||
.tag(MangaSource::class.java, page.source)
|
||||
.cacheControl(CommonHeaders.CACHE_CONTROL_DISABLED)
|
||||
.build()
|
||||
okHttpClient.newCall(request).await().use {
|
||||
|
||||
@@ -19,15 +19,28 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||
import org.koitharu.kotatsu.core.db.migrations.*
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration10To11
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration11To12
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration12To13
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration13To14
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration14To15
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration1To2
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration2To3
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration3To4
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration4To5
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration5To6
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration6To7
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration7To8
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration8To9
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration9To10
|
||||
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
|
||||
import org.koitharu.kotatsu.scrobbling.data.ScrobblingDao
|
||||
import org.koitharu.kotatsu.scrobbling.data.ScrobblingEntity
|
||||
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblingDao
|
||||
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblingEntity
|
||||
import org.koitharu.kotatsu.suggestions.data.SuggestionDao
|
||||
import org.koitharu.kotatsu.suggestions.data.SuggestionEntity
|
||||
import org.koitharu.kotatsu.tracker.data.TrackEntity
|
||||
|
||||
@@ -5,7 +5,7 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.utils.ext.daysDiff
|
||||
import org.koitharu.kotatsu.utils.ext.format
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
|
||||
sealed class DateTimeAgo : ListModel {
|
||||
|
||||
@@ -17,6 +17,8 @@ sealed class DateTimeAgo : ListModel {
|
||||
}
|
||||
|
||||
override fun toString() = "just_now"
|
||||
|
||||
override fun equals(other: Any?): Boolean = other === JustNow
|
||||
}
|
||||
|
||||
class MinutesAgo(val minutes: Int) : DateTimeAgo() {
|
||||
@@ -60,6 +62,8 @@ sealed class DateTimeAgo : ListModel {
|
||||
}
|
||||
|
||||
override fun toString() = "today"
|
||||
|
||||
override fun equals(other: Any?): Boolean = other === Today
|
||||
}
|
||||
|
||||
object Yesterday : DateTimeAgo() {
|
||||
@@ -68,6 +72,8 @@ sealed class DateTimeAgo : ListModel {
|
||||
}
|
||||
|
||||
override fun toString() = "yesterday"
|
||||
|
||||
override fun equals(other: Any?): Boolean = other === Yesterday
|
||||
}
|
||||
|
||||
class DaysAgo(val days: Int) : DateTimeAgo() {
|
||||
@@ -119,5 +125,7 @@ sealed class DateTimeAgo : ListModel {
|
||||
}
|
||||
|
||||
override fun toString() = "long_ago"
|
||||
|
||||
override fun equals(other: Any?): Boolean = other === LongAgo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ import org.koitharu.kotatsu.parsers.model.MangaState
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingInfo
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||
import org.koitharu.kotatsu.search.ui.SearchActivity
|
||||
import org.koitharu.kotatsu.utils.FileSize
|
||||
|
||||
@@ -21,7 +21,7 @@ import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesB
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
|
||||
import org.koitharu.kotatsu.scrobbling.ui.selector.ScrobblingSelectorBottomSheet
|
||||
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorBottomSheet
|
||||
import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity
|
||||
import org.koitharu.kotatsu.utils.ShareHelper
|
||||
|
||||
|
||||
@@ -45,9 +45,9 @@ import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
import org.koitharu.kotatsu.scrobbling.domain.Scrobbler
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingInfo
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingStatus
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.asFlowLiveData
|
||||
|
||||
@@ -6,7 +6,7 @@ import coil.ImageLoader
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.databinding.ItemScrobblingInfoBinding
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingInfo
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
||||
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
|
||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.utils.ext.newImageRequest
|
||||
|
||||
@@ -21,9 +21,9 @@ import org.koitharu.kotatsu.base.ui.BaseBottomSheet
|
||||
import org.koitharu.kotatsu.databinding.SheetScrobblingBinding
|
||||
import org.koitharu.kotatsu.details.ui.DetailsViewModel
|
||||
import org.koitharu.kotatsu.image.ui.ImageActivity
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingInfo
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingStatus
|
||||
import org.koitharu.kotatsu.scrobbling.ui.selector.ScrobblingSelectorBottomSheet
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
|
||||
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorBottomSheet
|
||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.utils.ext.newImageRequest
|
||||
|
||||
@@ -5,7 +5,7 @@ import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import coil.ImageLoader
|
||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingInfo
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
||||
|
||||
class ScrollingInfoAdapter(
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
@@ -27,7 +27,7 @@ class ScrollingInfoAdapter(
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
override fun getChangePayload(oldItem: ScrobblingInfo, newItem: ScrobblingInfo): Any? {
|
||||
override fun getChangePayload(oldItem: ScrobblingInfo, newItem: ScrobblingInfo): Any {
|
||||
return Unit
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,5 +84,8 @@ sealed interface ExploreItem : ListModel {
|
||||
@StringRes actionStringRes: Int,
|
||||
) : EmptyState(icon, textPrimary, textSecondary, actionStringRes), ExploreItem
|
||||
|
||||
object Loading : ExploreItem
|
||||
object Loading : ExploreItem {
|
||||
|
||||
override fun equals(other: Any?): Boolean = other === Loading
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ import org.koitharu.kotatsu.history.data.HistoryEntity
|
||||
import org.koitharu.kotatsu.history.data.toMangaHistory
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.scrobbling.domain.Scrobbler
|
||||
import org.koitharu.kotatsu.scrobbling.domain.tryScrobble
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.tryScrobble
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import org.koitharu.kotatsu.utils.ext.mapItems
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -12,17 +12,22 @@ import org.koitharu.kotatsu.utils.ext.setTextAndVisible
|
||||
|
||||
fun emptyStateListAD(
|
||||
coil: ImageLoader,
|
||||
listener: ListStateHolderListener,
|
||||
listener: ListStateHolderListener?,
|
||||
) = adapterDelegateViewBinding<EmptyState, ListModel, ItemEmptyStateBinding>(
|
||||
{ inflater, parent -> ItemEmptyStateBinding.inflate(inflater, parent, false) },
|
||||
) {
|
||||
binding.buttonRetry.setOnClickListener { listener.onEmptyActionClick() }
|
||||
|
||||
if (listener != null) {
|
||||
binding.buttonRetry.setOnClickListener { listener.onEmptyActionClick() }
|
||||
}
|
||||
|
||||
bind {
|
||||
binding.icon.newImageRequest(item.icon)?.enqueueWith(coil)
|
||||
binding.textPrimary.setText(item.textPrimary)
|
||||
binding.textSecondary.setTextAndVisible(item.textSecondary)
|
||||
binding.buttonRetry.setTextAndVisible(item.actionStringRes)
|
||||
if (listener != null) {
|
||||
binding.buttonRetry.setTextAndVisible(item.actionStringRes)
|
||||
}
|
||||
}
|
||||
|
||||
onViewRecycled {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
package org.koitharu.kotatsu.list.ui.model
|
||||
|
||||
interface ListModel
|
||||
interface ListModel {
|
||||
|
||||
override fun equals(other: Any?): Boolean
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
package org.koitharu.kotatsu.list.ui.model
|
||||
|
||||
object LoadingFooter : ListModel
|
||||
object LoadingFooter : ListModel {
|
||||
|
||||
override fun equals(other: Any?): Boolean = other === LoadingFooter
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
package org.koitharu.kotatsu.list.ui.model
|
||||
|
||||
object LoadingState : ListModel
|
||||
object LoadingState : ListModel {
|
||||
|
||||
override fun equals(other: Any?): Boolean = other === LoadingState
|
||||
}
|
||||
|
||||
@@ -15,14 +15,14 @@ import org.koitharu.kotatsu.scrobbling.anilist.data.AniListAuthenticator
|
||||
import org.koitharu.kotatsu.scrobbling.anilist.data.AniListInterceptor
|
||||
import org.koitharu.kotatsu.scrobbling.anilist.data.AniListRepository
|
||||
import org.koitharu.kotatsu.scrobbling.anilist.domain.AniListScrobbler
|
||||
import org.koitharu.kotatsu.scrobbling.data.ScrobblerStorage
|
||||
import org.koitharu.kotatsu.scrobbling.domain.Scrobbler
|
||||
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerType
|
||||
import org.koitharu.kotatsu.scrobbling.mal.data.MALAuthenticator
|
||||
import org.koitharu.kotatsu.scrobbling.mal.data.MALInterceptor
|
||||
import org.koitharu.kotatsu.scrobbling.mal.data.MALRepository
|
||||
import org.koitharu.kotatsu.scrobbling.mal.domain.MALScrobbler
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerType
|
||||
import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriAuthenticator
|
||||
import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriInterceptor
|
||||
import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriRepository
|
||||
|
||||
@@ -7,9 +7,9 @@ import okhttp3.Response
|
||||
import okhttp3.Route
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||
import org.koitharu.kotatsu.scrobbling.data.ScrobblerStorage
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerType
|
||||
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerType
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.scrobbling.anilist.data
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||
import org.koitharu.kotatsu.scrobbling.data.ScrobblerStorage
|
||||
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage
|
||||
|
||||
private const val JSON = "application/json"
|
||||
|
||||
|
||||
@@ -15,13 +15,12 @@ import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
|
||||
import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
||||
import org.koitharu.kotatsu.parsers.util.parseJson
|
||||
import org.koitharu.kotatsu.parsers.util.toIntUp
|
||||
import org.koitharu.kotatsu.scrobbling.data.ScrobblerRepository
|
||||
import org.koitharu.kotatsu.scrobbling.data.ScrobblerStorage
|
||||
import org.koitharu.kotatsu.scrobbling.data.ScrobblingEntity
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerMangaInfo
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
||||
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage
|
||||
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblingEntity
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerMangaInfo
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private const val REDIRECT_URI = "kotatsu://anilist-auth"
|
||||
@@ -36,7 +35,7 @@ class AniListRepository(
|
||||
private val okHttp: OkHttpClient,
|
||||
private val storage: ScrobblerStorage,
|
||||
private val db: MangaDatabase,
|
||||
) : ScrobblerRepository {
|
||||
) : org.koitharu.kotatsu.scrobbling.common.data.ScrobblerRepository {
|
||||
|
||||
override val oauthUrl: String
|
||||
get() = "${BASE_URL}oauth/authorize?client_id=${BuildConfig.ANILIST_CLIENT_ID}&" +
|
||||
|
||||
@@ -2,9 +2,9 @@ package org.koitharu.kotatsu.scrobbling.anilist.domain
|
||||
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.scrobbling.anilist.data.AniListRepository
|
||||
import org.koitharu.kotatsu.scrobbling.domain.Scrobbler
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingStatus
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
package org.koitharu.kotatsu.scrobbling.anilist.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.preference.Preference
|
||||
import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import coil.transform.CircleCropTransformation
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
||||
import org.koitharu.kotatsu.utils.PreferenceIconTarget
|
||||
import org.koitharu.kotatsu.utils.ext.assistedViewModels
|
||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AniListSettingsFragment : BasePreferenceFragment(R.string.anilist) {
|
||||
|
||||
@Inject
|
||||
lateinit var coil: ImageLoader
|
||||
|
||||
@Inject
|
||||
lateinit var viewModelFactory: AniListSettingsViewModel.Factory
|
||||
|
||||
private val viewModel by assistedViewModels {
|
||||
viewModelFactory.create(arguments?.getString(ARG_AUTH_CODE))
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_anilist)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
viewModel.user.observe(viewLifecycleOwner, this::onUserChanged)
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
return when (preference.key) {
|
||||
KEY_USER -> openAuthorization()
|
||||
KEY_LOGOUT -> {
|
||||
viewModel.logout()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onUserChanged(user: ScrobblerUser?) {
|
||||
val pref = findPreference<Preference>(KEY_USER) ?: return
|
||||
pref.isSelectable = user == null
|
||||
pref.title = user?.nickname ?: getString(R.string.sign_in)
|
||||
ImageRequest.Builder(requireContext())
|
||||
.data(user?.avatar)
|
||||
.transformations(CircleCropTransformation())
|
||||
.target(PreferenceIconTarget(pref))
|
||||
.enqueueWith(coil)
|
||||
findPreference<Preference>(KEY_LOGOUT)?.isVisible = user != null
|
||||
}
|
||||
|
||||
private fun openAuthorization(): Boolean {
|
||||
return runCatching {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.data = Uri.parse(viewModel.authorizationUrl)
|
||||
startActivity(intent)
|
||||
}.isSuccess
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_USER = "al_user"
|
||||
private const val KEY_LOGOUT = "al_logout"
|
||||
|
||||
private const val ARG_AUTH_CODE = "auth_code"
|
||||
|
||||
fun newInstance(authCode: String?) = AniListSettingsFragment().withArgs(1) {
|
||||
putString(ARG_AUTH_CODE, authCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package org.koitharu.kotatsu.scrobbling.anilist.ui
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.scrobbling.anilist.data.AniListRepository
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
||||
|
||||
class AniListSettingsViewModel @AssistedInject constructor(
|
||||
private val repository: AniListRepository,
|
||||
@Assisted authCode: String?,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val authorizationUrl: String
|
||||
get() = repository.oauthUrl
|
||||
|
||||
val user = MutableLiveData<ScrobblerUser?>()
|
||||
|
||||
init {
|
||||
if (authCode != null) {
|
||||
authorize(authCode)
|
||||
} else {
|
||||
loadUser()
|
||||
}
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
launchJob(Dispatchers.Default) {
|
||||
repository.logout()
|
||||
user.postValue(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadUser() = launchJob(Dispatchers.Default) {
|
||||
val userModel = if (repository.isAuthorized) {
|
||||
repository.cachedUser?.let(user::postValue)
|
||||
repository.loadUser()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
user.postValue(userModel)
|
||||
}
|
||||
|
||||
private fun authorize(code: String) = launchJob(Dispatchers.Default) {
|
||||
repository.authorize(code)
|
||||
user.postValue(repository.loadUser())
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
|
||||
fun create(authCode: String?): AniListSettingsViewModel
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package org.koitharu.kotatsu.scrobbling.data
|
||||
package org.koitharu.kotatsu.scrobbling.common.data
|
||||
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerMangaInfo
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerMangaInfo
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser
|
||||
|
||||
interface ScrobblerRepository {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package org.koitharu.kotatsu.scrobbling.data
|
||||
package org.koitharu.kotatsu.scrobbling.common.data
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.edit
|
||||
import org.jsoup.internal.StringUtil.StringJoiner
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser
|
||||
|
||||
private const val KEY_ACCESS_TOKEN = "access_token"
|
||||
private const val KEY_REFRESH_TOKEN = "refresh_token"
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.scrobbling.data
|
||||
package org.koitharu.kotatsu.scrobbling.common.data
|
||||
|
||||
import androidx.room.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -12,6 +12,9 @@ abstract class ScrobblingDao {
|
||||
@Query("SELECT * FROM scrobblings WHERE scrobbler = :scrobbler AND manga_id = :mangaId")
|
||||
abstract fun observe(scrobbler: Int, mangaId: Long): Flow<ScrobblingEntity?>
|
||||
|
||||
@Query("SELECT * FROM scrobblings WHERE scrobbler = :scrobbler")
|
||||
abstract fun observe(scrobbler: Int): Flow<List<ScrobblingEntity>>
|
||||
|
||||
@Upsert
|
||||
abstract suspend fun upsert(entity: ScrobblingEntity)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.scrobbling.data
|
||||
package org.koitharu.kotatsu.scrobbling.common.data
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
@@ -1,19 +1,23 @@
|
||||
package org.koitharu.kotatsu.scrobbling.domain
|
||||
package org.koitharu.kotatsu.scrobbling.common.domain
|
||||
|
||||
import androidx.collection.LongSparseArray
|
||||
import androidx.collection.getOrElse
|
||||
import androidx.core.text.parseAsHtml
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.scrobbling.data.ScrobblerRepository
|
||||
import org.koitharu.kotatsu.scrobbling.data.ScrobblingEntity
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerMangaInfo
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingInfo
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingStatus
|
||||
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblingEntity
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerMangaInfo
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
|
||||
import org.koitharu.kotatsu.utils.ext.findKeyByValue
|
||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
||||
@@ -22,15 +26,37 @@ import java.util.EnumMap
|
||||
abstract class Scrobbler(
|
||||
protected val db: MangaDatabase,
|
||||
val scrobblerService: ScrobblerService,
|
||||
private val repository: ScrobblerRepository,
|
||||
private val repository: org.koitharu.kotatsu.scrobbling.common.data.ScrobblerRepository,
|
||||
) {
|
||||
|
||||
private val infoCache = LongSparseArray<ScrobblerMangaInfo>()
|
||||
protected val statuses = EnumMap<ScrobblingStatus, String>(ScrobblingStatus::class.java)
|
||||
|
||||
val user: Flow<ScrobblerUser> = flow {
|
||||
repository.cachedUser?.let {
|
||||
emit(it)
|
||||
}
|
||||
runCatchingCancellable {
|
||||
repository.loadUser()
|
||||
}.onSuccess {
|
||||
emit(it)
|
||||
}.onFailure {
|
||||
it.printStackTraceDebug()
|
||||
}
|
||||
}
|
||||
|
||||
val isAvailable: Boolean
|
||||
get() = repository.isAuthorized
|
||||
|
||||
suspend fun authorize(authCode: String): ScrobblerUser {
|
||||
repository.authorize(authCode)
|
||||
return repository.loadUser()
|
||||
}
|
||||
|
||||
suspend fun logout() {
|
||||
repository.logout()
|
||||
}
|
||||
|
||||
suspend fun findManga(query: String, offset: Int): List<ScrobblerManga> {
|
||||
return repository.findManga(query, offset)
|
||||
}
|
||||
@@ -46,14 +72,27 @@ abstract class Scrobbler(
|
||||
|
||||
suspend fun getScrobblingInfoOrNull(mangaId: Long): ScrobblingInfo? {
|
||||
val entity = db.scrobblingDao.find(scrobblerService.id, mangaId) ?: return null
|
||||
return entity.toScrobblingInfo(mangaId)
|
||||
return entity.toScrobblingInfo()
|
||||
}
|
||||
|
||||
abstract suspend fun updateScrobblingInfo(mangaId: Long, rating: Float, status: ScrobblingStatus?, comment: String?)
|
||||
|
||||
fun observeScrobblingInfo(mangaId: Long): Flow<ScrobblingInfo?> {
|
||||
return db.scrobblingDao.observe(scrobblerService.id, mangaId)
|
||||
.map { it?.toScrobblingInfo(mangaId) }
|
||||
.map { it?.toScrobblingInfo() }
|
||||
}
|
||||
|
||||
fun observeAllScrobblingInfo(): Flow<List<ScrobblingInfo>> {
|
||||
return db.scrobblingDao.observe(scrobblerService.id)
|
||||
.map { entities ->
|
||||
coroutineScope {
|
||||
entities.map {
|
||||
async {
|
||||
it.toScrobblingInfo()
|
||||
}
|
||||
}.awaitAll()
|
||||
}.filterNotNull()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun unregisterScrobbling(mangaId: Long) {
|
||||
@@ -64,7 +103,7 @@ abstract class Scrobbler(
|
||||
return repository.getMangaInfo(id)
|
||||
}
|
||||
|
||||
private suspend fun ScrobblingEntity.toScrobblingInfo(mangaId: Long): ScrobblingInfo? {
|
||||
private suspend fun ScrobblingEntity.toScrobblingInfo(): ScrobblingInfo? {
|
||||
val mangaInfo = infoCache.getOrElse(targetId) {
|
||||
runCatchingCancellable {
|
||||
getMangaInfo(targetId)
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.scrobbling.domain.model
|
||||
package org.koitharu.kotatsu.scrobbling.common.domain.model
|
||||
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
@@ -37,4 +37,4 @@ class ScrobblerManga(
|
||||
override fun toString(): String {
|
||||
return "ScrobblerManga #$id \"$name\" $url"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.scrobbling.domain.model
|
||||
package org.koitharu.kotatsu.scrobbling.common.domain.model
|
||||
|
||||
class ScrobblerMangaInfo(
|
||||
val id: Long,
|
||||
@@ -6,4 +6,4 @@ class ScrobblerMangaInfo(
|
||||
val cover: String,
|
||||
val url: String,
|
||||
val descriptionHtml: String,
|
||||
)
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.scrobbling.domain.model
|
||||
package org.koitharu.kotatsu.scrobbling.common.domain.model
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.scrobbling.domain.model
|
||||
package org.koitharu.kotatsu.scrobbling.common.domain.model
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.scrobbling.domain.model
|
||||
package org.koitharu.kotatsu.scrobbling.common.domain.model
|
||||
|
||||
class ScrobblerUser(
|
||||
val id: Long,
|
||||
@@ -1,4 +1,6 @@
|
||||
package org.koitharu.kotatsu.scrobbling.domain.model
|
||||
package org.koitharu.kotatsu.scrobbling.common.domain.model
|
||||
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
class ScrobblingInfo(
|
||||
val scrobbler: ScrobblerService,
|
||||
@@ -12,7 +14,7 @@ class ScrobblingInfo(
|
||||
val coverUrl: String,
|
||||
val description: CharSequence?,
|
||||
val externalUrl: String,
|
||||
) {
|
||||
) : ListModel {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
@@ -49,4 +51,4 @@ class ScrobblingInfo(
|
||||
result = 31 * result + externalUrl.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.koitharu.kotatsu.scrobbling.common.domain.model
|
||||
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
enum class ScrobblingStatus : ListModel {
|
||||
|
||||
PLANNED,
|
||||
READING,
|
||||
RE_READING,
|
||||
COMPLETED,
|
||||
ON_HOLD,
|
||||
DROPPED,
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package org.koitharu.kotatsu.scrobbling.common.ui.config
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
import coil.ImageLoader
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.base.ui.list.decor.TypedSpacingItemDecoration
|
||||
import org.koitharu.kotatsu.databinding.ActivityScrobblerConfigBinding
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
||||
import org.koitharu.kotatsu.scrobbling.common.ui.config.adapter.ScrobblingMangaAdapter
|
||||
import org.koitharu.kotatsu.tracker.ui.feed.adapter.FeedAdapter
|
||||
import org.koitharu.kotatsu.utils.ext.assistedViewModels
|
||||
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
|
||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.utils.ext.hideCompat
|
||||
import org.koitharu.kotatsu.utils.ext.newImageRequest
|
||||
import org.koitharu.kotatsu.utils.ext.showCompat
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ScrobblerConfigActivity : BaseActivity<ActivityScrobblerConfigBinding>(),
|
||||
OnListItemClickListener<ScrobblingInfo>, View.OnClickListener {
|
||||
|
||||
@Inject
|
||||
lateinit var viewModelFactory: ScrobblerConfigViewModel.Factory
|
||||
|
||||
@Inject
|
||||
lateinit var coil: ImageLoader
|
||||
|
||||
private val viewModel: ScrobblerConfigViewModel by assistedViewModels {
|
||||
viewModelFactory.create(requireNotNull(getScrobblerService(intent)))
|
||||
}
|
||||
|
||||
private var paddingVertical = 0
|
||||
private var paddingHorizontal = 0
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(ActivityScrobblerConfigBinding.inflate(layoutInflater))
|
||||
setTitle(viewModel.titleResId)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
val listAdapter = ScrobblingMangaAdapter(this, coil, this)
|
||||
with(binding.recyclerView) {
|
||||
adapter = listAdapter
|
||||
setHasFixedSize(true)
|
||||
val spacing = resources.getDimensionPixelOffset(R.dimen.list_spacing)
|
||||
paddingHorizontal = spacing
|
||||
paddingVertical = resources.getDimensionPixelOffset(R.dimen.grid_spacing_outer)
|
||||
val decoration = TypedSpacingItemDecoration(
|
||||
FeedAdapter.ITEM_TYPE_FEED to 0,
|
||||
fallbackSpacing = spacing,
|
||||
)
|
||||
addItemDecoration(decoration)
|
||||
}
|
||||
binding.imageViewAvatar.setOnClickListener(this)
|
||||
|
||||
viewModel.content.observe(this, listAdapter::setItems)
|
||||
viewModel.user.observe(this, this::onUserChanged)
|
||||
viewModel.isLoading.observe(this, this::onLoadingStateChanged)
|
||||
viewModel.onError.observe(this, this::onError)
|
||||
viewModel.onLoggedOut.observe(this) {
|
||||
finishAfterTransition()
|
||||
}
|
||||
|
||||
processIntent(intent)
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
if (intent != null) {
|
||||
setIntent(intent)
|
||||
processIntent(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
binding.recyclerView.updatePadding(
|
||||
left = insets.left + paddingHorizontal,
|
||||
right = insets.right + paddingHorizontal,
|
||||
bottom = insets.bottom + paddingVertical,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onItemClick(item: ScrobblingInfo, view: View) {
|
||||
startActivity(
|
||||
DetailsActivity.newIntent(this, item.mangaId),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.imageView_avatar -> showUserDialog()
|
||||
}
|
||||
}
|
||||
|
||||
private fun processIntent(intent: Intent) {
|
||||
if (intent.action == Intent.ACTION_VIEW) {
|
||||
val uri = intent.data ?: return
|
||||
val code = uri.getQueryParameter("code")
|
||||
if (!code.isNullOrEmpty()) {
|
||||
viewModel.onAuthCodeReceived(code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onUserChanged(user: ScrobblerUser?) {
|
||||
if (user == null) {
|
||||
binding.imageViewAvatar.disposeImageRequest()
|
||||
binding.imageViewAvatar.isVisible = false
|
||||
return
|
||||
}
|
||||
binding.imageViewAvatar.isVisible = true
|
||||
binding.imageViewAvatar.newImageRequest(user.avatar, null)
|
||||
?.enqueueWith(coil)
|
||||
}
|
||||
|
||||
private fun onLoadingStateChanged(isLoading: Boolean) {
|
||||
binding.progressBar.run {
|
||||
if (isLoading) {
|
||||
showCompat()
|
||||
} else {
|
||||
hideCompat()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onError(e: Throwable) {
|
||||
Snackbar.make(
|
||||
binding.recyclerView,
|
||||
e.getDisplayMessage(resources),
|
||||
Snackbar.LENGTH_LONG,
|
||||
).show()
|
||||
}
|
||||
|
||||
private fun showUserDialog() {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(title)
|
||||
.setMessage(getString(R.string.logged_in_as, viewModel.user.value?.nickname))
|
||||
.setNegativeButton(R.string.close, null)
|
||||
.setPositiveButton(R.string.logout) { _, _ ->
|
||||
viewModel.logout()
|
||||
}.show()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val EXTRA_SERVICE_ID = "service"
|
||||
|
||||
private const val HOST_SHIKIMORI_AUTH = "shikimori-auth"
|
||||
private const val HOST_ANILIST_AUTH = "anilist-auth"
|
||||
private const val HOST_MAL_AUTH = "mal-auth"
|
||||
|
||||
fun newIntent(context: Context, service: ScrobblerService) =
|
||||
Intent(context, ScrobblerConfigActivity::class.java)
|
||||
.putExtra(EXTRA_SERVICE_ID, service.id)
|
||||
|
||||
private fun getScrobblerService(
|
||||
intent: Intent
|
||||
): ScrobblerService? {
|
||||
val serviceId = intent.getIntExtra(EXTRA_SERVICE_ID, 0)
|
||||
if (serviceId != 0) {
|
||||
return enumValues<ScrobblerService>().first { it.id == serviceId }
|
||||
}
|
||||
val uri = intent.data ?: return null
|
||||
return when (uri.host) {
|
||||
HOST_SHIKIMORI_AUTH -> ScrobblerService.SHIKIMORI
|
||||
HOST_ANILIST_AUTH -> ScrobblerService.ANILIST
|
||||
HOST_MAL_AUTH -> ScrobblerService.MAL
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package org.koitharu.kotatsu.scrobbling.common.ui.config
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.asFlowLiveData
|
||||
import org.koitharu.kotatsu.utils.ext.onFirst
|
||||
|
||||
class ScrobblerConfigViewModel @AssistedInject constructor(
|
||||
@Assisted scrobblerService: ScrobblerService,
|
||||
scrobblers: Set<@JvmSuppressWildcards Scrobbler>,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val scrobbler = scrobblers.first { it.scrobblerService == scrobblerService }
|
||||
|
||||
val titleResId = scrobbler.scrobblerService.titleResId
|
||||
|
||||
val user = MutableLiveData<ScrobblerUser?>(null)
|
||||
val onLoggedOut = SingleLiveEvent<Unit>()
|
||||
|
||||
val content = scrobbler.observeAllScrobblingInfo()
|
||||
.onStart { loadingCounter.increment() }
|
||||
.onFirst { loadingCounter.decrement() }
|
||||
.catch { errorEvent.postCall(it) }
|
||||
.map { buildContentList(it) }
|
||||
.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
|
||||
|
||||
init {
|
||||
scrobbler.user
|
||||
.onEach { user.postValue(it) }
|
||||
.launchIn(viewModelScope + Dispatchers.Default)
|
||||
}
|
||||
|
||||
fun onAuthCodeReceived(authCode: String) {
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
val newUser = scrobbler.authorize(authCode)
|
||||
user.postValue(newUser)
|
||||
}
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
scrobbler.logout()
|
||||
user.postValue(null)
|
||||
onLoggedOut.postCall(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildContentList(list: List<ScrobblingInfo>): List<ListModel> {
|
||||
if (list.isEmpty()) {
|
||||
return listOf(
|
||||
EmptyState(
|
||||
icon = R.drawable.ic_empty_history,
|
||||
textPrimary = R.string.nothing_here,
|
||||
textSecondary = R.string.scrobbling_empty_hint,
|
||||
actionStringRes = 0,
|
||||
),
|
||||
)
|
||||
}
|
||||
val grouped = list.groupBy { it.status }
|
||||
val statuses = enumValues<ScrobblingStatus>()
|
||||
val result = ArrayList<ListModel>(list.size + statuses.size)
|
||||
for (st in statuses) {
|
||||
val subList = grouped[st]
|
||||
if (subList.isNullOrEmpty()) {
|
||||
continue
|
||||
}
|
||||
result.add(st)
|
||||
result.addAll(subList)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
|
||||
fun create(service: ScrobblerService): ScrobblerConfigViewModel
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.koitharu.kotatsu.scrobbling.common.ui.config.adapter
|
||||
|
||||
import android.widget.TextView
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
|
||||
|
||||
fun scrobblingHeaderAD() = adapterDelegate<ScrobblingStatus, ListModel>(R.layout.item_header) {
|
||||
|
||||
bind {
|
||||
(itemView as TextView).text = context.resources
|
||||
.getStringArray(R.array.scrobbling_statuses)
|
||||
.getOrNull(item.ordinal)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package org.koitharu.kotatsu.scrobbling.common.ui.config.adapter
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import coil.ImageLoader
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.databinding.ItemScrobblingMangaBinding
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
||||
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
|
||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.utils.ext.newImageRequest
|
||||
|
||||
fun scrobblingMangaAD(
|
||||
clickListener: OnListItemClickListener<ScrobblingInfo>,
|
||||
coil: ImageLoader,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
) = adapterDelegateViewBinding<ScrobblingInfo, ListModel, ItemScrobblingMangaBinding>(
|
||||
{ layoutInflater, parent -> ItemScrobblingMangaBinding.inflate(layoutInflater, parent, false) },
|
||||
) {
|
||||
|
||||
val clickListenerAdapter = AdapterDelegateClickListenerAdapter(this, clickListener)
|
||||
itemView.setOnClickListener(clickListenerAdapter)
|
||||
|
||||
bind {
|
||||
binding.imageViewCover.newImageRequest(item.coverUrl, null)?.run {
|
||||
placeholder(R.drawable.ic_placeholder)
|
||||
fallback(R.drawable.ic_placeholder)
|
||||
error(R.drawable.ic_error_placeholder)
|
||||
lifecycle(lifecycleOwner)
|
||||
enqueueWith(coil)
|
||||
}
|
||||
binding.textViewTitle.text = item.title
|
||||
binding.ratingBar.rating = item.rating * binding.ratingBar.numStars
|
||||
}
|
||||
|
||||
onViewRecycled {
|
||||
binding.imageViewCover.disposeImageRequest()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.koitharu.kotatsu.scrobbling.common.ui.config.adapter
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import coil.ImageLoader
|
||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
|
||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
|
||||
|
||||
class ScrobblingMangaAdapter(
|
||||
clickListener: OnListItemClickListener<ScrobblingInfo>,
|
||||
coil: ImageLoader,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) {
|
||||
|
||||
init {
|
||||
delegatesManager.addDelegate(scrobblingMangaAD(clickListener, coil, lifecycleOwner))
|
||||
.addDelegate(scrobblingHeaderAD())
|
||||
.addDelegate(emptyStateListAD(coil, null))
|
||||
}
|
||||
|
||||
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
|
||||
|
||||
override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
|
||||
return when {
|
||||
oldItem is ScrobblingInfo && newItem is ScrobblingInfo -> {
|
||||
oldItem.targetId == newItem.targetId && oldItem.mangaId == newItem.mangaId
|
||||
}
|
||||
|
||||
oldItem is ScrobblingStatus && newItem is ScrobblingStatus -> {
|
||||
oldItem.ordinal == newItem.ordinal
|
||||
}
|
||||
|
||||
oldItem is EmptyState && newItem is EmptyState -> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.scrobbling.ui.selector
|
||||
package org.koitharu.kotatsu.scrobbling.common.ui.selector
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
@@ -24,10 +24,10 @@ import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.databinding.SheetScrobblingSelectorBinding
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService
|
||||
import org.koitharu.kotatsu.scrobbling.ui.selector.adapter.ScrobblerMangaSelectionDecoration
|
||||
import org.koitharu.kotatsu.scrobbling.ui.selector.adapter.ScrobblerSelectorAdapter
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
|
||||
import org.koitharu.kotatsu.scrobbling.common.ui.selector.adapter.ScrobblerMangaSelectionDecoration
|
||||
import org.koitharu.kotatsu.scrobbling.common.ui.selector.adapter.ScrobblerSelectorAdapter
|
||||
import org.koitharu.kotatsu.utils.ext.assistedViewModels
|
||||
import org.koitharu.kotatsu.utils.ext.firstVisibleItemPosition
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.scrobbling.ui.selector
|
||||
package org.koitharu.kotatsu.scrobbling.common.ui.selector
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
@@ -18,9 +18,9 @@ import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.scrobbling.domain.Scrobbler
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga
|
||||
import org.koitharu.kotatsu.scrobbling.ui.selector.model.ScrobblerHint
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga
|
||||
import org.koitharu.kotatsu.scrobbling.common.ui.selector.model.ScrobblerHint
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||
@@ -1,10 +1,10 @@
|
||||
package org.koitharu.kotatsu.scrobbling.ui.selector.adapter
|
||||
package org.koitharu.kotatsu.scrobbling.common.ui.selector.adapter
|
||||
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.databinding.ItemEmptyHintBinding
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.scrobbling.ui.selector.model.ScrobblerHint
|
||||
import org.koitharu.kotatsu.scrobbling.common.ui.selector.model.ScrobblerHint
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.utils.ext.setTextAndVisible
|
||||
import org.koitharu.kotatsu.utils.ext.textAndVisible
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.scrobbling.ui.selector.adapter
|
||||
package org.koitharu.kotatsu.scrobbling.common.ui.selector.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
@@ -8,7 +8,7 @@ import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.NO_ID
|
||||
import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga
|
||||
import org.koitharu.kotatsu.utils.ext.getItem
|
||||
|
||||
class ScrobblerMangaSelectionDecoration(context: Context) : MangaSelectionDecoration(context) {
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.scrobbling.ui.selector.adapter
|
||||
package org.koitharu.kotatsu.scrobbling.common.ui.selector.adapter
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
@@ -9,8 +9,8 @@ import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
|
||||
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
|
||||
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga
|
||||
import org.koitharu.kotatsu.scrobbling.ui.selector.model.ScrobblerHint
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga
|
||||
import org.koitharu.kotatsu.scrobbling.common.ui.selector.model.ScrobblerHint
|
||||
import kotlin.jvm.internal.Intrinsics
|
||||
|
||||
class ScrobblerSelectorAdapter(
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.scrobbling.ui.selector.adapter
|
||||
package org.koitharu.kotatsu.scrobbling.common.ui.selector.adapter
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import coil.ImageLoader
|
||||
@@ -7,7 +7,7 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.databinding.ItemMangaListBinding
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga
|
||||
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
|
||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.utils.ext.newImageRequest
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.scrobbling.ui.selector.model
|
||||
package org.koitharu.kotatsu.scrobbling.common.ui.selector.model
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
@@ -1,11 +0,0 @@
|
||||
package org.koitharu.kotatsu.scrobbling.domain.model
|
||||
|
||||
enum class ScrobblingStatus {
|
||||
|
||||
PLANNED,
|
||||
READING,
|
||||
RE_READING,
|
||||
COMPLETED,
|
||||
ON_HOLD,
|
||||
DROPPED,
|
||||
}
|
||||
@@ -7,9 +7,9 @@ import okhttp3.Response
|
||||
import okhttp3.Route
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||
import org.koitharu.kotatsu.scrobbling.data.ScrobblerStorage
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerType
|
||||
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerType
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.scrobbling.mal.data
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||
import org.koitharu.kotatsu.scrobbling.data.ScrobblerStorage
|
||||
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage
|
||||
|
||||
private const val JSON = "application/json"
|
||||
|
||||
|
||||
@@ -9,20 +9,18 @@ import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.parsers.util.await
|
||||
import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
||||
import org.koitharu.kotatsu.parsers.util.json.mapJSONNotNull
|
||||
import org.koitharu.kotatsu.parsers.util.parseJson
|
||||
import org.koitharu.kotatsu.parsers.util.toIntUp
|
||||
import org.koitharu.kotatsu.scrobbling.data.ScrobblerRepository
|
||||
import org.koitharu.kotatsu.scrobbling.data.ScrobblerStorage
|
||||
import org.koitharu.kotatsu.scrobbling.data.ScrobblingEntity
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerMangaInfo
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
||||
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage
|
||||
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblingEntity
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerMangaInfo
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser
|
||||
import org.koitharu.kotatsu.utils.PKCEGenerator
|
||||
|
||||
private const val REDIRECT_URI = "kotatsu://mal-auth"
|
||||
private const val BASE_OAUTH_URL = "https://myanimelist.net"
|
||||
private const val BASE_WEB_URL = "https://myanimelist.net"
|
||||
private const val BASE_API_URL = "https://api.myanimelist.net/v2"
|
||||
private const val AVATAR_STUB = "https://cdn.myanimelist.net/images/questionmark_50.gif"
|
||||
|
||||
@@ -30,12 +28,12 @@ class MALRepository(
|
||||
private val okHttp: OkHttpClient,
|
||||
private val storage: ScrobblerStorage,
|
||||
private val db: MangaDatabase,
|
||||
) : ScrobblerRepository {
|
||||
) : org.koitharu.kotatsu.scrobbling.common.data.ScrobblerRepository {
|
||||
|
||||
private var codeVerifier: String = getPKCEChallengeCode()
|
||||
|
||||
override val oauthUrl: String
|
||||
get() = "$BASE_OAUTH_URL/v1/oauth2/authorize?" +
|
||||
get() = "$BASE_WEB_URL/v1/oauth2/authorize?" +
|
||||
"response_type=code" +
|
||||
"&client_id=${BuildConfig.MAL_CLIENT_ID}" +
|
||||
"&redirect_uri=$REDIRECT_URI" +
|
||||
@@ -61,7 +59,7 @@ class MALRepository(
|
||||
}
|
||||
val request = Request.Builder()
|
||||
.post(body.build())
|
||||
.url("${BASE_OAUTH_URL}/v1/oauth2/token")
|
||||
.url("${BASE_WEB_URL}/v1/oauth2/token")
|
||||
|
||||
val response = okHttp.newCall(request.build()).await().parseJson()
|
||||
storage.accessToken = response.getString("access_token")
|
||||
@@ -83,17 +81,15 @@ class MALRepository(
|
||||
override suspend fun findManga(query: String, offset: Int): List<ScrobblerManga> {
|
||||
val url = BASE_API_URL.toHttpUrl().newBuilder()
|
||||
.addPathSegment("manga")
|
||||
.addQueryParameter("offset", offset.toFloat().toIntUp().toString())
|
||||
.addQueryParameter("offset", offset.toString())
|
||||
.addQueryParameter("nsfw", "true")
|
||||
.addQueryParameter(
|
||||
"q",
|
||||
query.take(64)
|
||||
) // WARNING! MAL API throws a 400 when the query is over 64 characters
|
||||
// WARNING! MAL API throws a 400 when the query is over 64 characters
|
||||
.addQueryParameter("q", query.take(64))
|
||||
.build()
|
||||
val request = Request.Builder().url(url).get().build()
|
||||
val response = okHttp.newCall(request).await().parseJson()
|
||||
val data = response.getJSONArray("data")
|
||||
return data.mapJSON { jsonToManga(it) }
|
||||
return data.mapJSONNotNull { jsonToManga(it) }
|
||||
}
|
||||
|
||||
override suspend fun getMangaInfo(id: Long): ScrobblerMangaInfo {
|
||||
@@ -181,7 +177,7 @@ class MALRepository(
|
||||
return codeVerifier
|
||||
}
|
||||
|
||||
private fun jsonToManga(json: JSONObject): ScrobblerManga {
|
||||
private fun jsonToManga(json: JSONObject): ScrobblerManga? {
|
||||
for (i in 0 until json.length()) {
|
||||
val node = json.getJSONObject("node")
|
||||
return ScrobblerManga(
|
||||
@@ -189,23 +185,17 @@ class MALRepository(
|
||||
name = node.getString("title"),
|
||||
altName = null,
|
||||
cover = node.getJSONObject("main_picture").getString("large"),
|
||||
url = "https://myanimelist.net/manga/${node.getLong("id")}"
|
||||
url = "$BASE_WEB_URL/manga/${node.getLong("id")}",
|
||||
)
|
||||
}
|
||||
return ScrobblerManga(
|
||||
id = 1,
|
||||
name = "",
|
||||
altName = null,
|
||||
cover = "",
|
||||
url = ""
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
private fun ScrobblerMangaInfo(json: JSONObject) = ScrobblerMangaInfo(
|
||||
id = json.getLong("id"),
|
||||
name = json.getString("title"),
|
||||
cover = json.getJSONObject("main_picture").getString("large"),
|
||||
url = "https://myanimelist.net/manga/${json.getLong("id")}",
|
||||
url = "$BASE_WEB_URL/manga/${json.getLong("id")}",
|
||||
descriptionHtml = json.getString("synopsis"),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
package org.koitharu.kotatsu.scrobbling.mal.domain
|
||||
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.scrobbling.domain.Scrobbler
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerMangaInfo
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingStatus
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
|
||||
import org.koitharu.kotatsu.scrobbling.mal.data.MALRepository
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
package org.koitharu.kotatsu.scrobbling.mal.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.preference.Preference
|
||||
import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import coil.transform.CircleCropTransformation
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
||||
import org.koitharu.kotatsu.utils.PreferenceIconTarget
|
||||
import org.koitharu.kotatsu.utils.ext.assistedViewModels
|
||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MALSettingsFragment : BasePreferenceFragment(R.string.mal) {
|
||||
|
||||
@Inject
|
||||
lateinit var coil: ImageLoader
|
||||
|
||||
@Inject
|
||||
lateinit var viewModelFactory: MALSettingsViewModel.Factory
|
||||
|
||||
private val viewModel by assistedViewModels {
|
||||
viewModelFactory.create(arguments?.getString(ARG_AUTH_CODE))
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_mal)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
viewModel.user.observe(viewLifecycleOwner, this::onUserChanged)
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
return when (preference.key) {
|
||||
KEY_USER -> openAuthorization()
|
||||
KEY_LOGOUT -> {
|
||||
viewModel.logout()
|
||||
true
|
||||
}
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onUserChanged(user: ScrobblerUser?) {
|
||||
val pref = findPreference<Preference>(KEY_USER) ?: return
|
||||
pref.isSelectable = user == null
|
||||
pref.title = user?.nickname ?: getString(R.string.sign_in)
|
||||
ImageRequest.Builder(requireContext())
|
||||
.data(user?.avatar)
|
||||
.transformations(CircleCropTransformation())
|
||||
.target(PreferenceIconTarget(pref))
|
||||
.enqueueWith(coil)
|
||||
findPreference<Preference>(KEY_LOGOUT)?.isVisible = user != null
|
||||
}
|
||||
|
||||
private fun openAuthorization(): Boolean {
|
||||
return runCatching {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.data = Uri.parse(viewModel.authorizationUrl)
|
||||
startActivity(intent)
|
||||
}.isSuccess
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_USER = "mal_user"
|
||||
private const val KEY_LOGOUT = "mal_logout"
|
||||
|
||||
private const val ARG_AUTH_CODE = "auth_code"
|
||||
|
||||
fun newInstance(authCode: String?) = MALSettingsFragment().withArgs(1) {
|
||||
putString(ARG_AUTH_CODE, authCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
package org.koitharu.kotatsu.scrobbling.mal.ui
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
||||
import org.koitharu.kotatsu.scrobbling.mal.data.MALRepository
|
||||
|
||||
class MALSettingsViewModel @AssistedInject constructor(
|
||||
private val repository: MALRepository,
|
||||
@Assisted authCode: String?,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val authorizationUrl: String
|
||||
get() = repository.oauthUrl
|
||||
|
||||
val user = MutableLiveData<ScrobblerUser?>()
|
||||
|
||||
init {
|
||||
if (authCode != null) {
|
||||
authorize(authCode)
|
||||
} else {
|
||||
loadUser()
|
||||
}
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
launchJob(Dispatchers.Default) {
|
||||
repository.logout()
|
||||
user.postValue(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadUser() = launchJob(Dispatchers.Default) {
|
||||
val userModel = if (repository.isAuthorized) {
|
||||
repository.cachedUser?.let(user::postValue)
|
||||
repository.loadUser()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
user.postValue(userModel)
|
||||
}
|
||||
|
||||
private fun authorize(code: String) = launchJob(Dispatchers.Default) {
|
||||
repository.authorize(code)
|
||||
user.postValue(repository.loadUser())
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
|
||||
fun create(authCode: String?): MALSettingsViewModel
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,9 @@ import okhttp3.Response
|
||||
import okhttp3.Route
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||
import org.koitharu.kotatsu.scrobbling.data.ScrobblerStorage
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerType
|
||||
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerType
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import okio.IOException
|
||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||
import org.koitharu.kotatsu.scrobbling.data.ScrobblerStorage
|
||||
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage
|
||||
|
||||
private const val USER_AGENT_SHIKIMORI = "Kotatsu"
|
||||
|
||||
|
||||
@@ -14,13 +14,13 @@ import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
||||
import org.koitharu.kotatsu.parsers.util.parseJson
|
||||
import org.koitharu.kotatsu.parsers.util.parseJsonArray
|
||||
import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl
|
||||
import org.koitharu.kotatsu.scrobbling.data.ScrobblerRepository
|
||||
import org.koitharu.kotatsu.scrobbling.data.ScrobblerStorage
|
||||
import org.koitharu.kotatsu.scrobbling.data.ScrobblingEntity
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerMangaInfo
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
||||
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerRepository
|
||||
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage
|
||||
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblingEntity
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerMangaInfo
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser
|
||||
import org.koitharu.kotatsu.utils.ext.toRequestBody
|
||||
|
||||
private const val REDIRECT_URI = "kotatsu://shikimori-auth"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package org.koitharu.kotatsu.scrobbling.shikimori.domain
|
||||
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.scrobbling.domain.Scrobbler
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingStatus
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
|
||||
import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriRepository
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
package org.koitharu.kotatsu.scrobbling.shikimori.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.preference.Preference
|
||||
import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import coil.transform.CircleCropTransformation
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
||||
import org.koitharu.kotatsu.utils.PreferenceIconTarget
|
||||
import org.koitharu.kotatsu.utils.ext.assistedViewModels
|
||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ShikimoriSettingsFragment : BasePreferenceFragment(R.string.shikimori) {
|
||||
|
||||
@Inject
|
||||
lateinit var coil: ImageLoader
|
||||
|
||||
@Inject
|
||||
lateinit var viewModelFactory: ShikimoriSettingsViewModel.Factory
|
||||
|
||||
private val viewModel by assistedViewModels {
|
||||
viewModelFactory.create(arguments?.getString(ARG_AUTH_CODE))
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_shikimori)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
viewModel.user.observe(viewLifecycleOwner, this::onUserChanged)
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
return when (preference.key) {
|
||||
KEY_USER -> openAuthorization()
|
||||
KEY_LOGOUT -> {
|
||||
viewModel.logout()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onUserChanged(user: ScrobblerUser?) {
|
||||
val pref = findPreference<Preference>(KEY_USER) ?: return
|
||||
pref.isSelectable = user == null
|
||||
pref.title = user?.nickname ?: getString(R.string.sign_in)
|
||||
ImageRequest.Builder(requireContext())
|
||||
.data(user?.avatar)
|
||||
.transformations(CircleCropTransformation())
|
||||
.target(PreferenceIconTarget(pref))
|
||||
.enqueueWith(coil)
|
||||
findPreference<Preference>(KEY_LOGOUT)?.isVisible = user != null
|
||||
}
|
||||
|
||||
private fun openAuthorization(): Boolean {
|
||||
return runCatching {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.data = Uri.parse(viewModel.authorizationUrl)
|
||||
startActivity(intent)
|
||||
}.isSuccess
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_USER = "shiki_user"
|
||||
private const val KEY_LOGOUT = "shiki_logout"
|
||||
|
||||
private const val ARG_AUTH_CODE = "auth_code"
|
||||
|
||||
fun newInstance(authCode: String?) = ShikimoriSettingsFragment().withArgs(1) {
|
||||
putString(ARG_AUTH_CODE, authCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package org.koitharu.kotatsu.scrobbling.shikimori.ui
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
||||
import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriRepository
|
||||
|
||||
class ShikimoriSettingsViewModel @AssistedInject constructor(
|
||||
private val repository: ShikimoriRepository,
|
||||
@Assisted authCode: String?,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val authorizationUrl: String
|
||||
get() = repository.oauthUrl
|
||||
|
||||
val user = MutableLiveData<ScrobblerUser?>()
|
||||
|
||||
init {
|
||||
if (authCode != null) {
|
||||
authorize(authCode)
|
||||
} else {
|
||||
loadUser()
|
||||
}
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
launchJob(Dispatchers.Default) {
|
||||
repository.logout()
|
||||
user.postValue(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadUser() = launchJob(Dispatchers.Default) {
|
||||
val userModel = if (repository.isAuthorized) {
|
||||
repository.cachedUser?.let(user::postValue)
|
||||
repository.loadUser()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
user.postValue(userModel)
|
||||
}
|
||||
|
||||
private fun authorize(code: String) = launchJob(Dispatchers.Default) {
|
||||
repository.authorize(code)
|
||||
user.postValue(repository.loadUser())
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
|
||||
fun create(authCode: String?): ShikimoriSettingsViewModel
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,8 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.local.data.CacheDir
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
||||
import org.koitharu.kotatsu.scrobbling.anilist.data.AniListRepository
|
||||
import org.koitharu.kotatsu.scrobbling.data.ScrobblerRepository
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
|
||||
import org.koitharu.kotatsu.scrobbling.common.ui.config.ScrobblerConfigActivity
|
||||
import org.koitharu.kotatsu.scrobbling.mal.data.MALRepository
|
||||
import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriRepository
|
||||
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
|
||||
@@ -130,28 +131,28 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
|
||||
AppSettings.KEY_SHIKIMORI -> {
|
||||
if (!shikimoriRepository.isAuthorized) {
|
||||
launchScrobblerAuth(shikimoriRepository)
|
||||
true
|
||||
} else {
|
||||
super.onPreferenceTreeClick(preference)
|
||||
startActivity(ScrobblerConfigActivity.newIntent(preference.context, ScrobblerService.SHIKIMORI))
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
AppSettings.KEY_MAL -> {
|
||||
if (!malRepository.isAuthorized) {
|
||||
launchScrobblerAuth(malRepository)
|
||||
true
|
||||
} else {
|
||||
super.onPreferenceTreeClick(preference)
|
||||
startActivity(ScrobblerConfigActivity.newIntent(preference.context, ScrobblerService.MAL))
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
AppSettings.KEY_ANILIST -> {
|
||||
if (!aniListRepository.isAuthorized) {
|
||||
launchScrobblerAuth(aniListRepository)
|
||||
true
|
||||
} else {
|
||||
super.onPreferenceTreeClick(preference)
|
||||
startActivity(ScrobblerConfigActivity.newIntent(preference.context, ScrobblerService.ANILIST))
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
@@ -217,7 +218,10 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
|
||||
}.show()
|
||||
}
|
||||
|
||||
private fun bindScrobblerSummary(key: String, repository: ScrobblerRepository) {
|
||||
private fun bindScrobblerSummary(
|
||||
key: String,
|
||||
repository: org.koitharu.kotatsu.scrobbling.common.data.ScrobblerRepository
|
||||
) {
|
||||
val pref = findPreference<Preference>(key) ?: return
|
||||
if (!repository.isAuthorized) {
|
||||
pref.setSummary(R.string.disabled)
|
||||
@@ -242,7 +246,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchScrobblerAuth(repository: ScrobblerRepository) {
|
||||
private fun launchScrobblerAuth(repository: org.koitharu.kotatsu.scrobbling.common.data.ScrobblerRepository) {
|
||||
runCatching {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.data = Uri.parse(repository.oauthUrl)
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.settings
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
@@ -24,9 +23,6 @@ import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner
|
||||
import org.koitharu.kotatsu.databinding.ActivitySettingsBinding
|
||||
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.scrobbling.anilist.ui.AniListSettingsFragment
|
||||
import org.koitharu.kotatsu.scrobbling.mal.ui.MALSettingsFragment
|
||||
import org.koitharu.kotatsu.scrobbling.shikimori.ui.ShikimoriSettingsFragment
|
||||
import org.koitharu.kotatsu.settings.sources.SourcesSettingsFragment
|
||||
import org.koitharu.kotatsu.settings.tracker.TrackerSettingsFragment
|
||||
import org.koitharu.kotatsu.utils.ext.isScrolledToTop
|
||||
@@ -126,10 +122,8 @@ class SettingsActivity :
|
||||
|
||||
private fun openDefaultFragment() {
|
||||
val fragment = when (intent?.action) {
|
||||
Intent.ACTION_VIEW -> handleUri(intent.data) ?: return
|
||||
ACTION_READER -> ReaderSettingsFragment()
|
||||
ACTION_SUGGESTIONS -> SuggestionsSettingsFragment()
|
||||
ACTION_SHIKIMORI -> ShikimoriSettingsFragment()
|
||||
ACTION_HISTORY -> HistorySettingsFragment()
|
||||
ACTION_TRACKER -> TrackerSettingsFragment()
|
||||
ACTION_SOURCE -> SourceSettingsFragment.newInstance(
|
||||
@@ -145,21 +139,6 @@ class SettingsActivity :
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleUri(uri: Uri?): Fragment? {
|
||||
when (uri?.host) {
|
||||
HOST_SHIKIMORI_AUTH ->
|
||||
return ShikimoriSettingsFragment.newInstance(authCode = uri.getQueryParameter("code"))
|
||||
|
||||
HOST_ANILIST_AUTH ->
|
||||
return AniListSettingsFragment.newInstance(authCode = uri.getQueryParameter("code"))
|
||||
|
||||
HOST_MAL_AUTH ->
|
||||
return MALSettingsFragment.newInstance(authCode = uri.getQueryParameter("code"))
|
||||
}
|
||||
finishAfterTransition()
|
||||
return null
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val ACTION_READER = "${BuildConfig.APPLICATION_ID}.action.MANAGE_READER_SETTINGS"
|
||||
@@ -171,20 +150,12 @@ class SettingsActivity :
|
||||
private const val ACTION_MANAGE_SOURCES = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCES_LIST"
|
||||
private const val EXTRA_SOURCE = "source"
|
||||
|
||||
private const val HOST_SHIKIMORI_AUTH = "shikimori-auth"
|
||||
private const val HOST_ANILIST_AUTH = "anilist-auth"
|
||||
private const val HOST_MAL_AUTH = "mal-auth"
|
||||
|
||||
fun newIntent(context: Context) = Intent(context, SettingsActivity::class.java)
|
||||
|
||||
fun newReaderSettingsIntent(context: Context) =
|
||||
Intent(context, SettingsActivity::class.java)
|
||||
.setAction(ACTION_READER)
|
||||
|
||||
fun newShikimoriSettingsIntent(context: Context) =
|
||||
Intent(context, SettingsActivity::class.java)
|
||||
.setAction(ACTION_SHIKIMORI)
|
||||
|
||||
fun newSuggestionsSettingsIntent(context: Context) =
|
||||
Intent(context, SettingsActivity::class.java)
|
||||
.setAction(ACTION_SUGGESTIONS)
|
||||
|
||||
@@ -41,8 +41,6 @@ class FeedFragment :
|
||||
private val viewModel by viewModels<FeedViewModel>()
|
||||
|
||||
private var feedAdapter: FeedAdapter? = null
|
||||
private var paddingVertical = 0
|
||||
private var paddingHorizontal = 0
|
||||
|
||||
override fun onInflateView(
|
||||
inflater: LayoutInflater,
|
||||
@@ -57,8 +55,6 @@ class FeedFragment :
|
||||
setHasFixedSize(true)
|
||||
addOnScrollListener(PaginationScrollListener(4, this@FeedFragment))
|
||||
val spacing = resources.getDimensionPixelOffset(R.dimen.list_spacing)
|
||||
paddingHorizontal = spacing
|
||||
paddingVertical = resources.getDimensionPixelOffset(R.dimen.grid_spacing_outer)
|
||||
val decoration = TypedSpacingItemDecoration(
|
||||
FeedAdapter.ITEM_TYPE_FEED to 0,
|
||||
fallbackSpacing = spacing,
|
||||
|
||||
@@ -2,7 +2,12 @@ package org.koitharu.kotatsu.utils.ext
|
||||
|
||||
import android.os.SystemClock
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
|
||||
fun <T> Flow<T>.onFirst(action: suspend (T) -> Unit): Flow<T> {
|
||||
var isFirstCall = true
|
||||
@@ -11,6 +16,8 @@ fun <T> Flow<T>.onFirst(action: suspend (T) -> Unit): Flow<T> {
|
||||
action(it)
|
||||
isFirstCall = false
|
||||
}
|
||||
}.onCompletion {
|
||||
isFirstCall = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 723 B After Width: | Height: | Size: 723 B |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
@@ -2,6 +2,7 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?colorControlNormal"
|
||||
android:viewportWidth="18"
|
||||
android:viewportHeight="18">
|
||||
<path
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?colorControlNormal"
|
||||
android:viewportWidth="30.72"
|
||||
android:viewportHeight="30.72">
|
||||
<path
|
||||
|
||||
5
app/src/main/res/drawable/ic_shikimori.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<bitmap
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:src="@drawable/ic_shikimori_raw"
|
||||
android:tint="?colorControlNormal" />
|
||||
74
app/src/main/res/layout/activity_scrobbler_config.xml
Normal file
@@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
android:id="@+id/collapsingToolbarLayout"
|
||||
style="?attr/collapsingToolbarLayoutMediumStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
|
||||
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
|
||||
app:toolbarId="@id/toolbar">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:layout_collapseMode="pin">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/imageView_avatar"
|
||||
android:layout_width="28dp"
|
||||
android:layout_height="28dp"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:layout_marginHorizontal="@dimen/toolbar_button_margin"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:padding="1dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Circle"
|
||||
app:strokeColor="?colorOutline"
|
||||
app:strokeWidth="1dp"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
</com.google.android.material.appbar.MaterialToolbar>
|
||||
|
||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
android:paddingLeft="@dimen/list_spacing"
|
||||
android:paddingTop="@dimen/grid_spacing_outer"
|
||||
android:paddingRight="@dimen/list_spacing"
|
||||
android:paddingBottom="@dimen/grid_spacing_outer"
|
||||
android:scrollbars="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
|
||||
tools:listitem="@layout/item_scrobbling_manga" />
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone"
|
||||
app:layout_anchor="@id/recyclerView"
|
||||
app:layout_anchorGravity="center"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
51
app/src/main/res/layout/item_scrobbling_manga.xml
Normal file
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/list_selector"
|
||||
android:clipChildren="false"
|
||||
android:padding="@dimen/list_spacing">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/imageView_cover"
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
|
||||
tools:src="@sample/covers" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
app:layout_constraintBottom_toTopOf="@+id/ratingBar"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/imageView_cover"
|
||||
app:layout_constraintTop_toTopOf="@+id/imageView_cover"
|
||||
tools:text="@sample/titles" />
|
||||
|
||||
<RatingBar
|
||||
android:id="@+id/ratingBar"
|
||||
style="?ratingBarStyleSmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:isIndicator="true"
|
||||
android:max="1"
|
||||
android:numStars="5"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/imageView_cover"
|
||||
app:layout_constraintStart_toEndOf="@+id/imageView_cover"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView_title" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -416,4 +416,6 @@
|
||||
<string name="theme_name_mion">Mion</string>
|
||||
<string name="theme_name_rikka">Rikka</string>
|
||||
<string name="theme_name_sakura">Sakura</string>
|
||||
<string name="nothing_here">There is nothing here</string>
|
||||
<string name="scrobbling_empty_hint">To track reading progress, select Menu → Track on the manga details screen.</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
app:initialExpandedChildrenCount="5">
|
||||
|
||||
<Preference
|
||||
android:key="al_user"
|
||||
android:persistent="false"
|
||||
android:title="@string/loading_"
|
||||
app:iconSpaceReserved="true" />
|
||||
|
||||
<Preference
|
||||
android:key="al_logout"
|
||||
android:persistent="false"
|
||||
android:title="@string/logout"
|
||||
app:allowDividerAbove="true" />
|
||||
|
||||
</PreferenceScreen>
|
||||
@@ -22,23 +22,20 @@
|
||||
|
||||
<PreferenceCategory android:title="@string/tracking">
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.scrobbling.shikimori.ui.ShikimoriSettingsFragment"
|
||||
android:icon="@drawable/ic_shikimori"
|
||||
<Preference
|
||||
android:key="shikimori"
|
||||
android:title="@string/shikimori" />
|
||||
android:title="@string/shikimori"
|
||||
app:icon="@drawable/ic_shikimori" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.scrobbling.mal.ui.MALSettingsFragment"
|
||||
android:key="mal"
|
||||
android:icon="@drawable/ic_mal"
|
||||
android:title="@string/mal" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.scrobbling.anilist.ui.AniListSettingsFragment"
|
||||
android:icon="@drawable/ic_anilist"
|
||||
<Preference
|
||||
android:key="anilist"
|
||||
android:title="@string/anilist" />
|
||||
android:title="@string/anilist"
|
||||
app:icon="@drawable/ic_anilist" />
|
||||
|
||||
<Preference
|
||||
android:key="mal"
|
||||
android:title="@string/mal"
|
||||
app:icon="@drawable/ic_mal" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
app:initialExpandedChildrenCount="5">
|
||||
|
||||
<Preference
|
||||
android:key="mal_user"
|
||||
android:persistent="false"
|
||||
android:title="@string/loading_"
|
||||
app:iconSpaceReserved="true" />
|
||||
|
||||
<Preference
|
||||
android:key="mal_logout"
|
||||
android:persistent="false"
|
||||
android:title="@string/logout"
|
||||
app:allowDividerAbove="true" />
|
||||
|
||||
</PreferenceScreen>
|
||||
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
app:initialExpandedChildrenCount="5">
|
||||
|
||||
<Preference
|
||||
android:key="shiki_user"
|
||||
android:persistent="false"
|
||||
android:title="@string/loading_"
|
||||
app:iconSpaceReserved="true" />
|
||||
|
||||
<Preference
|
||||
android:key="shiki_logout"
|
||||
android:persistent="false"
|
||||
android:title="@string/logout"
|
||||
app:allowDividerAbove="true" />
|
||||
|
||||
</PreferenceScreen>
|
||||