diff --git a/.gitignore b/.gitignore
index 84744a835..56cee6345 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,7 @@
/.idea/deploymentTargetDropDown.xml
/.idea/androidTestResultsUserPreferences.xml
/.idea/render.experimental.xml
+/.idea/inspectionProfiles/
.DS_Store
/build
/captures
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
deleted file mode 100644
index 38963f65d..000000000
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index 565dbcbc9..ad55f77f0 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -15,8 +15,8 @@ android {
applicationId 'org.koitharu.kotatsu'
minSdkVersion 21
targetSdkVersion 33
- versionCode 512
- versionName '4.3.1'
+ versionCode 513
+ versionName '4.3.2'
generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -86,7 +86,7 @@ afterEvaluate {
}
}
dependencies {
- implementation('com.github.KotatsuApp:kotatsu-parsers:e5a6b82853') {
+ implementation('com.github.KotatsuApp:kotatsu-parsers:7f630184c0') {
exclude group: 'org.json', module: 'json'
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt
index 1d0954317..1d67f55aa 100644
--- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt
@@ -12,6 +12,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.ActionBarContextView
import androidx.appcompat.widget.Toolbar
+import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import androidx.core.view.ViewCompat
@@ -51,12 +52,9 @@ abstract class BaseActivity :
override fun onCreate(savedInstanceState: Bundle?) {
EntryPointAccessors.fromApplication(this, BaseActivityEntryPoint::class.java).inject(this)
- val isAmoled = settings.isAmoledTheme
- val isDynamic = settings.isDynamicTheme
- when {
- isAmoled && isDynamic -> setTheme(R.style.Theme_Kotatsu_Monet_Amoled)
- isAmoled -> setTheme(R.style.Theme_Kotatsu_Amoled)
- isDynamic -> setTheme(R.style.Theme_Kotatsu_Monet)
+ setTheme(settings.colorScheme.styleResId)
+ if (settings.isAmoledTheme) {
+ setTheme(R.style.ThemeOverlay_Kotatsu_Amoled)
}
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
@@ -89,9 +87,8 @@ abstract class BaseActivity :
} else super.onOptionsItemSelected(item)
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
- if (BuildConfig.DEBUG && keyCode == KeyEvent.KEYCODE_VOLUME_UP) { // TODO remove
- // ActivityCompat.recreate(this)
- TODO("Test error")
+ if (BuildConfig.DEBUG && keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
+ ActivityCompat.recreate(this)
return true
}
return super.onKeyDown(keyCode, event)
diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseBottomSheet.kt
index 1bba104b8..947b80e8c 100644
--- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseBottomSheet.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseBottomSheet.kt
@@ -9,13 +9,13 @@ import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams
import androidx.core.view.updateLayoutParams
import androidx.viewbinding.ViewBinding
-import com.google.android.material.R as materialR
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.dialog.AppBottomSheetDialog
import org.koitharu.kotatsu.utils.ext.displayCompat
+import com.google.android.material.R as materialR
abstract class BaseBottomSheet : BottomSheetDialogFragment() {
@@ -27,6 +27,9 @@ abstract class BaseBottomSheet : BottomSheetDialogFragment() {
protected val behavior: BottomSheetBehavior<*>?
get() = (dialog as? BottomSheetDialog)?.behavior
+ val isExpanded: Boolean
+ get() = behavior?.state == BottomSheetBehavior.STATE_EXPANDED
+
final override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/BottomSheetHeaderBar.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/BottomSheetHeaderBar.kt
index cd60b79f8..80b5749bf 100644
--- a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/BottomSheetHeaderBar.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/BottomSheetHeaderBar.kt
@@ -4,10 +4,12 @@ import android.animation.LayoutTransition
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
+import android.view.Menu
import android.view.View
import android.view.ViewGroup
import android.view.WindowInsets
import androidx.annotation.AttrRes
+import androidx.annotation.MenuRes
import androidx.annotation.StringRes
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
@@ -15,16 +17,16 @@ import androidx.core.content.withStyledAttributes
import androidx.core.view.*
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
-import com.google.android.material.R as materialR
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomsheet.BottomSheetBehavior
-import java.util.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.LayoutSheetHeaderBinding
import org.koitharu.kotatsu.utils.ext.getAnimationDuration
import org.koitharu.kotatsu.utils.ext.getThemeDrawable
import org.koitharu.kotatsu.utils.ext.parents
+import java.util.*
+import com.google.android.material.R as materialR
private const val THROTTLE_DELAY = 200L
@@ -53,6 +55,9 @@ class BottomSheetHeaderBar @JvmOverloads constructor(
val toolbar: MaterialToolbar
get() = binding.toolbar
+ val menu: Menu
+ get() = binding.toolbar.menu
+
var title: CharSequence?
get() = binding.toolbar.title
set(value) {
@@ -140,6 +145,10 @@ class BottomSheetHeaderBar @JvmOverloads constructor(
binding.toolbar.invalidateMenu()
}
+ fun inflateMenu(@MenuRes resId: Int) {
+ binding.toolbar.inflateMenu(resId)
+ }
+
fun setNavigationOnClickListener(onClickListener: OnClickListener) {
binding.toolbar.setNavigationOnClickListener(onClickListener)
}
@@ -258,6 +267,7 @@ class BottomSheetHeaderBar @JvmOverloads constructor(
}
lp
}
+
else -> Toolbar.LayoutParams(params)
}
}
@@ -282,7 +292,7 @@ class BottomSheetHeaderBar @JvmOverloads constructor(
suppressLayoutCompat(false)
}
- private inner class Callback : BottomSheetBehavior.BottomSheetCallback(), View.OnClickListener {
+ private inner class Callback : BottomSheetBehavior.BottomSheetCallback(), OnClickListener {
override fun onStateChanged(bottomSheet: View, newState: Int) {
onBottomSheetStateChanged(newState)
diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/CheckableButtonGroup.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/CheckableButtonGroup.kt
deleted file mode 100644
index f159a56cd..000000000
--- a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/CheckableButtonGroup.kt
+++ /dev/null
@@ -1,119 +0,0 @@
-package org.koitharu.kotatsu.base.ui.widgets
-
-import android.content.Context
-import android.util.AttributeSet
-import android.view.View
-import android.view.ViewGroup
-import android.widget.LinearLayout
-import androidx.annotation.AttrRes
-import androidx.annotation.IdRes
-import androidx.core.view.children
-import com.google.android.material.R as materialR
-import com.google.android.material.button.MaterialButton
-import com.google.android.material.shape.ShapeAppearanceModel
-
-class CheckableButtonGroup @JvmOverloads constructor(
- context: Context,
- attrs: AttributeSet? = null,
- @AttrRes defStyleAttr: Int = materialR.attr.materialButtonToggleGroupStyle,
-) : LinearLayout(context, attrs, defStyleAttr, materialR.style.Widget_MaterialComponents_MaterialButtonToggleGroup),
- View.OnClickListener {
-
- private val originalCornerData = ArrayList()
-
- var onCheckedChangeListener: OnCheckedChangeListener? = null
-
- override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {
- if (child is MaterialButton) {
- setupButton(child)
- }
- super.addView(child, index, params)
- }
-
- override fun onFinishInflate() {
- super.onFinishInflate()
- updateChildShapes()
- }
-
- override fun onClick(v: View) {
- setCheckedId(v.id)
- }
-
- fun setCheckedId(@IdRes viewRes: Int) {
- children.forEach {
- (it as? MaterialButton)?.isChecked = it.id == viewRes
- }
- onCheckedChangeListener?.onCheckedChanged(this, viewRes)
- }
-
- private fun updateChildShapes() {
- val childCount = childCount
- val firstVisibleChildIndex = 0
- val lastVisibleChildIndex = childCount - 1
- for (i in 0 until childCount) {
- val button: MaterialButton = getChildAt(i) as? MaterialButton ?: continue
- if (button.visibility == GONE) {
- continue
- }
- val builder = button.shapeAppearanceModel.toBuilder()
- val newCornerData: CornerData? =
- getNewCornerData(i, firstVisibleChildIndex, lastVisibleChildIndex)
- updateBuilderWithCornerData(builder, newCornerData)
- button.shapeAppearanceModel = builder.build()
- }
- }
-
- private fun setupButton(button: MaterialButton) {
- button.setOnClickListener(this)
- button.isElegantTextHeight = false
- // Saves original corner data
- val shapeAppearanceModel: ShapeAppearanceModel = button.shapeAppearanceModel
- originalCornerData.add(
- CornerData(
- shapeAppearanceModel.topLeftCornerSize,
- shapeAppearanceModel.bottomLeftCornerSize,
- shapeAppearanceModel.topRightCornerSize,
- shapeAppearanceModel.bottomRightCornerSize,
- ),
- )
- }
-
- private fun getNewCornerData(
- index: Int,
- firstVisibleChildIndex: Int,
- lastVisibleChildIndex: Int,
- ): CornerData? {
- val cornerData: CornerData = originalCornerData.get(index)
-
- // If only one (visible) child exists, use its original corners
- if (firstVisibleChildIndex == lastVisibleChildIndex) {
- return cornerData
- }
- val isHorizontal = orientation == HORIZONTAL
- if (index == firstVisibleChildIndex) {
- return if (isHorizontal) cornerData.start(this) else cornerData.top()
- }
- return if (index == lastVisibleChildIndex) {
- if (isHorizontal) cornerData.end(this) else cornerData.bottom()
- } else null
- }
-
- private fun updateBuilderWithCornerData(
- shapeAppearanceModelBuilder: ShapeAppearanceModel.Builder,
- cornerData: CornerData?,
- ) {
- if (cornerData == null) {
- shapeAppearanceModelBuilder.setAllCornerSizes(0f)
- return
- }
- shapeAppearanceModelBuilder
- .setTopLeftCornerSize(cornerData.topLeft)
- .setBottomLeftCornerSize(cornerData.bottomLeft)
- .setTopRightCornerSize(cornerData.topRight)
- .setBottomRightCornerSize(cornerData.bottomRight)
- }
-
- fun interface OnCheckedChangeListener {
- fun onCheckedChanged(group: CheckableButtonGroup, checkedId: Int)
- }
-}
diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/CornerData.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/CornerData.kt
deleted file mode 100644
index 9818d1f27..000000000
--- a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/CornerData.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-package org.koitharu.kotatsu.base.ui.widgets
-
-import android.view.View
-import androidx.core.view.ViewCompat
-import com.google.android.material.shape.AbsoluteCornerSize
-import com.google.android.material.shape.CornerSize
-
-class CornerData(
- var topLeft: CornerSize,
- var bottomLeft: CornerSize,
- var topRight: CornerSize,
- var bottomRight: CornerSize,
-) {
-
- fun start(view: View): CornerData {
- return if (isLayoutRtl(view)) right() else left()
- }
-
- fun end(view: View): CornerData {
- return if (isLayoutRtl(view)) left() else right()
- }
-
- fun left(): CornerData {
- return CornerData(topLeft, bottomLeft, noCorner, noCorner)
- }
-
- fun right(): CornerData {
- return CornerData(noCorner, noCorner, topRight, bottomRight)
- }
-
- fun top(): CornerData {
- return CornerData(topLeft, noCorner, topRight, noCorner)
- }
-
- fun bottom(): CornerData {
- return CornerData(noCorner, bottomLeft, noCorner, bottomRight)
- }
-
- private companion object {
-
- val noCorner: CornerSize = AbsoluteCornerSize(0f)
-
- fun isLayoutRtl(view: View): Boolean {
- return ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_RTL
- }
- }
-}
diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ShapeView.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ShapeView.kt
new file mode 100644
index 000000000..5ec934c1e
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ShapeView.kt
@@ -0,0 +1,94 @@
+package org.koitharu.kotatsu.base.ui.widgets
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Outline
+import android.graphics.Paint
+import android.graphics.Path
+import android.util.AttributeSet
+import android.view.View
+import android.view.ViewOutlineProvider
+import androidx.core.content.withStyledAttributes
+import androidx.core.graphics.withClip
+import com.google.android.material.drawable.DrawableUtils
+import org.koitharu.kotatsu.R
+
+class ShapeView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+) : View(context, attrs, defStyleAttr) {
+
+ private val corners = FloatArray(8)
+ private val outlinePath = Path()
+ private val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG)
+
+ init {
+ context.withStyledAttributes(attrs, R.styleable.ShapeView, defStyleAttr) {
+ val cornerSize = getDimension(R.styleable.ShapeView_cornerSize, 0f)
+ corners[0] = getDimension(R.styleable.ShapeView_cornerSizeTopLeft, cornerSize)
+ corners[1] = corners[0]
+ corners[2] = getDimension(R.styleable.ShapeView_cornerSizeTopRight, cornerSize)
+ corners[3] = corners[2]
+ corners[4] = getDimension(R.styleable.ShapeView_cornerSizeBottomRight, cornerSize)
+ corners[5] = corners[4]
+ corners[6] = getDimension(R.styleable.ShapeView_cornerSizeBottomLeft, cornerSize)
+ corners[7] = corners[6]
+ strokePaint.color = getColor(R.styleable.ShapeView_strokeColor, Color.TRANSPARENT)
+ strokePaint.strokeWidth = getDimension(R.styleable.ShapeView_strokeWidth, 0f)
+ strokePaint.style = Paint.Style.STROKE
+ }
+ outlineProvider = OutlineProvider()
+ }
+
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+ super.onSizeChanged(w, h, oldw, oldh)
+ if (w != oldw || h != oldh) {
+ rebuildPath()
+ }
+ }
+
+ override fun draw(canvas: Canvas) {
+ canvas.withClip(outlinePath) {
+ super.draw(canvas)
+ }
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ super.onDraw(canvas)
+ if (strokePaint.strokeWidth > 0f) {
+ canvas.drawPath(outlinePath, strokePaint)
+ }
+ }
+
+ private fun rebuildPath() {
+ outlinePath.reset()
+ val w = width
+ val h = height
+ if (w > 0 && h > 0) {
+ outlinePath.addRoundRect(0f, 0f, w.toFloat(), h.toFloat(), corners, Path.Direction.CW)
+ }
+ }
+
+ private inner class OutlineProvider : ViewOutlineProvider() {
+
+ @SuppressLint("RestrictedApi")
+ override fun getOutline(view: View?, outline: Outline) {
+ val corner = corners[0]
+ var isRoundRect = true
+ for (item in corners) {
+ if (item != corner) {
+ isRoundRect = false
+ break
+ }
+ }
+ if (isRoundRect) {
+ outline.setRoundRect(0, 0, width, height, corner)
+ } else {
+ DrawableUtils.setOutlineToPath(outline, outlinePath)
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt
index 8e7ba77f1..e6af87f8c 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt
@@ -9,7 +9,6 @@ import androidx.collection.arraySetOf
import androidx.core.content.edit
import androidx.core.os.LocaleListCompat
import androidx.preference.PreferenceManager
-import com.google.android.material.color.DynamicColors
import dagger.hilt.android.qualifiers.ApplicationContext
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.model.ZoomMode
@@ -70,8 +69,8 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val theme: Int
get() = prefs.getString(KEY_THEME, null)?.toIntOrNull() ?: AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
- val isDynamicTheme: Boolean
- get() = DynamicColors.isDynamicColorAvailable() && prefs.getBoolean(KEY_DYNAMIC_THEME, false)
+ val colorScheme: ColorScheme
+ get() = prefs.getEnumValue(KEY_COLOR_THEME, ColorScheme.default)
val isAmoledTheme: Boolean
get() = prefs.getBoolean(KEY_THEME_AMOLED, false)
@@ -312,7 +311,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_LIST_MODE = "list_mode_2"
const val KEY_THEME = "theme"
- const val KEY_DYNAMIC_THEME = "dynamic_theme"
+ const val KEY_COLOR_THEME = "color_theme"
const val KEY_THEME_AMOLED = "amoled_theme"
const val KEY_DATE_FORMAT = "date_format"
const val KEY_SOURCES_ORDER = "sources_order_2"
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/ColorScheme.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/ColorScheme.kt
new file mode 100644
index 000000000..d0933d422
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/ColorScheme.kt
@@ -0,0 +1,40 @@
+package org.koitharu.kotatsu.core.prefs
+
+import androidx.annotation.StringRes
+import androidx.annotation.StyleRes
+import com.google.android.material.color.DynamicColors
+import org.koitharu.kotatsu.R
+
+enum class ColorScheme(
+ @StyleRes val styleResId: Int,
+ @StringRes val titleResId: Int,
+) {
+
+ DEFAULT(R.style.Theme_Kotatsu, R.string.system_default),
+ MONET(R.style.Theme_Kotatsu_Monet, R.string.theme_name_dynamic),
+ MINT(R.style.Theme_Kotatsu_Mint, R.string.theme_name_mint),
+ OCTOBER(R.style.Theme_Kotatsu_October, R.string.theme_name_october),
+ ;
+
+ companion object {
+
+ val default: ColorScheme
+ get() = if (DynamicColors.isDynamicColorAvailable()) {
+ MONET
+ } else {
+ DEFAULT
+ }
+
+ fun getAvailableList(): List {
+ val list = enumValues().toMutableList()
+ if (!DynamicColors.isDynamicColorAvailable()) {
+ list.remove(MONET)
+ }
+ return list
+ }
+
+ fun safeValueOf(name: String): ColorScheme? {
+ return enumValues().find { it.name == name }
+ }
+ }
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt
index 561ebbb36..65887cbc3 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt
@@ -42,7 +42,7 @@ class DetailsMenuProvider(
menu.findItem(R.id.action_delete).isVisible = manga?.source == MangaSource.LOCAL
menu.findItem(R.id.action_browser).isVisible = manga?.source != MangaSource.LOCAL
menu.findItem(R.id.action_shortcut).isVisible = ShortcutManagerCompat.isRequestPinShortcutSupported(activity)
- menu.findItem(R.id.action_shiki_track).isVisible = viewModel.isScrobblingAvailable
+ menu.findItem(R.id.action_scrobbling).isVisible = viewModel.isScrobblingAvailable
menu.findItem(R.id.action_favourite).setIcon(
if (viewModel.favouriteCategories.value == true) R.drawable.ic_heart else R.drawable.ic_heart_outline,
)
@@ -60,11 +60,13 @@ class DetailsMenuProvider(
}
}
}
+
R.id.action_favourite -> {
viewModel.manga.value?.let {
FavouriteCategoriesBottomSheet.show(activity.supportFragmentManager, it)
}
}
+
R.id.action_delete -> {
val title = viewModel.manga.value?.title.orEmpty()
MaterialAlertDialogBuilder(activity)
@@ -76,6 +78,7 @@ class DetailsMenuProvider(
.setNegativeButton(android.R.string.cancel, null)
.show()
}
+
R.id.action_save -> {
viewModel.manga.value?.let {
val chaptersCount = it.chapters?.size ?: 0
@@ -87,21 +90,25 @@ class DetailsMenuProvider(
}
}
}
+
R.id.action_browser -> {
viewModel.manga.value?.let {
activity.startActivity(BrowserActivity.newIntent(activity, it.publicUrl, it.title))
}
}
+
R.id.action_related -> {
viewModel.manga.value?.let {
activity.startActivity(MultiSearchActivity.newIntent(activity, it.title))
}
}
- R.id.action_shiki_track -> {
+
+ R.id.action_scrobbling -> {
viewModel.manga.value?.let {
- ScrobblingSelectorBottomSheet.show(activity.supportFragmentManager, it)
+ ScrobblingSelectorBottomSheet.show(activity.supportFragmentManager, it, null)
}
}
+
R.id.action_shortcut -> {
viewModel.manga.value?.let {
activity.lifecycleScope.launch {
@@ -112,6 +119,7 @@ class DetailsMenuProvider(
}
}
}
+
else -> return false
}
return true
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt
index d53e12f7a..e7c98d27e 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt
@@ -256,29 +256,24 @@ class DetailsViewModel @AssistedInject constructor(
}
}
- fun updateScrobbling(rating: Float, status: ScrobblingStatus?) {
- for (info in scrobblingInfo.value ?: return) {
- val scrobbler = scrobblers.first { it.scrobblerService == info.scrobbler }
- if (!scrobbler.isAvailable) continue
- launchJob(Dispatchers.Default) {
- scrobbler.updateScrobblingInfo(
- mangaId = delegate.mangaId,
- rating = rating,
- status = status,
- comment = null,
- )
- }
+ fun updateScrobbling(index: Int, rating: Float, status: ScrobblingStatus?) {
+ val scrobbler = getScrobbler(index) ?: return
+ launchJob(Dispatchers.Default) {
+ scrobbler.updateScrobblingInfo(
+ mangaId = delegate.mangaId,
+ rating = rating,
+ status = status,
+ comment = null,
+ )
}
}
- fun unregisterScrobbling() {
- for (scrobbler in scrobblers) {
- if (!scrobbler.isAvailable) continue
- launchJob(Dispatchers.Default) {
- scrobbler.unregisterScrobbling(
- mangaId = delegate.mangaId,
- )
- }
+ fun unregisterScrobbling(index: Int) {
+ val scrobbler = getScrobbler(index) ?: return
+ launchJob(Dispatchers.Default) {
+ scrobbler.unregisterScrobbling(
+ mangaId = delegate.mangaId,
+ )
}
}
@@ -315,6 +310,19 @@ class DetailsViewModel @AssistedInject constructor(
return spannable.trim()
}
+ private fun getScrobbler(index: Int): Scrobbler? {
+ val info = scrobblingInfo.value?.getOrNull(index)
+ val scrobbler = if (info != null) {
+ scrobblers.find { it.scrobblerService == info.scrobbler && it.isAvailable }
+ } else {
+ null
+ }
+ if (scrobbler == null) {
+ errorEvent.call(IllegalStateException("Scrobbler [$index] is not available"))
+ }
+ return scrobbler
+ }
+
@AssistedFactory
interface Factory {
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoBottomSheet.kt
index f5f8510e5..39fd5fd45 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoBottomSheet.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoBottomSheet.kt
@@ -15,9 +15,7 @@ import androidx.core.net.toUri
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.activityViewModels
import coil.ImageLoader
-import coil.request.ImageRequest
import dagger.hilt.android.AndroidEntryPoint
-import javax.inject.Inject
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
import org.koitharu.kotatsu.databinding.SheetScrobblingBinding
@@ -26,7 +24,12 @@ import org.koitharu.kotatsu.image.ui.ImageActivity
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingInfo
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingStatus
import org.koitharu.kotatsu.scrobbling.ui.selector.ScrobblingSelectorBottomSheet
-import org.koitharu.kotatsu.utils.ext.*
+import org.koitharu.kotatsu.utils.ext.enqueueWith
+import org.koitharu.kotatsu.utils.ext.getDisplayMessage
+import org.koitharu.kotatsu.utils.ext.newImageRequest
+import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf
+import org.koitharu.kotatsu.utils.ext.withArgs
+import javax.inject.Inject
@AndroidEntryPoint
class ScrobblingInfoBottomSheet :
@@ -41,6 +44,7 @@ class ScrobblingInfoBottomSheet :
@Inject
lateinit var coil: ImageLoader
+
private var menu: PopupMenu? = null
override fun onCreate(savedInstanceState: Bundle?) {
@@ -78,6 +82,7 @@ class ScrobblingInfoBottomSheet :
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
viewModel.updateScrobbling(
+ index = scrobblerIndex,
rating = binding.ratingBar.rating / binding.ratingBar.numStars,
status = enumValues().getOrNull(position),
)
@@ -88,6 +93,7 @@ class ScrobblingInfoBottomSheet :
override fun onRatingChanged(ratingBar: RatingBar, rating: Float, fromUser: Boolean) {
if (fromUser) {
viewModel.updateScrobbling(
+ index = scrobblerIndex,
rating = rating / ratingBar.numStars,
status = enumValues().getOrNull(binding.spinnerStatus.selectedItemPosition),
)
@@ -115,15 +121,15 @@ class ScrobblingInfoBottomSheet :
binding.ratingBar.rating = scrobbling.rating * binding.ratingBar.numStars
binding.textViewDescription.text = scrobbling.description
binding.spinnerStatus.setSelection(scrobbling.status?.ordinal ?: -1)
- ImageRequest.Builder(context ?: return)
- .target(binding.imageViewCover)
- .data(scrobbling.coverUrl)
- .crossfade(context)
- .lifecycle(viewLifecycleOwner)
- .placeholder(R.drawable.ic_placeholder)
- .fallback(R.drawable.ic_placeholder)
- .error(R.drawable.ic_error_placeholder)
- .enqueueWith(coil)
+ binding.imageViewLogo.contentDescription = getString(scrobbling.scrobbler.titleResId)
+ binding.imageViewLogo.setImageResource(scrobbling.scrobbler.iconResId)
+ binding.imageViewCover.newImageRequest(scrobbling.coverUrl)?.apply {
+ lifecycle(viewLifecycleOwner)
+ placeholder(R.drawable.ic_placeholder)
+ fallback(R.drawable.ic_placeholder)
+ error(R.drawable.ic_error_placeholder)
+ enqueueWith(coil)
+ }
}
override fun onMenuItemClick(item: MenuItem): Boolean {
@@ -135,13 +141,16 @@ class ScrobblingInfoBottomSheet :
Intent.createChooser(intent, getString(R.string.open_in_browser)),
)
}
+
R.id.action_unregister -> {
- viewModel.unregisterScrobbling()
+ viewModel.unregisterScrobbling(scrobblerIndex)
dismiss()
}
+
R.id.action_edit -> {
val manga = viewModel.manga.value ?: return false
- ScrobblingSelectorBottomSheet.show(parentFragmentManager, manga)
+ val scrobblerService = viewModel.scrobblingInfo.value?.getOrNull(scrobblerIndex)?.scrobbler
+ ScrobblingSelectorBottomSheet.show(parentFragmentManager, manga, scrobblerService)
dismiss()
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/data/ScrobblerStorage.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/data/ScrobblerStorage.kt
index 953f6eec9..c5f7973c5 100644
--- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/data/ScrobblerStorage.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/data/ScrobblerStorage.kt
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.scrobbling.data
import android.content.Context
import androidx.core.content.edit
+import org.jsoup.internal.StringUtil.StringJoiner
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
@@ -39,12 +40,12 @@ class ScrobblerStorage(context: Context, service: ScrobblerService) {
remove(KEY_USER)
return@edit
}
- val str = buildString {
- appendLine(value.id)
- appendLine(value.nickname)
- appendLine(value.avatar)
- appendLine(value.service.name)
- }
+ val str = StringJoiner("\n")
+ .add(value.id)
+ .add(value.nickname)
+ .add(value.avatar)
+ .add(value.service.name)
+ .complete()
putString(KEY_USER, str)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/ScrobblingSelectorBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/ScrobblingSelectorBottomSheet.kt
index 8b7228ea6..0cffa3422 100644
--- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/ScrobblingSelectorBottomSheet.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/ScrobblingSelectorBottomSheet.kt
@@ -3,16 +3,18 @@ package org.koitharu.kotatsu.scrobbling.ui.selector
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
-import android.view.*
-import android.widget.AdapterView
-import android.widget.ArrayAdapter
+import android.view.KeyEvent
+import android.view.LayoutInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
import android.widget.Toast
import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentManager
import coil.ImageLoader
+import com.google.android.material.tabs.TabLayout
import dagger.hilt.android.AndroidEntryPoint
-import javax.inject.Inject
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaIntent
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
@@ -20,14 +22,18 @@ import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.databinding.SheetScrobblingSelectorBinding
+import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga
+import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.ui.selector.adapter.ScrobblerMangaSelectionDecoration
import org.koitharu.kotatsu.scrobbling.ui.selector.adapter.ScrobblerSelectorAdapter
import org.koitharu.kotatsu.utils.ext.assistedViewModels
+import org.koitharu.kotatsu.utils.ext.firstVisibleItemPosition
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.requireParcelable
import org.koitharu.kotatsu.utils.ext.withArgs
+import javax.inject.Inject
@AndroidEntryPoint
class ScrobblingSelectorBottomSheet :
@@ -38,7 +44,8 @@ class ScrobblingSelectorBottomSheet :
MenuItem.OnActionExpandListener,
SearchView.OnQueryTextListener,
DialogInterface.OnKeyListener,
- AdapterView.OnItemSelectedListener {
+ TabLayout.OnTabSelectedListener,
+ ListStateHolderListener {
@Inject
lateinit var viewModelFactory: ScrobblingSelectorViewModel.Factory
@@ -64,7 +71,7 @@ class ScrobblingSelectorBottomSheet :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- val listAdapter = ScrobblerSelectorAdapter(viewLifecycleOwner, coil, this)
+ val listAdapter = ScrobblerSelectorAdapter(viewLifecycleOwner, coil, this, this)
val decoration = ScrobblerMangaSelectionDecoration(view.context)
with(binding.recyclerView) {
adapter = listAdapter
@@ -73,7 +80,7 @@ class ScrobblingSelectorBottomSheet :
}
binding.buttonDone.setOnClickListener(this)
initOptionsMenu()
- initSpinner()
+ initTabs()
viewModel.content.observe(viewLifecycleOwner) { listAdapter.items = it }
viewModel.selectedItemId.observe(viewLifecycleOwner) {
@@ -99,6 +106,12 @@ class ScrobblingSelectorBottomSheet :
viewModel.selectedItemId.value = item.id
}
+ override fun onRetryClick(error: Throwable) = Unit
+
+ override fun onEmptyActionClick() {
+ openSearch()
+ }
+
override fun onScrolledToEnd() {
viewModel.loadList(append = true)
}
@@ -120,7 +133,7 @@ class ScrobblingSelectorBottomSheet :
return false
}
viewModel.search(query)
- binding.headerBar.toolbar.menu.findItem(R.id.action_search)?.collapseActionView()
+ binding.headerBar.menu.findItem(R.id.action_search)?.collapseActionView()
return true
}
@@ -128,7 +141,7 @@ class ScrobblingSelectorBottomSheet :
override fun onKey(dialog: DialogInterface?, keyCode: Int, event: KeyEvent?): Boolean {
if (keyCode == KeyEvent.KEYCODE_BACK) {
- val menuItem = binding.headerBar.toolbar.menu.findItem(R.id.action_search) ?: return false
+ val menuItem = binding.headerBar.menu.findItem(R.id.action_search) ?: return false
if (menuItem.isActionViewExpanded) {
if (event?.action == KeyEvent.ACTION_UP) {
menuItem.collapseActionView()
@@ -139,11 +152,23 @@ class ScrobblingSelectorBottomSheet :
return false
}
- override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
- viewModel.setScrobblerIndex(position)
+ override fun onTabSelected(tab: TabLayout.Tab) {
+ viewModel.setScrobblerIndex(tab.position)
}
- override fun onNothingSelected(parent: AdapterView<*>?) = Unit
+ override fun onTabUnselected(tab: TabLayout.Tab?) = Unit
+
+ override fun onTabReselected(tab: TabLayout.Tab?) {
+ if (!isExpanded) {
+ setExpanded(isExpanded = true, isLocked = behavior?.isDraggable == false)
+ }
+ binding.recyclerView.firstVisibleItemPosition = 0
+ }
+
+ private fun openSearch() {
+ val menuItem = binding.headerBar.menu.findItem(R.id.action_search) ?: return
+ menuItem.expandActionView()
+ }
private fun onError(e: Throwable) {
Toast.makeText(requireContext(), e.getDisplayMessage(resources), Toast.LENGTH_LONG).show()
@@ -153,8 +178,8 @@ class ScrobblingSelectorBottomSheet :
}
private fun initOptionsMenu() {
- binding.headerBar.toolbar.inflateMenu(R.menu.opt_shiki_selector)
- val searchMenuItem = binding.headerBar.toolbar.menu.findItem(R.id.action_search)
+ binding.headerBar.inflateMenu(R.menu.opt_shiki_selector)
+ val searchMenuItem = binding.headerBar.menu.findItem(R.id.action_search)
searchMenuItem.setOnActionExpandListener(this)
val searchView = searchMenuItem.actionView as SearchView
searchView.setOnQueryTextListener(this)
@@ -162,28 +187,41 @@ class ScrobblingSelectorBottomSheet :
searchView.queryHint = searchMenuItem.title
}
- private fun initSpinner() {
+ private fun initTabs() {
val entries = viewModel.availableScrobblers
+ val tabs = binding.tabs
if (entries.size <= 1) {
- binding.spinnerScrobblers.isVisible = false
+ tabs.isVisible = false
return
}
- val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, entries)
- adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
- binding.spinnerScrobblers.adapter = adapter
- viewModel.selectedScrobblerIndex.observe(viewLifecycleOwner) {
- binding.spinnerScrobblers.setSelection(it)
+ val selectedId = arguments?.getInt(ARG_SCROBBLER, -1) ?: -1
+ tabs.removeAllTabs()
+ tabs.clearOnTabSelectedListeners()
+ tabs.addOnTabSelectedListener(this)
+ for (entry in entries) {
+ val tab = tabs.newTab()
+ tab.tag = entry.scrobblerService
+ tab.setIcon(entry.scrobblerService.iconResId)
+ tab.setText(entry.scrobblerService.titleResId)
+ tabs.addTab(tab)
+ if (entry.scrobblerService.id == selectedId) {
+ tab.select()
+ }
}
- binding.spinnerScrobblers.onItemSelectedListener = this
+ tabs.isVisible = true
}
companion object {
private const val TAG = "ScrobblingSelectorBottomSheet"
+ private const val ARG_SCROBBLER = "scrobbler"
- fun show(fm: FragmentManager, manga: Manga) =
- ScrobblingSelectorBottomSheet().withArgs(1) {
+ fun show(fm: FragmentManager, manga: Manga, scrobblerService: ScrobblerService?) =
+ ScrobblingSelectorBottomSheet().withArgs(2) {
putParcelable(MangaIntent.KEY_MANGA, ParcelableManga(manga, withChapters = false))
+ if (scrobblerService != null) {
+ putInt(ARG_SCROBBLER, scrobblerService.id)
+ }
}.show(fm, TAG)
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/ScrobblingSelectorViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/ScrobblingSelectorViewModel.kt
index a9509e2b7..cb926248f 100644
--- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/ScrobblingSelectorViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/ScrobblingSelectorViewModel.kt
@@ -12,7 +12,9 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
+import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseViewModel
+import org.koitharu.kotatsu.list.ui.model.EmptyHint
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
import org.koitharu.kotatsu.list.ui.model.LoadingState
@@ -46,7 +48,7 @@ class ScrobblingSelectorViewModel @AssistedInject constructor(
hasNextPage,
) { list, isHasNextPage ->
when {
- list.isEmpty() -> listOf()
+ list.isEmpty() -> listOf(emptyResultsHint())
isHasNextPage -> list + LoadingFooter
else -> list
}
@@ -125,6 +127,13 @@ class ScrobblingSelectorViewModel @AssistedInject constructor(
}
}
+ private fun emptyResultsHint() = EmptyHint(
+ icon = R.drawable.ic_empty_history,
+ textPrimary = R.string.nothing_found,
+ textSecondary = R.string.text_search_holder_secondary,
+ actionStringRes = R.string.search,
+ )
+
@AssistedFactory
interface Factory {
diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/adapter/ScrobblerSelectorAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/adapter/ScrobblerSelectorAdapter.kt
index bb61e79ca..cf1bd5aeb 100644
--- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/adapter/ScrobblerSelectorAdapter.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/adapter/ScrobblerSelectorAdapter.kt
@@ -4,23 +4,27 @@ import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
-import kotlin.jvm.internal.Intrinsics
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
+import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
+import org.koitharu.kotatsu.list.ui.adapter.emptyHintAD
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga
+import kotlin.jvm.internal.Intrinsics
class ScrobblerSelectorAdapter(
lifecycleOwner: LifecycleOwner,
coil: ImageLoader,
clickListener: OnListItemClickListener,
+ stateHolderListener: ListStateHolderListener,
) : AsyncListDifferDelegationAdapter(DiffCallback()) {
init {
delegatesManager.addDelegate(loadingStateAD())
- .addDelegate(scrobblerMangaAD(lifecycleOwner, coil, clickListener))
+ .addDelegate(scrobblingMangaAD(lifecycleOwner, coil, clickListener))
.addDelegate(loadingFooterAD())
+ .addDelegate(emptyHintAD(stateHolderListener))
}
private class DiffCallback : DiffUtil.ItemCallback() {
diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/adapter/ScrobblerMangaAD.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/adapter/ScrobblingMangaAD.kt
similarity index 98%
rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/adapter/ScrobblerMangaAD.kt
rename to app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/adapter/ScrobblingMangaAD.kt
index 20232633e..73723229a 100644
--- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/adapter/ScrobblerMangaAD.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/adapter/ScrobblingMangaAD.kt
@@ -13,7 +13,7 @@ import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest
import org.koitharu.kotatsu.utils.ext.textAndVisible
-fun scrobblerMangaAD(
+fun scrobblingMangaAD(
lifecycleOwner: LifecycleOwner,
coil: ImageLoader,
clickListener: OnListItemClickListener,
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt
index c4103e685..5c1c6ff27 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt
@@ -14,7 +14,6 @@ import androidx.core.view.postDelayed
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.TwoStatePreference
-import com.google.android.material.color.DynamicColors
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
@@ -56,7 +55,6 @@ class AppearanceSettingsFragment :
entryValues = ListMode.values().names()
setDefaultValueCompat(ListMode.GRID.name)
}
- findPreference(AppSettings.KEY_DYNAMIC_THEME)?.isVisible = DynamicColors.isDynamicColorAvailable()
findPreference(AppSettings.KEY_DATE_FORMAT)?.run {
entryValues = resources.getStringArray(R.array.date_formats)
val now = Date().time
@@ -105,10 +103,7 @@ class AppearanceSettingsFragment :
AppCompatDelegate.setDefaultNightMode(settings.theme)
}
- AppSettings.KEY_DYNAMIC_THEME -> {
- postRestart()
- }
-
+ AppSettings.KEY_COLOR_THEME,
AppSettings.KEY_THEME_AMOLED -> {
postRestart()
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt
index 8fca72bbb..beccdf6f5 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt
@@ -9,7 +9,9 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
@@ -18,12 +20,14 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.local.data.CacheDir
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.scrobbling.anilist.data.AniListRepository
+import org.koitharu.kotatsu.scrobbling.data.ScrobblerRepository
import org.koitharu.kotatsu.scrobbling.mal.data.MALRepository
import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriRepository
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.FileSize
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
+import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
import javax.inject.Inject
@@ -82,9 +86,9 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
override fun onResume() {
super.onResume()
- bindShikimoriSummary()
- bindMALSummary()
- bindAniListSummary()
+ bindScrobblerSummary(AppSettings.KEY_SHIKIMORI, shikimoriRepository)
+ bindScrobblerSummary(AppSettings.KEY_ANILIST, aniListRepository)
+ bindScrobblerSummary(AppSettings.KEY_MAL, malRepository)
}
override fun onPreferenceTreeClick(preference: Preference): Boolean {
@@ -125,7 +129,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
AppSettings.KEY_SHIKIMORI -> {
if (!shikimoriRepository.isAuthorized) {
- launchShikimoriAuth()
+ launchScrobblerAuth(shikimoriRepository)
true
} else {
super.onPreferenceTreeClick(preference)
@@ -134,7 +138,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
AppSettings.KEY_MAL -> {
if (!malRepository.isAuthorized) {
- launchMALAuth()
+ launchScrobblerAuth(malRepository)
true
} else {
super.onPreferenceTreeClick(preference)
@@ -143,7 +147,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
AppSettings.KEY_ANILIST -> {
if (!aniListRepository.isAuthorized) {
- launchAniListAuth()
+ launchScrobblerAuth(aniListRepository)
true
} else {
super.onPreferenceTreeClick(preference)
@@ -213,54 +217,35 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
}.show()
}
- private fun bindShikimoriSummary() {
- findPreference(AppSettings.KEY_SHIKIMORI)?.summary = if (shikimoriRepository.isAuthorized) {
- getString(R.string.logged_in_as, shikimoriRepository.cachedUser?.nickname)
+ private fun bindScrobblerSummary(key: String, repository: ScrobblerRepository) {
+ val pref = findPreference(key) ?: return
+ if (!repository.isAuthorized) {
+ pref.setSummary(R.string.disabled)
+ return
+ }
+ val username = repository.cachedUser?.nickname
+ if (username != null) {
+ pref.summary = getString(R.string.logged_in_as, username)
} else {
- getString(R.string.disabled)
+ pref.setSummary(R.string.loading_)
+ viewLifecycleScope.launch {
+ pref.summary = withContext(Dispatchers.Default) {
+ runCatching {
+ val user = repository.loadUser()
+ getString(R.string.logged_in_as, user.nickname)
+ }.getOrElse {
+ it.printStackTraceDebug()
+ it.getDisplayMessage(resources)
+ }
+ }
+ }
}
}
- private fun bindAniListSummary() {
- findPreference(AppSettings.KEY_ANILIST)?.summary = if (aniListRepository.isAuthorized) {
- getString(R.string.logged_in_as, aniListRepository.cachedUser?.nickname)
- } else {
- getString(R.string.disabled)
- }
- }
-
- private fun launchShikimoriAuth() {
+ private fun launchScrobblerAuth(repository: ScrobblerRepository) {
runCatching {
val intent = Intent(Intent.ACTION_VIEW)
- intent.data = Uri.parse(shikimoriRepository.oauthUrl)
- startActivity(intent)
- }.onFailure {
- Snackbar.make(listView, it.getDisplayMessage(resources), Snackbar.LENGTH_LONG).show()
- }
- }
-
- private fun bindMALSummary() {
- findPreference(AppSettings.KEY_MAL)?.summary = if (malRepository.isAuthorized) {
- getString(R.string.logged_in_as, malRepository.cachedUser?.nickname)
- } else {
- getString(R.string.disabled)
- }
- }
-
- private fun launchMALAuth() {
- runCatching {
- val intent = Intent(Intent.ACTION_VIEW)
- intent.data = Uri.parse(malRepository.oauthUrl)
- startActivity(intent)
- }.onFailure {
- Snackbar.make(listView, it.getDisplayMessage(resources), Snackbar.LENGTH_LONG).show()
- }
- }
-
- private fun launchAniListAuth() {
- runCatching {
- val intent = Intent(Intent.ACTION_VIEW)
- intent.data = Uri.parse(aniListRepository.oauthUrl)
+ intent.data = Uri.parse(repository.oauthUrl)
startActivity(intent)
}.onFailure {
Snackbar.make(listView, it.getDisplayMessage(resources), Snackbar.LENGTH_LONG).show()
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/utils/ThemeChooserPreference.kt b/app/src/main/java/org/koitharu/kotatsu/settings/utils/ThemeChooserPreference.kt
new file mode 100644
index 000000000..bd417fe36
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/utils/ThemeChooserPreference.kt
@@ -0,0 +1,84 @@
+package org.koitharu.kotatsu.settings.utils
+
+import android.content.Context
+import android.content.res.TypedArray
+import android.os.Build
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.LinearLayout
+import androidx.appcompat.view.ContextThemeWrapper
+import androidx.core.view.isVisible
+import androidx.preference.Preference
+import androidx.preference.PreferenceViewHolder
+import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.core.prefs.ColorScheme
+import org.koitharu.kotatsu.databinding.ItemColorSchemeBinding
+
+class ThemeChooserPreference @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = R.attr.themeChooserPreferenceStyle,
+ defStyleRes: Int = R.style.Preference_ThemeChooser,
+) : Preference(context, attrs, defStyleAttr, defStyleRes) {
+
+ private val entries = ColorScheme.getAvailableList()
+ private var currentValue: ColorScheme = ColorScheme.default
+ private val itemClickListener = View.OnClickListener {
+ val tag = it.tag as? ColorScheme ?: return@OnClickListener
+ setValueInternal(tag.name, true)
+ }
+
+ var value: String
+ get() = currentValue.name
+ set(value) = setValueInternal(value, notifyChanged = true)
+
+ override fun onBindViewHolder(holder: PreferenceViewHolder) {
+ super.onBindViewHolder(holder)
+ val layout = holder.findViewById(R.id.linear) as? LinearLayout ?: return
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ layout.suppressLayout(true)
+ }
+ layout.removeAllViews()
+ for (theme in entries) {
+ val context = ContextThemeWrapper(context, theme.styleResId)
+ val item = ItemColorSchemeBinding.inflate(LayoutInflater.from(context), layout, false)
+ item.card.isChecked = theme == currentValue
+ item.textViewTitle.setText(theme.titleResId)
+ item.root.tag = theme
+ item.card.tag = theme
+ item.imageViewCheck.isVisible = theme == currentValue
+ item.root.setOnClickListener(itemClickListener)
+ item.card.setOnClickListener(itemClickListener)
+ layout.addView(item.root)
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ layout.suppressLayout(false)
+ }
+ }
+
+ override fun onSetInitialValue(defaultValue: Any?) {
+ value = getPersistedString(
+ when (defaultValue) {
+ is String -> ColorScheme.safeValueOf(defaultValue) ?: ColorScheme.default
+ is ColorScheme -> defaultValue
+ else -> ColorScheme.default
+ }.name,
+ )
+ }
+
+ override fun onGetDefaultValue(a: TypedArray, index: Int): Any {
+ return a.getInt(index, 0)
+ }
+
+ private fun setValueInternal(enumName: String, notifyChanged: Boolean) {
+ val newValue = ColorScheme.safeValueOf(enumName) ?: return
+ if (newValue != currentValue) {
+ currentValue = newValue
+ persistString(newValue.name)
+ if (notifyChanged) {
+ notifyChanged()
+ }
+ }
+ }
+}
diff --git a/app/src/main/res/layout/item_color_scheme.xml b/app/src/main/res/layout/item_color_scheme.xml
new file mode 100644
index 000000000..d02b92e89
--- /dev/null
+++ b/app/src/main/res/layout/item_color_scheme.xml
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/preference_theme.xml b/app/src/main/res/layout/preference_theme.xml
new file mode 100644
index 000000000..ee3e59e47
--- /dev/null
+++ b/app/src/main/res/layout/preference_theme.xml
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/sheet_scrobbling.xml b/app/src/main/res/layout/sheet_scrobbling.xml
index 05e24fc5a..3cfaaf4fd 100644
--- a/app/src/main/res/layout/sheet_scrobbling.xml
+++ b/app/src/main/res/layout/sheet_scrobbling.xml
@@ -8,7 +8,8 @@
+ android:layout_height="wrap_content"
+ android:paddingBottom="16dp">
+
+
-
+ android:visibility="gone"
+ app:tabGravity="start"
+ tools:visibility="visible" />
diff --git a/app/src/main/res/menu/opt_details.xml b/app/src/main/res/menu/opt_details.xml
index 7f5d3445f..5aa52c579 100644
--- a/app/src/main/res/menu/opt_details.xml
+++ b/app/src/main/res/menu/opt_details.xml
@@ -32,7 +32,7 @@
app:showAsAction="never" />
diff --git a/app/src/main/res/values-night-v23/color_themes.xml b/app/src/main/res/values-night-v23/color_themes.xml
new file mode 100644
index 000000000..cb3b95d88
--- /dev/null
+++ b/app/src/main/res/values-night-v23/color_themes.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
index 8696139cf..931057c26 100644
--- a/app/src/main/res/values-night/themes.xml
+++ b/app/src/main/res/values-night/themes.xml
@@ -3,14 +3,9 @@
-
-
-
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values-v23/bools.xml b/app/src/main/res/values-v23/bools.xml
new file mode 100644
index 000000000..22d1802ff
--- /dev/null
+++ b/app/src/main/res/values-v23/bools.xml
@@ -0,0 +1,4 @@
+
+
+ true
+
diff --git a/app/src/main/res/values-v23/color_themes.xml b/app/src/main/res/values-v23/color_themes.xml
new file mode 100644
index 000000000..ee818955e
--- /dev/null
+++ b/app/src/main/res/values-v23/color_themes.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
index bddae4f97..a6b7770b3 100644
--- a/app/src/main/res/values/attrs.xml
+++ b/app/src/main/res/values/attrs.xml
@@ -4,6 +4,7 @@
+
@@ -75,4 +76,14 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/bools.xml b/app/src/main/res/values/bools.xml
index 79442ec80..00dba4d3f 100644
--- a/app/src/main/res/values/bools.xml
+++ b/app/src/main/res/values/bools.xml
@@ -4,4 +4,5 @@
true
false
true
+ false
diff --git a/app/src/main/res/values/color_themes.xml b/app/src/main/res/values/color_themes.xml
new file mode 100644
index 000000000..a3c29c58b
--- /dev/null
+++ b/app/src/main/res/values/color_themes.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 085f1668a..62fa87ed3 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -408,4 +408,8 @@
Enable logging
Record some actions for debug purposes
Show suspicious content
+ Mint
+ Dynamic
+ Color scheme
+ October
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index a1c298b32..6939659cb 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -255,6 +255,11 @@
- @layout/preference_widget_material_switch
+
+