Scrobblers config activity
@@ -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"
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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}&" +
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.scrobbling.anilist.ui
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import androidx.preference.Preference
|
|
||||||
import coil.ImageLoader
|
|
||||||
import coil.request.ImageRequest
|
|
||||||
import coil.transform.CircleCropTransformation
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
|
||||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
|
||||||
import org.koitharu.kotatsu.utils.PreferenceIconTarget
|
|
||||||
import org.koitharu.kotatsu.utils.ext.assistedViewModels
|
|
||||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
|
||||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class AniListSettingsFragment : BasePreferenceFragment(R.string.anilist) {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var coil: ImageLoader
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var viewModelFactory: AniListSettingsViewModel.Factory
|
|
||||||
|
|
||||||
private val viewModel by assistedViewModels {
|
|
||||||
viewModelFactory.create(arguments?.getString(ARG_AUTH_CODE))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
|
||||||
addPreferencesFromResource(R.xml.pref_anilist)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
viewModel.user.observe(viewLifecycleOwner, this::onUserChanged)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
|
||||||
return when (preference.key) {
|
|
||||||
KEY_USER -> openAuthorization()
|
|
||||||
KEY_LOGOUT -> {
|
|
||||||
viewModel.logout()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> super.onPreferenceTreeClick(preference)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onUserChanged(user: ScrobblerUser?) {
|
|
||||||
val pref = findPreference<Preference>(KEY_USER) ?: return
|
|
||||||
pref.isSelectable = user == null
|
|
||||||
pref.title = user?.nickname ?: getString(R.string.sign_in)
|
|
||||||
ImageRequest.Builder(requireContext())
|
|
||||||
.data(user?.avatar)
|
|
||||||
.transformations(CircleCropTransformation())
|
|
||||||
.target(PreferenceIconTarget(pref))
|
|
||||||
.enqueueWith(coil)
|
|
||||||
findPreference<Preference>(KEY_LOGOUT)?.isVisible = user != null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openAuthorization(): Boolean {
|
|
||||||
return runCatching {
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW)
|
|
||||||
intent.data = Uri.parse(viewModel.authorizationUrl)
|
|
||||||
startActivity(intent)
|
|
||||||
}.isSuccess
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val KEY_USER = "al_user"
|
|
||||||
private const val KEY_LOGOUT = "al_logout"
|
|
||||||
|
|
||||||
private const val ARG_AUTH_CODE = "auth_code"
|
|
||||||
|
|
||||||
fun newInstance(authCode: String?) = AniListSettingsFragment().withArgs(1) {
|
|
||||||
putString(ARG_AUTH_CODE, authCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.scrobbling.anilist.ui
|
|
||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import dagger.assisted.Assisted
|
|
||||||
import dagger.assisted.AssistedFactory
|
|
||||||
import dagger.assisted.AssistedInject
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
|
||||||
import org.koitharu.kotatsu.scrobbling.anilist.data.AniListRepository
|
|
||||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
|
||||||
|
|
||||||
class AniListSettingsViewModel @AssistedInject constructor(
|
|
||||||
private val repository: AniListRepository,
|
|
||||||
@Assisted authCode: String?,
|
|
||||||
) : BaseViewModel() {
|
|
||||||
|
|
||||||
val authorizationUrl: String
|
|
||||||
get() = repository.oauthUrl
|
|
||||||
|
|
||||||
val user = MutableLiveData<ScrobblerUser?>()
|
|
||||||
|
|
||||||
init {
|
|
||||||
if (authCode != null) {
|
|
||||||
authorize(authCode)
|
|
||||||
} else {
|
|
||||||
loadUser()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun logout() {
|
|
||||||
launchJob(Dispatchers.Default) {
|
|
||||||
repository.logout()
|
|
||||||
user.postValue(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadUser() = launchJob(Dispatchers.Default) {
|
|
||||||
val userModel = if (repository.isAuthorized) {
|
|
||||||
repository.cachedUser?.let(user::postValue)
|
|
||||||
repository.loadUser()
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
user.postValue(userModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun authorize(code: String) = launchJob(Dispatchers.Default) {
|
|
||||||
repository.authorize(code)
|
|
||||||
user.postValue(repository.loadUser())
|
|
||||||
}
|
|
||||||
|
|
||||||
@AssistedFactory
|
|
||||||
interface Factory {
|
|
||||||
|
|
||||||
fun create(authCode: String?): AniListSettingsViewModel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package org.koitharu.kotatsu.scrobbling.data
|
package org.koitharu.kotatsu.scrobbling.common.data
|
||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
import org.koitharu.kotatsu.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 {
|
||||||
|
|
||||||
@@ -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"
|
||||||
@@ -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)
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
)
|
)
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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,
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package org.koitharu.kotatsu.scrobbling.common.domain.model
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
|
||||||
|
enum class ScrobblingStatus : ListModel {
|
||||||
|
|
||||||
|
PLANNED,
|
||||||
|
READING,
|
||||||
|
RE_READING,
|
||||||
|
COMPLETED,
|
||||||
|
ON_HOLD,
|
||||||
|
DROPPED,
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
package org.koitharu.kotatsu.scrobbling.common.ui.config
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.core.graphics.Insets
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import coil.ImageLoader
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||||
|
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||||
|
import org.koitharu.kotatsu.base.ui.list.decor.TypedSpacingItemDecoration
|
||||||
|
import org.koitharu.kotatsu.databinding.ActivityScrobblerConfigBinding
|
||||||
|
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||||
|
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
|
||||||
|
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser
|
||||||
|
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
||||||
|
import org.koitharu.kotatsu.scrobbling.common.ui.config.adapter.ScrobblingMangaAdapter
|
||||||
|
import org.koitharu.kotatsu.tracker.ui.feed.adapter.FeedAdapter
|
||||||
|
import org.koitharu.kotatsu.utils.ext.assistedViewModels
|
||||||
|
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
|
||||||
|
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||||
|
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||||
|
import org.koitharu.kotatsu.utils.ext.hideCompat
|
||||||
|
import org.koitharu.kotatsu.utils.ext.newImageRequest
|
||||||
|
import org.koitharu.kotatsu.utils.ext.showCompat
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class ScrobblerConfigActivity : BaseActivity<ActivityScrobblerConfigBinding>(),
|
||||||
|
OnListItemClickListener<ScrobblingInfo>, View.OnClickListener {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var viewModelFactory: ScrobblerConfigViewModel.Factory
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var coil: ImageLoader
|
||||||
|
|
||||||
|
private val viewModel: ScrobblerConfigViewModel by assistedViewModels {
|
||||||
|
viewModelFactory.create(requireNotNull(getScrobblerService(intent)))
|
||||||
|
}
|
||||||
|
|
||||||
|
private var paddingVertical = 0
|
||||||
|
private var paddingHorizontal = 0
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(ActivityScrobblerConfigBinding.inflate(layoutInflater))
|
||||||
|
setTitle(viewModel.titleResId)
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
|
||||||
|
val listAdapter = ScrobblingMangaAdapter(this, coil, this)
|
||||||
|
with(binding.recyclerView) {
|
||||||
|
adapter = listAdapter
|
||||||
|
setHasFixedSize(true)
|
||||||
|
val spacing = resources.getDimensionPixelOffset(R.dimen.list_spacing)
|
||||||
|
paddingHorizontal = spacing
|
||||||
|
paddingVertical = resources.getDimensionPixelOffset(R.dimen.grid_spacing_outer)
|
||||||
|
val decoration = TypedSpacingItemDecoration(
|
||||||
|
FeedAdapter.ITEM_TYPE_FEED to 0,
|
||||||
|
fallbackSpacing = spacing,
|
||||||
|
)
|
||||||
|
addItemDecoration(decoration)
|
||||||
|
}
|
||||||
|
binding.imageViewAvatar.setOnClickListener(this)
|
||||||
|
|
||||||
|
viewModel.content.observe(this, listAdapter::setItems)
|
||||||
|
viewModel.user.observe(this, this::onUserChanged)
|
||||||
|
viewModel.isLoading.observe(this, this::onLoadingStateChanged)
|
||||||
|
viewModel.onError.observe(this, this::onError)
|
||||||
|
viewModel.onLoggedOut.observe(this) {
|
||||||
|
finishAfterTransition()
|
||||||
|
}
|
||||||
|
|
||||||
|
processIntent(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent?) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
if (intent != null) {
|
||||||
|
setIntent(intent)
|
||||||
|
processIntent(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onWindowInsetsChanged(insets: Insets) {
|
||||||
|
binding.recyclerView.updatePadding(
|
||||||
|
left = insets.left + paddingHorizontal,
|
||||||
|
right = insets.right + paddingHorizontal,
|
||||||
|
bottom = insets.bottom + paddingVertical,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemClick(item: ScrobblingInfo, view: View) {
|
||||||
|
startActivity(
|
||||||
|
DetailsActivity.newIntent(this, item.mangaId),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClick(v: View) {
|
||||||
|
when (v.id) {
|
||||||
|
R.id.imageView_avatar -> showUserDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processIntent(intent: Intent) {
|
||||||
|
if (intent.action == Intent.ACTION_VIEW) {
|
||||||
|
val uri = intent.data ?: return
|
||||||
|
val code = uri.getQueryParameter("code")
|
||||||
|
if (!code.isNullOrEmpty()) {
|
||||||
|
viewModel.onAuthCodeReceived(code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onUserChanged(user: ScrobblerUser?) {
|
||||||
|
if (user == null) {
|
||||||
|
binding.imageViewAvatar.disposeImageRequest()
|
||||||
|
binding.imageViewAvatar.isVisible = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
binding.imageViewAvatar.isVisible = true
|
||||||
|
binding.imageViewAvatar.newImageRequest(user.avatar, null)
|
||||||
|
?.enqueueWith(coil)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onLoadingStateChanged(isLoading: Boolean) {
|
||||||
|
binding.progressBar.run {
|
||||||
|
if (isLoading) {
|
||||||
|
showCompat()
|
||||||
|
} else {
|
||||||
|
hideCompat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onError(e: Throwable) {
|
||||||
|
Snackbar.make(
|
||||||
|
binding.recyclerView,
|
||||||
|
e.getDisplayMessage(resources),
|
||||||
|
Snackbar.LENGTH_LONG,
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showUserDialog() {
|
||||||
|
MaterialAlertDialogBuilder(this)
|
||||||
|
.setTitle(title)
|
||||||
|
.setMessage(getString(R.string.logged_in_as, viewModel.user.value?.nickname))
|
||||||
|
.setNegativeButton(R.string.close, null)
|
||||||
|
.setPositiveButton(R.string.logout) { _, _ ->
|
||||||
|
viewModel.logout()
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val EXTRA_SERVICE_ID = "service"
|
||||||
|
|
||||||
|
private const val HOST_SHIKIMORI_AUTH = "shikimori-auth"
|
||||||
|
private const val HOST_ANILIST_AUTH = "anilist-auth"
|
||||||
|
private const val HOST_MAL_AUTH = "mal-auth"
|
||||||
|
|
||||||
|
fun newIntent(context: Context, service: ScrobblerService) =
|
||||||
|
Intent(context, ScrobblerConfigActivity::class.java)
|
||||||
|
.putExtra(EXTRA_SERVICE_ID, service.id)
|
||||||
|
|
||||||
|
private fun getScrobblerService(
|
||||||
|
intent: Intent
|
||||||
|
): ScrobblerService? {
|
||||||
|
val serviceId = intent.getIntExtra(EXTRA_SERVICE_ID, 0)
|
||||||
|
if (serviceId != 0) {
|
||||||
|
return enumValues<ScrobblerService>().first { it.id == serviceId }
|
||||||
|
}
|
||||||
|
val uri = intent.data ?: return null
|
||||||
|
return when (uri.host) {
|
||||||
|
HOST_SHIKIMORI_AUTH -> ScrobblerService.SHIKIMORI
|
||||||
|
HOST_ANILIST_AUTH -> ScrobblerService.ANILIST
|
||||||
|
HOST_MAL_AUTH -> ScrobblerService.MAL
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package org.koitharu.kotatsu.scrobbling.common.ui.config
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import kotlinx.coroutines.plus
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
|
||||||
|
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
|
||||||
|
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser
|
||||||
|
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
||||||
|
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
|
||||||
|
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||||
|
import org.koitharu.kotatsu.utils.asFlowLiveData
|
||||||
|
import org.koitharu.kotatsu.utils.ext.onFirst
|
||||||
|
|
||||||
|
class ScrobblerConfigViewModel @AssistedInject constructor(
|
||||||
|
@Assisted scrobblerService: ScrobblerService,
|
||||||
|
scrobblers: Set<@JvmSuppressWildcards Scrobbler>,
|
||||||
|
) : BaseViewModel() {
|
||||||
|
|
||||||
|
private val scrobbler = scrobblers.first { it.scrobblerService == scrobblerService }
|
||||||
|
|
||||||
|
val titleResId = scrobbler.scrobblerService.titleResId
|
||||||
|
|
||||||
|
val user = MutableLiveData<ScrobblerUser?>(null)
|
||||||
|
val onLoggedOut = SingleLiveEvent<Unit>()
|
||||||
|
|
||||||
|
val content = scrobbler.observeAllScrobblingInfo()
|
||||||
|
.onStart { loadingCounter.increment() }
|
||||||
|
.onFirst { loadingCounter.decrement() }
|
||||||
|
.catch { errorEvent.postCall(it) }
|
||||||
|
.map { buildContentList(it) }
|
||||||
|
.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
|
||||||
|
|
||||||
|
init {
|
||||||
|
scrobbler.user
|
||||||
|
.onEach { user.postValue(it) }
|
||||||
|
.launchIn(viewModelScope + Dispatchers.Default)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAuthCodeReceived(authCode: String) {
|
||||||
|
launchLoadingJob(Dispatchers.Default) {
|
||||||
|
val newUser = scrobbler.authorize(authCode)
|
||||||
|
user.postValue(newUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun logout() {
|
||||||
|
launchLoadingJob(Dispatchers.Default) {
|
||||||
|
scrobbler.logout()
|
||||||
|
user.postValue(null)
|
||||||
|
onLoggedOut.postCall(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildContentList(list: List<ScrobblingInfo>): List<ListModel> {
|
||||||
|
if (list.isEmpty()) {
|
||||||
|
return listOf(
|
||||||
|
EmptyState(
|
||||||
|
icon = R.drawable.ic_empty_history,
|
||||||
|
textPrimary = R.string.nothing_here,
|
||||||
|
textSecondary = R.string.scrobbling_empty_hint,
|
||||||
|
actionStringRes = 0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val grouped = list.groupBy { it.status }
|
||||||
|
val statuses = enumValues<ScrobblingStatus>()
|
||||||
|
val result = ArrayList<ListModel>(list.size + statuses.size)
|
||||||
|
for (st in statuses) {
|
||||||
|
val subList = grouped[st]
|
||||||
|
if (subList.isNullOrEmpty()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result.add(st)
|
||||||
|
result.addAll(subList)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory {
|
||||||
|
|
||||||
|
fun create(service: ScrobblerService): ScrobblerConfigViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package org.koitharu.kotatsu.scrobbling.common.ui.config.adapter
|
||||||
|
|
||||||
|
import android.widget.TextView
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
|
||||||
|
|
||||||
|
fun scrobblingHeaderAD() = adapterDelegate<ScrobblingStatus, ListModel>(R.layout.item_header) {
|
||||||
|
|
||||||
|
bind {
|
||||||
|
(itemView as TextView).text = context.resources
|
||||||
|
.getStringArray(R.array.scrobbling_statuses)
|
||||||
|
.getOrNull(item.ordinal)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package org.koitharu.kotatsu.scrobbling.common.ui.config.adapter
|
||||||
|
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import coil.ImageLoader
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter
|
||||||
|
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||||
|
import org.koitharu.kotatsu.databinding.ItemScrobblingMangaBinding
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
||||||
|
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
|
||||||
|
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||||
|
import org.koitharu.kotatsu.utils.ext.newImageRequest
|
||||||
|
|
||||||
|
fun scrobblingMangaAD(
|
||||||
|
clickListener: OnListItemClickListener<ScrobblingInfo>,
|
||||||
|
coil: ImageLoader,
|
||||||
|
lifecycleOwner: LifecycleOwner,
|
||||||
|
) = adapterDelegateViewBinding<ScrobblingInfo, ListModel, ItemScrobblingMangaBinding>(
|
||||||
|
{ layoutInflater, parent -> ItemScrobblingMangaBinding.inflate(layoutInflater, parent, false) },
|
||||||
|
) {
|
||||||
|
|
||||||
|
val clickListenerAdapter = AdapterDelegateClickListenerAdapter(this, clickListener)
|
||||||
|
itemView.setOnClickListener(clickListenerAdapter)
|
||||||
|
|
||||||
|
bind {
|
||||||
|
binding.imageViewCover.newImageRequest(item.coverUrl, null)?.run {
|
||||||
|
placeholder(R.drawable.ic_placeholder)
|
||||||
|
fallback(R.drawable.ic_placeholder)
|
||||||
|
error(R.drawable.ic_error_placeholder)
|
||||||
|
lifecycle(lifecycleOwner)
|
||||||
|
enqueueWith(coil)
|
||||||
|
}
|
||||||
|
binding.textViewTitle.text = item.title
|
||||||
|
binding.ratingBar.rating = item.rating * binding.ratingBar.numStars
|
||||||
|
}
|
||||||
|
|
||||||
|
onViewRecycled {
|
||||||
|
binding.imageViewCover.disposeImageRequest()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package org.koitharu.kotatsu.scrobbling.common.ui.config.adapter
|
||||||
|
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import coil.ImageLoader
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||||
|
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
||||||
|
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
|
||||||
|
|
||||||
|
class ScrobblingMangaAdapter(
|
||||||
|
clickListener: OnListItemClickListener<ScrobblingInfo>,
|
||||||
|
coil: ImageLoader,
|
||||||
|
lifecycleOwner: LifecycleOwner,
|
||||||
|
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
delegatesManager.addDelegate(scrobblingMangaAD(clickListener, coil, lifecycleOwner))
|
||||||
|
.addDelegate(scrobblingHeaderAD())
|
||||||
|
.addDelegate(emptyStateListAD(coil, null))
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
|
||||||
|
|
||||||
|
override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
|
||||||
|
return when {
|
||||||
|
oldItem is ScrobblingInfo && newItem is ScrobblingInfo -> {
|
||||||
|
oldItem.targetId == newItem.targetId && oldItem.mangaId == newItem.mangaId
|
||||||
|
}
|
||||||
|
|
||||||
|
oldItem is ScrobblingStatus && newItem is ScrobblingStatus -> {
|
||||||
|
oldItem.ordinal == newItem.ordinal
|
||||||
|
}
|
||||||
|
|
||||||
|
oldItem is EmptyState && newItem is EmptyState -> true
|
||||||
|
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.scrobbling.ui.selector
|
package org.koitharu.kotatsu.scrobbling.common.ui.selector
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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) {
|
||||||
@@ -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(
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.scrobbling.domain.model
|
|
||||||
|
|
||||||
enum class ScrobblingStatus {
|
|
||||||
|
|
||||||
PLANNED,
|
|
||||||
READING,
|
|
||||||
RE_READING,
|
|
||||||
COMPLETED,
|
|
||||||
ON_HOLD,
|
|
||||||
DROPPED,
|
|
||||||
}
|
|
||||||
@@ -7,9 +7,9 @@ import okhttp3.Response
|
|||||||
import okhttp3.Route
|
import 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
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.scrobbling.mal.ui
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import androidx.preference.Preference
|
|
||||||
import coil.ImageLoader
|
|
||||||
import coil.request.ImageRequest
|
|
||||||
import coil.transform.CircleCropTransformation
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
|
||||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
|
||||||
import org.koitharu.kotatsu.utils.PreferenceIconTarget
|
|
||||||
import org.koitharu.kotatsu.utils.ext.assistedViewModels
|
|
||||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
|
||||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class MALSettingsFragment : BasePreferenceFragment(R.string.mal) {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var coil: ImageLoader
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var viewModelFactory: MALSettingsViewModel.Factory
|
|
||||||
|
|
||||||
private val viewModel by assistedViewModels {
|
|
||||||
viewModelFactory.create(arguments?.getString(ARG_AUTH_CODE))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
|
||||||
addPreferencesFromResource(R.xml.pref_mal)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
viewModel.user.observe(viewLifecycleOwner, this::onUserChanged)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
|
||||||
return when (preference.key) {
|
|
||||||
KEY_USER -> openAuthorization()
|
|
||||||
KEY_LOGOUT -> {
|
|
||||||
viewModel.logout()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> super.onPreferenceTreeClick(preference)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onUserChanged(user: ScrobblerUser?) {
|
|
||||||
val pref = findPreference<Preference>(KEY_USER) ?: return
|
|
||||||
pref.isSelectable = user == null
|
|
||||||
pref.title = user?.nickname ?: getString(R.string.sign_in)
|
|
||||||
ImageRequest.Builder(requireContext())
|
|
||||||
.data(user?.avatar)
|
|
||||||
.transformations(CircleCropTransformation())
|
|
||||||
.target(PreferenceIconTarget(pref))
|
|
||||||
.enqueueWith(coil)
|
|
||||||
findPreference<Preference>(KEY_LOGOUT)?.isVisible = user != null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openAuthorization(): Boolean {
|
|
||||||
return runCatching {
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW)
|
|
||||||
intent.data = Uri.parse(viewModel.authorizationUrl)
|
|
||||||
startActivity(intent)
|
|
||||||
}.isSuccess
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val KEY_USER = "mal_user"
|
|
||||||
private const val KEY_LOGOUT = "mal_logout"
|
|
||||||
|
|
||||||
private const val ARG_AUTH_CODE = "auth_code"
|
|
||||||
|
|
||||||
fun newInstance(authCode: String?) = MALSettingsFragment().withArgs(1) {
|
|
||||||
putString(ARG_AUTH_CODE, authCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.scrobbling.mal.ui
|
|
||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import dagger.assisted.Assisted
|
|
||||||
import dagger.assisted.AssistedFactory
|
|
||||||
import dagger.assisted.AssistedInject
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
|
||||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
|
||||||
import org.koitharu.kotatsu.scrobbling.mal.data.MALRepository
|
|
||||||
|
|
||||||
class MALSettingsViewModel @AssistedInject constructor(
|
|
||||||
private val repository: MALRepository,
|
|
||||||
@Assisted authCode: String?,
|
|
||||||
) : BaseViewModel() {
|
|
||||||
|
|
||||||
val authorizationUrl: String
|
|
||||||
get() = repository.oauthUrl
|
|
||||||
|
|
||||||
val user = MutableLiveData<ScrobblerUser?>()
|
|
||||||
|
|
||||||
init {
|
|
||||||
if (authCode != null) {
|
|
||||||
authorize(authCode)
|
|
||||||
} else {
|
|
||||||
loadUser()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun logout() {
|
|
||||||
launchJob(Dispatchers.Default) {
|
|
||||||
repository.logout()
|
|
||||||
user.postValue(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadUser() = launchJob(Dispatchers.Default) {
|
|
||||||
val userModel = if (repository.isAuthorized) {
|
|
||||||
repository.cachedUser?.let(user::postValue)
|
|
||||||
repository.loadUser()
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
user.postValue(userModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun authorize(code: String) = launchJob(Dispatchers.Default) {
|
|
||||||
repository.authorize(code)
|
|
||||||
user.postValue(repository.loadUser())
|
|
||||||
}
|
|
||||||
|
|
||||||
@AssistedFactory
|
|
||||||
interface Factory {
|
|
||||||
|
|
||||||
fun create(authCode: String?): MALSettingsViewModel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,9 +7,9 @@ import okhttp3.Response
|
|||||||
import okhttp3.Route
|
import 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
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.scrobbling.shikimori.ui
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import androidx.preference.Preference
|
|
||||||
import coil.ImageLoader
|
|
||||||
import coil.request.ImageRequest
|
|
||||||
import coil.transform.CircleCropTransformation
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
|
||||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
|
||||||
import org.koitharu.kotatsu.utils.PreferenceIconTarget
|
|
||||||
import org.koitharu.kotatsu.utils.ext.assistedViewModels
|
|
||||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
|
||||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class ShikimoriSettingsFragment : BasePreferenceFragment(R.string.shikimori) {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var coil: ImageLoader
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var viewModelFactory: ShikimoriSettingsViewModel.Factory
|
|
||||||
|
|
||||||
private val viewModel by assistedViewModels {
|
|
||||||
viewModelFactory.create(arguments?.getString(ARG_AUTH_CODE))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
|
||||||
addPreferencesFromResource(R.xml.pref_shikimori)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
viewModel.user.observe(viewLifecycleOwner, this::onUserChanged)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
|
||||||
return when (preference.key) {
|
|
||||||
KEY_USER -> openAuthorization()
|
|
||||||
KEY_LOGOUT -> {
|
|
||||||
viewModel.logout()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> super.onPreferenceTreeClick(preference)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onUserChanged(user: ScrobblerUser?) {
|
|
||||||
val pref = findPreference<Preference>(KEY_USER) ?: return
|
|
||||||
pref.isSelectable = user == null
|
|
||||||
pref.title = user?.nickname ?: getString(R.string.sign_in)
|
|
||||||
ImageRequest.Builder(requireContext())
|
|
||||||
.data(user?.avatar)
|
|
||||||
.transformations(CircleCropTransformation())
|
|
||||||
.target(PreferenceIconTarget(pref))
|
|
||||||
.enqueueWith(coil)
|
|
||||||
findPreference<Preference>(KEY_LOGOUT)?.isVisible = user != null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openAuthorization(): Boolean {
|
|
||||||
return runCatching {
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW)
|
|
||||||
intent.data = Uri.parse(viewModel.authorizationUrl)
|
|
||||||
startActivity(intent)
|
|
||||||
}.isSuccess
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val KEY_USER = "shiki_user"
|
|
||||||
private const val KEY_LOGOUT = "shiki_logout"
|
|
||||||
|
|
||||||
private const val ARG_AUTH_CODE = "auth_code"
|
|
||||||
|
|
||||||
fun newInstance(authCode: String?) = ShikimoriSettingsFragment().withArgs(1) {
|
|
||||||
putString(ARG_AUTH_CODE, authCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.scrobbling.shikimori.ui
|
|
||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import dagger.assisted.Assisted
|
|
||||||
import dagger.assisted.AssistedFactory
|
|
||||||
import dagger.assisted.AssistedInject
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
|
||||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
|
||||||
import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriRepository
|
|
||||||
|
|
||||||
class ShikimoriSettingsViewModel @AssistedInject constructor(
|
|
||||||
private val repository: ShikimoriRepository,
|
|
||||||
@Assisted authCode: String?,
|
|
||||||
) : BaseViewModel() {
|
|
||||||
|
|
||||||
val authorizationUrl: String
|
|
||||||
get() = repository.oauthUrl
|
|
||||||
|
|
||||||
val user = MutableLiveData<ScrobblerUser?>()
|
|
||||||
|
|
||||||
init {
|
|
||||||
if (authCode != null) {
|
|
||||||
authorize(authCode)
|
|
||||||
} else {
|
|
||||||
loadUser()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun logout() {
|
|
||||||
launchJob(Dispatchers.Default) {
|
|
||||||
repository.logout()
|
|
||||||
user.postValue(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadUser() = launchJob(Dispatchers.Default) {
|
|
||||||
val userModel = if (repository.isAuthorized) {
|
|
||||||
repository.cachedUser?.let(user::postValue)
|
|
||||||
repository.loadUser()
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
user.postValue(userModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun authorize(code: String) = launchJob(Dispatchers.Default) {
|
|
||||||
repository.authorize(code)
|
|
||||||
user.postValue(repository.loadUser())
|
|
||||||
}
|
|
||||||
|
|
||||||
@AssistedFactory
|
|
||||||
interface Factory {
|
|
||||||
|
|
||||||
fun create(authCode: String?): ShikimoriSettingsViewModel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,7 +20,8 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
|
|||||||
import org.koitharu.kotatsu.local.data.CacheDir
|
import org.koitharu.kotatsu.local.data.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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 723 B After Width: | Height: | Size: 723 B |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
@@ -2,6 +2,7 @@
|
|||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
5
app/src/main/res/drawable/ic_shikimori.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<bitmap
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:src="@drawable/ic_shikimori_raw"
|
||||||
|
android:tint="?colorControlNormal" />
|
||||||
74
app/src/main/res/layout/activity_scrobbler_config.xml
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/appbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fitsSystemWindows="true">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||||
|
android:id="@+id/collapsingToolbarLayout"
|
||||||
|
style="?attr/collapsingToolbarLayoutMediumStyle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
|
||||||
|
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
|
||||||
|
app:toolbarId="@id/toolbar">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
app:layout_collapseMode="pin">
|
||||||
|
|
||||||
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
|
android:id="@+id/imageView_avatar"
|
||||||
|
android:layout_width="28dp"
|
||||||
|
android:layout_height="28dp"
|
||||||
|
android:layout_gravity="center_vertical|end"
|
||||||
|
android:layout_marginHorizontal="@dimen/toolbar_button_margin"
|
||||||
|
android:background="?selectableItemBackgroundBorderless"
|
||||||
|
android:padding="1dp"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Circle"
|
||||||
|
app:strokeColor="?colorOutline"
|
||||||
|
app:strokeWidth="1dp"
|
||||||
|
tools:src="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.MaterialToolbar>
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recyclerView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingLeft="@dimen/list_spacing"
|
||||||
|
android:paddingTop="@dimen/grid_spacing_outer"
|
||||||
|
android:paddingRight="@dimen/list_spacing"
|
||||||
|
android:paddingBottom="@dimen/grid_spacing_outer"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
|
||||||
|
tools:listitem="@layout/item_scrobbling_manga" />
|
||||||
|
|
||||||
|
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||||
|
android:id="@+id/progressBar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:indeterminate="true"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_anchor="@id/recyclerView"
|
||||||
|
app:layout_anchorGravity="center"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
51
app/src/main/res/layout/item_scrobbling_manga.xml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/list_selector"
|
||||||
|
android:clipChildren="false"
|
||||||
|
android:padding="@dimen/list_spacing">
|
||||||
|
|
||||||
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
|
android:id="@+id/imageView_cover"
|
||||||
|
android:layout_width="42dp"
|
||||||
|
android:layout_height="42dp"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
|
||||||
|
tools:src="@sample/covers" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView_title"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/ratingBar"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/imageView_cover"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/imageView_cover"
|
||||||
|
tools:text="@sample/titles" />
|
||||||
|
|
||||||
|
<RatingBar
|
||||||
|
android:id="@+id/ratingBar"
|
||||||
|
style="?ratingBarStyleSmall"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:isIndicator="true"
|
||||||
|
android:max="1"
|
||||||
|
android:numStars="5"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/imageView_cover"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/imageView_cover"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/textView_title" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -416,4 +416,6 @@
|
|||||||
<string name="theme_name_mion">Mion</string>
|
<string name="theme_name_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>
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<PreferenceScreen
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
app:initialExpandedChildrenCount="5">
|
|
||||||
|
|
||||||
<Preference
|
|
||||||
android:key="al_user"
|
|
||||||
android:persistent="false"
|
|
||||||
android:title="@string/loading_"
|
|
||||||
app:iconSpaceReserved="true" />
|
|
||||||
|
|
||||||
<Preference
|
|
||||||
android:key="al_logout"
|
|
||||||
android:persistent="false"
|
|
||||||
android:title="@string/logout"
|
|
||||||
app:allowDividerAbove="true" />
|
|
||||||
|
|
||||||
</PreferenceScreen>
|
|
||||||
@@ -22,23 +22,20 @@
|
|||||||
|
|
||||||
<PreferenceCategory android:title="@string/tracking">
|
<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>
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<PreferenceScreen
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
app:initialExpandedChildrenCount="5">
|
|
||||||
|
|
||||||
<Preference
|
|
||||||
android:key="mal_user"
|
|
||||||
android:persistent="false"
|
|
||||||
android:title="@string/loading_"
|
|
||||||
app:iconSpaceReserved="true" />
|
|
||||||
|
|
||||||
<Preference
|
|
||||||
android:key="mal_logout"
|
|
||||||
android:persistent="false"
|
|
||||||
android:title="@string/logout"
|
|
||||||
app:allowDividerAbove="true" />
|
|
||||||
|
|
||||||
</PreferenceScreen>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<PreferenceScreen
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
app:initialExpandedChildrenCount="5">
|
|
||||||
|
|
||||||
<Preference
|
|
||||||
android:key="shiki_user"
|
|
||||||
android:persistent="false"
|
|
||||||
android:title="@string/loading_"
|
|
||||||
app:iconSpaceReserved="true" />
|
|
||||||
|
|
||||||
<Preference
|
|
||||||
android:key="shiki_logout"
|
|
||||||
android:persistent="false"
|
|
||||||
android:title="@string/logout"
|
|
||||||
app:allowDividerAbove="true" />
|
|
||||||
|
|
||||||
</PreferenceScreen>
|
|
||||||