Scrobblers config activity

This commit is contained in:
Koitharu
2023-02-10 20:36:59 +02:00
parent c2e56f7ba6
commit 26b852365a
79 changed files with 814 additions and 718 deletions

View File

@@ -85,16 +85,7 @@
<activity <activity
android:name="org.koitharu.kotatsu.settings.SettingsActivity" android:name="org.koitharu.kotatsu.settings.SettingsActivity"
android:exported="true" android:exported="true"
android:label="@string/settings"> 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>
<activity <activity
android:name="org.koitharu.kotatsu.browser.BrowserActivity" android:name="org.koitharu.kotatsu.browser.BrowserActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden" android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
@@ -140,6 +131,22 @@
<activity <activity
android:name="org.koitharu.kotatsu.shelf.ui.config.ShelfSettingsActivity" android:name="org.koitharu.kotatsu.shelf.ui.config.ShelfSettingsActivity"
android:label="@string/settings" /> 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 <service
android:name="org.koitharu.kotatsu.download.ui.service.DownloadService" android:name="org.koitharu.kotatsu.download.ui.service.DownloadService"

View File

@@ -4,11 +4,6 @@ import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.util.Size import android.util.Size
import androidx.room.withTransaction 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.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
@@ -17,7 +12,11 @@ import kotlinx.coroutines.runInterruptible
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.* import org.koitharu.kotatsu.core.db.entity.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.network.CommonHeaders
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.ReaderMode 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.model.MangaTag
import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter 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 private const val MIN_WEBTOON_RATIO = 2
@@ -121,7 +125,7 @@ class MangaDataRepository @Inject constructor(
val request = Request.Builder() val request = Request.Builder()
.url(url) .url(url)
.get() .get()
.header(CommonHeaders.REFERER, page.referer) .tag(MangaSource::class.java, page.source)
.cacheControl(CommonHeaders.CACHE_CONTROL_DISABLED) .cacheControl(CommonHeaders.CACHE_CONTROL_DISABLED)
.build() .build()
okHttpClient.newCall(request).await().use { okHttpClient.newCall(request).await().use {

View File

@@ -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.MangaPrefsEntity
import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity 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.FavouriteCategoriesDao
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.FavouriteEntity import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.favourites.data.FavouritesDao import org.koitharu.kotatsu.favourites.data.FavouritesDao
import org.koitharu.kotatsu.history.data.HistoryDao import org.koitharu.kotatsu.history.data.HistoryDao
import org.koitharu.kotatsu.history.data.HistoryEntity import org.koitharu.kotatsu.history.data.HistoryEntity
import org.koitharu.kotatsu.scrobbling.data.ScrobblingDao import org.koitharu.kotatsu.scrobbling.common.data.ScrobblingDao
import org.koitharu.kotatsu.scrobbling.data.ScrobblingEntity import org.koitharu.kotatsu.scrobbling.common.data.ScrobblingEntity
import org.koitharu.kotatsu.suggestions.data.SuggestionDao import org.koitharu.kotatsu.suggestions.data.SuggestionDao
import org.koitharu.kotatsu.suggestions.data.SuggestionEntity import org.koitharu.kotatsu.suggestions.data.SuggestionEntity
import org.koitharu.kotatsu.tracker.data.TrackEntity import org.koitharu.kotatsu.tracker.data.TrackEntity

View File

@@ -5,7 +5,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.utils.ext.daysDiff import org.koitharu.kotatsu.utils.ext.daysDiff
import org.koitharu.kotatsu.utils.ext.format import org.koitharu.kotatsu.utils.ext.format
import java.util.* import java.util.Date
sealed class DateTimeAgo : ListModel { sealed class DateTimeAgo : ListModel {
@@ -17,6 +17,8 @@ sealed class DateTimeAgo : ListModel {
} }
override fun toString() = "just_now" override fun toString() = "just_now"
override fun equals(other: Any?): Boolean = other === JustNow
} }
class MinutesAgo(val minutes: Int) : DateTimeAgo() { class MinutesAgo(val minutes: Int) : DateTimeAgo() {
@@ -60,6 +62,8 @@ sealed class DateTimeAgo : ListModel {
} }
override fun toString() = "today" override fun toString() = "today"
override fun equals(other: Any?): Boolean = other === Today
} }
object Yesterday : DateTimeAgo() { object Yesterday : DateTimeAgo() {
@@ -68,6 +72,8 @@ sealed class DateTimeAgo : ListModel {
} }
override fun toString() = "yesterday" override fun toString() = "yesterday"
override fun equals(other: Any?): Boolean = other === Yesterday
} }
class DaysAgo(val days: Int) : DateTimeAgo() { class DaysAgo(val days: Int) : DateTimeAgo() {
@@ -119,5 +125,7 @@ sealed class DateTimeAgo : ListModel {
} }
override fun toString() = "long_ago" override fun toString() = "long_ago"
override fun equals(other: Any?): Boolean = other === LongAgo
} }
} }

View File

@@ -40,7 +40,7 @@ import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.reader.ui.ReaderState 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.MangaListActivity
import org.koitharu.kotatsu.search.ui.SearchActivity import org.koitharu.kotatsu.search.ui.SearchActivity
import org.koitharu.kotatsu.utils.FileSize import org.koitharu.kotatsu.utils.FileSize

View File

@@ -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.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet 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.search.ui.multi.MultiSearchActivity
import org.koitharu.kotatsu.utils.ShareHelper import org.koitharu.kotatsu.utils.ShareHelper

View File

@@ -45,9 +45,9 @@ import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.scrobbling.domain.Scrobbler import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingInfo import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingStatus import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.asFlowLiveData import org.koitharu.kotatsu.utils.asFlowLiveData

View File

@@ -6,7 +6,7 @@ import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.ItemScrobblingInfoBinding 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.disposeImageRequest
import org.koitharu.kotatsu.utils.ext.enqueueWith import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest import org.koitharu.kotatsu.utils.ext.newImageRequest

View File

@@ -21,9 +21,9 @@ import org.koitharu.kotatsu.base.ui.BaseBottomSheet
import org.koitharu.kotatsu.databinding.SheetScrobblingBinding import org.koitharu.kotatsu.databinding.SheetScrobblingBinding
import org.koitharu.kotatsu.details.ui.DetailsViewModel import org.koitharu.kotatsu.details.ui.DetailsViewModel
import org.koitharu.kotatsu.image.ui.ImageActivity import org.koitharu.kotatsu.image.ui.ImageActivity
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingInfo import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingStatus import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
import org.koitharu.kotatsu.scrobbling.ui.selector.ScrobblingSelectorBottomSheet import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorBottomSheet
import org.koitharu.kotatsu.utils.ext.enqueueWith import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.newImageRequest import org.koitharu.kotatsu.utils.ext.newImageRequest

View File

@@ -5,7 +5,7 @@ import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import coil.ImageLoader import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingInfo import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
class ScrollingInfoAdapter( class ScrollingInfoAdapter(
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
@@ -27,7 +27,7 @@ class ScrollingInfoAdapter(
return oldItem == newItem return oldItem == newItem
} }
override fun getChangePayload(oldItem: ScrobblingInfo, newItem: ScrobblingInfo): Any? { override fun getChangePayload(oldItem: ScrobblingInfo, newItem: ScrobblingInfo): Any {
return Unit return Unit
} }
} }

View File

@@ -84,5 +84,8 @@ sealed interface ExploreItem : ListModel {
@StringRes actionStringRes: Int, @StringRes actionStringRes: Int,
) : EmptyState(icon, textPrimary, textSecondary, actionStringRes), ExploreItem ) : EmptyState(icon, textPrimary, textSecondary, actionStringRes), ExploreItem
object Loading : ExploreItem object Loading : ExploreItem {
override fun equals(other: Any?): Boolean = other === Loading
}
} }

View File

@@ -22,8 +22,8 @@ import org.koitharu.kotatsu.history.data.HistoryEntity
import org.koitharu.kotatsu.history.data.toMangaHistory import org.koitharu.kotatsu.history.data.toMangaHistory
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.scrobbling.domain.Scrobbler import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
import org.koitharu.kotatsu.scrobbling.domain.tryScrobble import org.koitharu.kotatsu.scrobbling.common.domain.tryScrobble
import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.ext.mapItems import org.koitharu.kotatsu.utils.ext.mapItems
import javax.inject.Inject import javax.inject.Inject

View File

@@ -12,17 +12,22 @@ import org.koitharu.kotatsu.utils.ext.setTextAndVisible
fun emptyStateListAD( fun emptyStateListAD(
coil: ImageLoader, coil: ImageLoader,
listener: ListStateHolderListener, listener: ListStateHolderListener?,
) = adapterDelegateViewBinding<EmptyState, ListModel, ItemEmptyStateBinding>( ) = adapterDelegateViewBinding<EmptyState, ListModel, ItemEmptyStateBinding>(
{ inflater, parent -> ItemEmptyStateBinding.inflate(inflater, parent, false) }, { inflater, parent -> ItemEmptyStateBinding.inflate(inflater, parent, false) },
) { ) {
binding.buttonRetry.setOnClickListener { listener.onEmptyActionClick() }
if (listener != null) {
binding.buttonRetry.setOnClickListener { listener.onEmptyActionClick() }
}
bind { bind {
binding.icon.newImageRequest(item.icon)?.enqueueWith(coil) binding.icon.newImageRequest(item.icon)?.enqueueWith(coil)
binding.textPrimary.setText(item.textPrimary) binding.textPrimary.setText(item.textPrimary)
binding.textSecondary.setTextAndVisible(item.textSecondary) binding.textSecondary.setTextAndVisible(item.textSecondary)
binding.buttonRetry.setTextAndVisible(item.actionStringRes) if (listener != null) {
binding.buttonRetry.setTextAndVisible(item.actionStringRes)
}
} }
onViewRecycled { onViewRecycled {

View File

@@ -1,3 +1,6 @@
package org.koitharu.kotatsu.list.ui.model package org.koitharu.kotatsu.list.ui.model
interface ListModel interface ListModel {
override fun equals(other: Any?): Boolean
}

View File

@@ -1,3 +1,6 @@
package org.koitharu.kotatsu.list.ui.model package org.koitharu.kotatsu.list.ui.model
object LoadingFooter : ListModel object LoadingFooter : ListModel {
override fun equals(other: Any?): Boolean = other === LoadingFooter
}

View File

@@ -1,3 +1,6 @@
package org.koitharu.kotatsu.list.ui.model package org.koitharu.kotatsu.list.ui.model
object LoadingState : ListModel object LoadingState : ListModel {
override fun equals(other: Any?): Boolean = other === LoadingState
}

View File

@@ -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.AniListInterceptor
import org.koitharu.kotatsu.scrobbling.anilist.data.AniListRepository import org.koitharu.kotatsu.scrobbling.anilist.data.AniListRepository
import org.koitharu.kotatsu.scrobbling.anilist.domain.AniListScrobbler import org.koitharu.kotatsu.scrobbling.anilist.domain.AniListScrobbler
import org.koitharu.kotatsu.scrobbling.data.ScrobblerStorage import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage
import org.koitharu.kotatsu.scrobbling.domain.Scrobbler 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.MALAuthenticator
import org.koitharu.kotatsu.scrobbling.mal.data.MALInterceptor import org.koitharu.kotatsu.scrobbling.mal.data.MALInterceptor
import org.koitharu.kotatsu.scrobbling.mal.data.MALRepository import org.koitharu.kotatsu.scrobbling.mal.data.MALRepository
import org.koitharu.kotatsu.scrobbling.mal.domain.MALScrobbler 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.ShikimoriAuthenticator
import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriInterceptor import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriInterceptor
import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriRepository import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriRepository

View File

@@ -7,9 +7,9 @@ import okhttp3.Response
import okhttp3.Route import okhttp3.Route
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.network.CommonHeaders import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.scrobbling.data.ScrobblerStorage import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerType import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerType
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider import javax.inject.Provider

View File

@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.scrobbling.anilist.data
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import org.koitharu.kotatsu.core.network.CommonHeaders 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" private const val JSON = "application/json"

View File

@@ -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.json.mapJSON
import org.koitharu.kotatsu.parsers.util.parseJson import org.koitharu.kotatsu.parsers.util.parseJson
import org.koitharu.kotatsu.parsers.util.toIntUp import org.koitharu.kotatsu.parsers.util.toIntUp
import org.koitharu.kotatsu.scrobbling.data.ScrobblerRepository import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage
import org.koitharu.kotatsu.scrobbling.data.ScrobblerStorage import org.koitharu.kotatsu.scrobbling.common.data.ScrobblingEntity
import org.koitharu.kotatsu.scrobbling.data.ScrobblingEntity import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerMangaInfo
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerMangaInfo import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
import kotlin.math.roundToInt import kotlin.math.roundToInt
private const val REDIRECT_URI = "kotatsu://anilist-auth" private const val REDIRECT_URI = "kotatsu://anilist-auth"
@@ -36,7 +35,7 @@ class AniListRepository(
private val okHttp: OkHttpClient, private val okHttp: OkHttpClient,
private val storage: ScrobblerStorage, private val storage: ScrobblerStorage,
private val db: MangaDatabase, private val db: MangaDatabase,
) : ScrobblerRepository { ) : org.koitharu.kotatsu.scrobbling.common.data.ScrobblerRepository {
override val oauthUrl: String override val oauthUrl: String
get() = "${BASE_URL}oauth/authorize?client_id=${BuildConfig.ANILIST_CLIENT_ID}&" + get() = "${BASE_URL}oauth/authorize?client_id=${BuildConfig.ANILIST_CLIENT_ID}&" +

View File

@@ -2,9 +2,9 @@ package org.koitharu.kotatsu.scrobbling.anilist.domain
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.scrobbling.anilist.data.AniListRepository import org.koitharu.kotatsu.scrobbling.anilist.data.AniListRepository
import org.koitharu.kotatsu.scrobbling.domain.Scrobbler import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingStatus import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton

View File

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

View File

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

View File

@@ -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.parsers.model.MangaChapter
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerMangaInfo import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerMangaInfo
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser
interface ScrobblerRepository { interface ScrobblerRepository {

View File

@@ -1,10 +1,10 @@
package org.koitharu.kotatsu.scrobbling.data package org.koitharu.kotatsu.scrobbling.common.data
import android.content.Context import android.content.Context
import androidx.core.content.edit import androidx.core.content.edit
import org.jsoup.internal.StringUtil.StringJoiner import org.jsoup.internal.StringUtil.StringJoiner
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser
private const val KEY_ACCESS_TOKEN = "access_token" private const val KEY_ACCESS_TOKEN = "access_token"
private const val KEY_REFRESH_TOKEN = "refresh_token" private const val KEY_REFRESH_TOKEN = "refresh_token"

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.scrobbling.data package org.koitharu.kotatsu.scrobbling.common.data
import androidx.room.* import androidx.room.*
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -12,6 +12,9 @@ abstract class ScrobblingDao {
@Query("SELECT * FROM scrobblings WHERE scrobbler = :scrobbler AND manga_id = :mangaId") @Query("SELECT * FROM scrobblings WHERE scrobbler = :scrobbler AND manga_id = :mangaId")
abstract fun observe(scrobbler: Int, mangaId: Long): Flow<ScrobblingEntity?> 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 @Upsert
abstract suspend fun upsert(entity: ScrobblingEntity) abstract suspend fun upsert(entity: ScrobblingEntity)

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.scrobbling.data package org.koitharu.kotatsu.scrobbling.common.data
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity

View File

@@ -1,19 +1,23 @@
package org.koitharu.kotatsu.scrobbling.domain package org.koitharu.kotatsu.scrobbling.common.domain
import androidx.collection.LongSparseArray import androidx.collection.LongSparseArray
import androidx.collection.getOrElse import androidx.collection.getOrElse
import androidx.core.text.parseAsHtml 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.flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.scrobbling.data.ScrobblerRepository import org.koitharu.kotatsu.scrobbling.common.data.ScrobblingEntity
import org.koitharu.kotatsu.scrobbling.data.ScrobblingEntity import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerMangaInfo
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerMangaInfo import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingInfo import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingStatus import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
import org.koitharu.kotatsu.utils.ext.findKeyByValue import org.koitharu.kotatsu.utils.ext.findKeyByValue
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
@@ -22,15 +26,37 @@ import java.util.EnumMap
abstract class Scrobbler( abstract class Scrobbler(
protected val db: MangaDatabase, protected val db: MangaDatabase,
val scrobblerService: ScrobblerService, val scrobblerService: ScrobblerService,
private val repository: ScrobblerRepository, private val repository: org.koitharu.kotatsu.scrobbling.common.data.ScrobblerRepository,
) { ) {
private val infoCache = LongSparseArray<ScrobblerMangaInfo>() private val infoCache = LongSparseArray<ScrobblerMangaInfo>()
protected val statuses = EnumMap<ScrobblingStatus, String>(ScrobblingStatus::class.java) 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 val isAvailable: Boolean
get() = repository.isAuthorized 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> { suspend fun findManga(query: String, offset: Int): List<ScrobblerManga> {
return repository.findManga(query, offset) return repository.findManga(query, offset)
} }
@@ -46,14 +72,27 @@ abstract class Scrobbler(
suspend fun getScrobblingInfoOrNull(mangaId: Long): ScrobblingInfo? { suspend fun getScrobblingInfoOrNull(mangaId: Long): ScrobblingInfo? {
val entity = db.scrobblingDao.find(scrobblerService.id, mangaId) ?: return null 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?) abstract suspend fun updateScrobblingInfo(mangaId: Long, rating: Float, status: ScrobblingStatus?, comment: String?)
fun observeScrobblingInfo(mangaId: Long): Flow<ScrobblingInfo?> { fun observeScrobblingInfo(mangaId: Long): Flow<ScrobblingInfo?> {
return db.scrobblingDao.observe(scrobblerService.id, mangaId) 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) { suspend fun unregisterScrobbling(mangaId: Long) {
@@ -64,7 +103,7 @@ abstract class Scrobbler(
return repository.getMangaInfo(id) return repository.getMangaInfo(id)
} }
private suspend fun ScrobblingEntity.toScrobblingInfo(mangaId: Long): ScrobblingInfo? { private suspend fun ScrobblingEntity.toScrobblingInfo(): ScrobblingInfo? {
val mangaInfo = infoCache.getOrElse(targetId) { val mangaInfo = infoCache.getOrElse(targetId) {
runCatchingCancellable { runCatchingCancellable {
getMangaInfo(targetId) getMangaInfo(targetId)

View File

@@ -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 import org.koitharu.kotatsu.list.ui.model.ListModel
@@ -37,4 +37,4 @@ class ScrobblerManga(
override fun toString(): String { override fun toString(): String {
return "ScrobblerManga #$id \"$name\" $url" return "ScrobblerManga #$id \"$name\" $url"
} }
} }

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.scrobbling.domain.model package org.koitharu.kotatsu.scrobbling.common.domain.model
class ScrobblerMangaInfo( class ScrobblerMangaInfo(
val id: Long, val id: Long,
@@ -6,4 +6,4 @@ class ScrobblerMangaInfo(
val cover: String, val cover: String,
val url: String, val url: String,
val descriptionHtml: String, val descriptionHtml: String,
) )

View File

@@ -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.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.scrobbling.domain.model package org.koitharu.kotatsu.scrobbling.common.domain.model
import javax.inject.Qualifier import javax.inject.Qualifier

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.scrobbling.domain.model package org.koitharu.kotatsu.scrobbling.common.domain.model
class ScrobblerUser( class ScrobblerUser(
val id: Long, val id: Long,

View File

@@ -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( class ScrobblingInfo(
val scrobbler: ScrobblerService, val scrobbler: ScrobblerService,
@@ -12,7 +14,7 @@ class ScrobblingInfo(
val coverUrl: String, val coverUrl: String,
val description: CharSequence?, val description: CharSequence?,
val externalUrl: String, val externalUrl: String,
) { ) : ListModel {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
@@ -49,4 +51,4 @@ class ScrobblingInfo(
result = 31 * result + externalUrl.hashCode() result = 31 * result + externalUrl.hashCode()
return result return result
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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.app.Dialog
import android.content.DialogInterface 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.databinding.SheetScrobblingSelectorBinding
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.ui.selector.adapter.ScrobblerMangaSelectionDecoration import org.koitharu.kotatsu.scrobbling.common.ui.selector.adapter.ScrobblerMangaSelectionDecoration
import org.koitharu.kotatsu.scrobbling.ui.selector.adapter.ScrobblerSelectorAdapter import org.koitharu.kotatsu.scrobbling.common.ui.selector.adapter.ScrobblerSelectorAdapter
import org.koitharu.kotatsu.utils.ext.assistedViewModels import org.koitharu.kotatsu.utils.ext.assistedViewModels
import org.koitharu.kotatsu.utils.ext.firstVisibleItemPosition import org.koitharu.kotatsu.utils.ext.firstVisibleItemPosition
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage

View File

@@ -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.LiveData
import androidx.lifecycle.MutableLiveData 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.list.ui.model.LoadingState
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.scrobbling.domain.Scrobbler import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga
import org.koitharu.kotatsu.scrobbling.ui.selector.model.ScrobblerHint import org.koitharu.kotatsu.scrobbling.common.ui.selector.model.ScrobblerHint
import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.printStackTraceDebug

View File

@@ -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 com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.databinding.ItemEmptyHintBinding import org.koitharu.kotatsu.databinding.ItemEmptyHintBinding
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
import org.koitharu.kotatsu.list.ui.model.ListModel 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.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.setTextAndVisible import org.koitharu.kotatsu.utils.ext.setTextAndVisible
import org.koitharu.kotatsu.utils.ext.textAndVisible import org.koitharu.kotatsu.utils.ext.textAndVisible

View File

@@ -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.content.Context
import android.graphics.Canvas import android.graphics.Canvas
@@ -8,7 +8,7 @@ import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.NO_ID import androidx.recyclerview.widget.RecyclerView.NO_ID
import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration 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 import org.koitharu.kotatsu.utils.ext.getItem
class ScrobblerMangaSelectionDecoration(context: Context) : MangaSelectionDecoration(context) { class ScrobblerMangaSelectionDecoration(context: Context) : MangaSelectionDecoration(context) {

View File

@@ -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.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil 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.loadingFooterAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
import org.koitharu.kotatsu.list.ui.model.ListModel 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.scrobbling.ui.selector.model.ScrobblerHint import org.koitharu.kotatsu.scrobbling.common.ui.selector.model.ScrobblerHint
import kotlin.jvm.internal.Intrinsics import kotlin.jvm.internal.Intrinsics
class ScrobblerSelectorAdapter( class ScrobblerSelectorAdapter(

View File

@@ -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.lifecycle.LifecycleOwner
import coil.ImageLoader import coil.ImageLoader
@@ -7,7 +7,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.databinding.ItemMangaListBinding import org.koitharu.kotatsu.databinding.ItemMangaListBinding
import org.koitharu.kotatsu.list.ui.model.ListModel 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.disposeImageRequest
import org.koitharu.kotatsu.utils.ext.enqueueWith import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest import org.koitharu.kotatsu.utils.ext.newImageRequest

View File

@@ -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.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes

View File

@@ -1,11 +0,0 @@
package org.koitharu.kotatsu.scrobbling.domain.model
enum class ScrobblingStatus {
PLANNED,
READING,
RE_READING,
COMPLETED,
ON_HOLD,
DROPPED,
}

View File

@@ -7,9 +7,9 @@ import okhttp3.Response
import okhttp3.Route import okhttp3.Route
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.network.CommonHeaders import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.scrobbling.data.ScrobblerStorage import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerType import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerType
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider import javax.inject.Provider

View File

@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.scrobbling.mal.data
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import org.koitharu.kotatsu.core.network.CommonHeaders 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" private const val JSON = "application/json"

View File

@@ -9,20 +9,18 @@ import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.util.await 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.parseJson
import org.koitharu.kotatsu.parsers.util.toIntUp import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage
import org.koitharu.kotatsu.scrobbling.data.ScrobblerRepository import org.koitharu.kotatsu.scrobbling.common.data.ScrobblingEntity
import org.koitharu.kotatsu.scrobbling.data.ScrobblerStorage import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga
import org.koitharu.kotatsu.scrobbling.data.ScrobblingEntity import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerMangaInfo
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerMangaInfo import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
import org.koitharu.kotatsu.utils.PKCEGenerator import org.koitharu.kotatsu.utils.PKCEGenerator
private const val REDIRECT_URI = "kotatsu://mal-auth" 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 BASE_API_URL = "https://api.myanimelist.net/v2"
private const val AVATAR_STUB = "https://cdn.myanimelist.net/images/questionmark_50.gif" 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 okHttp: OkHttpClient,
private val storage: ScrobblerStorage, private val storage: ScrobblerStorage,
private val db: MangaDatabase, private val db: MangaDatabase,
) : ScrobblerRepository { ) : org.koitharu.kotatsu.scrobbling.common.data.ScrobblerRepository {
private var codeVerifier: String = getPKCEChallengeCode() private var codeVerifier: String = getPKCEChallengeCode()
override val oauthUrl: String override val oauthUrl: String
get() = "$BASE_OAUTH_URL/v1/oauth2/authorize?" + get() = "$BASE_WEB_URL/v1/oauth2/authorize?" +
"response_type=code" + "response_type=code" +
"&client_id=${BuildConfig.MAL_CLIENT_ID}" + "&client_id=${BuildConfig.MAL_CLIENT_ID}" +
"&redirect_uri=$REDIRECT_URI" + "&redirect_uri=$REDIRECT_URI" +
@@ -61,7 +59,7 @@ class MALRepository(
} }
val request = Request.Builder() val request = Request.Builder()
.post(body.build()) .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() val response = okHttp.newCall(request.build()).await().parseJson()
storage.accessToken = response.getString("access_token") storage.accessToken = response.getString("access_token")
@@ -83,17 +81,15 @@ class MALRepository(
override suspend fun findManga(query: String, offset: Int): List<ScrobblerManga> { override suspend fun findManga(query: String, offset: Int): List<ScrobblerManga> {
val url = BASE_API_URL.toHttpUrl().newBuilder() val url = BASE_API_URL.toHttpUrl().newBuilder()
.addPathSegment("manga") .addPathSegment("manga")
.addQueryParameter("offset", offset.toFloat().toIntUp().toString()) .addQueryParameter("offset", offset.toString())
.addQueryParameter("nsfw", "true") .addQueryParameter("nsfw", "true")
.addQueryParameter( // WARNING! MAL API throws a 400 when the query is over 64 characters
"q", .addQueryParameter("q", query.take(64))
query.take(64)
) // WARNING! MAL API throws a 400 when the query is over 64 characters
.build() .build()
val request = Request.Builder().url(url).get().build() val request = Request.Builder().url(url).get().build()
val response = okHttp.newCall(request).await().parseJson() val response = okHttp.newCall(request).await().parseJson()
val data = response.getJSONArray("data") val data = response.getJSONArray("data")
return data.mapJSON { jsonToManga(it) } return data.mapJSONNotNull { jsonToManga(it) }
} }
override suspend fun getMangaInfo(id: Long): ScrobblerMangaInfo { override suspend fun getMangaInfo(id: Long): ScrobblerMangaInfo {
@@ -181,7 +177,7 @@ class MALRepository(
return codeVerifier return codeVerifier
} }
private fun jsonToManga(json: JSONObject): ScrobblerManga { private fun jsonToManga(json: JSONObject): ScrobblerManga? {
for (i in 0 until json.length()) { for (i in 0 until json.length()) {
val node = json.getJSONObject("node") val node = json.getJSONObject("node")
return ScrobblerManga( return ScrobblerManga(
@@ -189,23 +185,17 @@ class MALRepository(
name = node.getString("title"), name = node.getString("title"),
altName = null, altName = null,
cover = node.getJSONObject("main_picture").getString("large"), 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( return null
id = 1,
name = "",
altName = null,
cover = "",
url = ""
)
} }
private fun ScrobblerMangaInfo(json: JSONObject) = ScrobblerMangaInfo( private fun ScrobblerMangaInfo(json: JSONObject) = ScrobblerMangaInfo(
id = json.getLong("id"), id = json.getLong("id"),
name = json.getString("title"), name = json.getString("title"),
cover = json.getJSONObject("main_picture").getString("large"), 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"), descriptionHtml = json.getString("synopsis"),
) )

View File

@@ -1,12 +1,9 @@
package org.koitharu.kotatsu.scrobbling.mal.domain package org.koitharu.kotatsu.scrobbling.mal.domain
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
import org.koitharu.kotatsu.scrobbling.domain.Scrobbler import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
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.mal.data.MALRepository import org.koitharu.kotatsu.scrobbling.mal.data.MALRepository
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton

View File

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

View File

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

View File

@@ -7,9 +7,9 @@ import okhttp3.Response
import okhttp3.Route import okhttp3.Route
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.network.CommonHeaders import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.scrobbling.data.ScrobblerStorage import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerType import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerType
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider import javax.inject.Provider

View File

@@ -4,7 +4,7 @@ import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import okio.IOException import okio.IOException
import org.koitharu.kotatsu.core.network.CommonHeaders 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" private const val USER_AGENT_SHIKIMORI = "Kotatsu"

View File

@@ -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.parseJson
import org.koitharu.kotatsu.parsers.util.parseJsonArray import org.koitharu.kotatsu.parsers.util.parseJsonArray
import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl
import org.koitharu.kotatsu.scrobbling.data.ScrobblerRepository import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerRepository
import org.koitharu.kotatsu.scrobbling.data.ScrobblerStorage import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage
import org.koitharu.kotatsu.scrobbling.data.ScrobblingEntity import org.koitharu.kotatsu.scrobbling.common.data.ScrobblingEntity
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerMangaInfo import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerMangaInfo
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser
import org.koitharu.kotatsu.utils.ext.toRequestBody import org.koitharu.kotatsu.utils.ext.toRequestBody
private const val REDIRECT_URI = "kotatsu://shikimori-auth" private const val REDIRECT_URI = "kotatsu://shikimori-auth"

View File

@@ -1,9 +1,9 @@
package org.koitharu.kotatsu.scrobbling.shikimori.domain package org.koitharu.kotatsu.scrobbling.shikimori.domain
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.scrobbling.domain.Scrobbler import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingStatus import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriRepository import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriRepository
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton

View File

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

View File

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

View File

@@ -20,7 +20,8 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.local.data.CacheDir import org.koitharu.kotatsu.local.data.CacheDir
import org.koitharu.kotatsu.local.data.LocalStorageManager import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.scrobbling.anilist.data.AniListRepository 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.mal.data.MALRepository
import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriRepository import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriRepository
import org.koitharu.kotatsu.search.domain.MangaSearchRepository import org.koitharu.kotatsu.search.domain.MangaSearchRepository
@@ -130,28 +131,28 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
AppSettings.KEY_SHIKIMORI -> { AppSettings.KEY_SHIKIMORI -> {
if (!shikimoriRepository.isAuthorized) { if (!shikimoriRepository.isAuthorized) {
launchScrobblerAuth(shikimoriRepository) launchScrobblerAuth(shikimoriRepository)
true
} else { } else {
super.onPreferenceTreeClick(preference) startActivity(ScrobblerConfigActivity.newIntent(preference.context, ScrobblerService.SHIKIMORI))
} }
true
} }
AppSettings.KEY_MAL -> { AppSettings.KEY_MAL -> {
if (!malRepository.isAuthorized) { if (!malRepository.isAuthorized) {
launchScrobblerAuth(malRepository) launchScrobblerAuth(malRepository)
true
} else { } else {
super.onPreferenceTreeClick(preference) startActivity(ScrobblerConfigActivity.newIntent(preference.context, ScrobblerService.MAL))
} }
true
} }
AppSettings.KEY_ANILIST -> { AppSettings.KEY_ANILIST -> {
if (!aniListRepository.isAuthorized) { if (!aniListRepository.isAuthorized) {
launchScrobblerAuth(aniListRepository) launchScrobblerAuth(aniListRepository)
true
} else { } else {
super.onPreferenceTreeClick(preference) startActivity(ScrobblerConfigActivity.newIntent(preference.context, ScrobblerService.ANILIST))
} }
true
} }
else -> super.onPreferenceTreeClick(preference) else -> super.onPreferenceTreeClick(preference)
@@ -217,7 +218,10 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
}.show() }.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 val pref = findPreference<Preference>(key) ?: return
if (!repository.isAuthorized) { if (!repository.isAuthorized) {
pref.setSummary(R.string.disabled) 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 { runCatching {
val intent = Intent(Intent.ACTION_VIEW) val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(repository.oauthUrl) intent.data = Uri.parse(repository.oauthUrl)

View File

@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.settings
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem 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.databinding.ActivitySettingsBinding
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.parsers.model.MangaSource 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.sources.SourcesSettingsFragment
import org.koitharu.kotatsu.settings.tracker.TrackerSettingsFragment import org.koitharu.kotatsu.settings.tracker.TrackerSettingsFragment
import org.koitharu.kotatsu.utils.ext.isScrolledToTop import org.koitharu.kotatsu.utils.ext.isScrolledToTop
@@ -126,10 +122,8 @@ class SettingsActivity :
private fun openDefaultFragment() { private fun openDefaultFragment() {
val fragment = when (intent?.action) { val fragment = when (intent?.action) {
Intent.ACTION_VIEW -> handleUri(intent.data) ?: return
ACTION_READER -> ReaderSettingsFragment() ACTION_READER -> ReaderSettingsFragment()
ACTION_SUGGESTIONS -> SuggestionsSettingsFragment() ACTION_SUGGESTIONS -> SuggestionsSettingsFragment()
ACTION_SHIKIMORI -> ShikimoriSettingsFragment()
ACTION_HISTORY -> HistorySettingsFragment() ACTION_HISTORY -> HistorySettingsFragment()
ACTION_TRACKER -> TrackerSettingsFragment() ACTION_TRACKER -> TrackerSettingsFragment()
ACTION_SOURCE -> SourceSettingsFragment.newInstance( 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 { companion object {
private const val ACTION_READER = "${BuildConfig.APPLICATION_ID}.action.MANAGE_READER_SETTINGS" 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 ACTION_MANAGE_SOURCES = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCES_LIST"
private const val EXTRA_SOURCE = "source" 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 newIntent(context: Context) = Intent(context, SettingsActivity::class.java)
fun newReaderSettingsIntent(context: Context) = fun newReaderSettingsIntent(context: Context) =
Intent(context, SettingsActivity::class.java) Intent(context, SettingsActivity::class.java)
.setAction(ACTION_READER) .setAction(ACTION_READER)
fun newShikimoriSettingsIntent(context: Context) =
Intent(context, SettingsActivity::class.java)
.setAction(ACTION_SHIKIMORI)
fun newSuggestionsSettingsIntent(context: Context) = fun newSuggestionsSettingsIntent(context: Context) =
Intent(context, SettingsActivity::class.java) Intent(context, SettingsActivity::class.java)
.setAction(ACTION_SUGGESTIONS) .setAction(ACTION_SUGGESTIONS)

View File

@@ -41,8 +41,6 @@ class FeedFragment :
private val viewModel by viewModels<FeedViewModel>() private val viewModel by viewModels<FeedViewModel>()
private var feedAdapter: FeedAdapter? = null private var feedAdapter: FeedAdapter? = null
private var paddingVertical = 0
private var paddingHorizontal = 0
override fun onInflateView( override fun onInflateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@@ -57,8 +55,6 @@ class FeedFragment :
setHasFixedSize(true) setHasFixedSize(true)
addOnScrollListener(PaginationScrollListener(4, this@FeedFragment)) addOnScrollListener(PaginationScrollListener(4, this@FeedFragment))
val spacing = resources.getDimensionPixelOffset(R.dimen.list_spacing) val spacing = resources.getDimensionPixelOffset(R.dimen.list_spacing)
paddingHorizontal = spacing
paddingVertical = resources.getDimensionPixelOffset(R.dimen.grid_spacing_outer)
val decoration = TypedSpacingItemDecoration( val decoration = TypedSpacingItemDecoration(
FeedAdapter.ITEM_TYPE_FEED to 0, FeedAdapter.ITEM_TYPE_FEED to 0,
fallbackSpacing = spacing, fallbackSpacing = spacing,

View File

@@ -2,7 +2,12 @@ package org.koitharu.kotatsu.utils.ext
import android.os.SystemClock import android.os.SystemClock
import kotlinx.coroutines.delay 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> { fun <T> Flow<T>.onFirst(action: suspend (T) -> Unit): Flow<T> {
var isFirstCall = true var isFirstCall = true
@@ -11,6 +16,8 @@ fun <T> Flow<T>.onFirst(action: suspend (T) -> Unit): Flow<T> {
action(it) action(it)
isFirstCall = false isFirstCall = false
} }
}.onCompletion {
isFirstCall = true
} }
} }

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 723 B

After

Width:  |  Height:  |  Size: 723 B

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -2,6 +2,7 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:tint="?colorControlNormal"
android:viewportWidth="18" android:viewportWidth="18"
android:viewportHeight="18"> android:viewportHeight="18">
<path <path

View File

@@ -2,6 +2,7 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:tint="?colorControlNormal"
android:viewportWidth="30.72" android:viewportWidth="30.72"
android:viewportHeight="30.72"> android:viewportHeight="30.72">
<path <path

View 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" />

View 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>

View 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>

View File

@@ -416,4 +416,6 @@
<string name="theme_name_mion">Mion</string> <string name="theme_name_mion">Mion</string>
<string name="theme_name_rikka">Rikka</string> <string name="theme_name_rikka">Rikka</string>
<string name="theme_name_sakura">Sakura</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> </resources>

View File

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

View File

@@ -22,23 +22,20 @@
<PreferenceCategory android:title="@string/tracking"> <PreferenceCategory android:title="@string/tracking">
<PreferenceScreen <Preference
android:fragment="org.koitharu.kotatsu.scrobbling.shikimori.ui.ShikimoriSettingsFragment"
android:icon="@drawable/ic_shikimori"
android:key="shikimori" android:key="shikimori"
android:title="@string/shikimori" /> android:title="@string/shikimori"
app:icon="@drawable/ic_shikimori" />
<PreferenceScreen <Preference
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"
android:key="anilist" 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> </PreferenceCategory>

View File

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

View File

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