Merge branch 'devel' into feature/mal

This commit is contained in:
Koitharu
2023-02-03 19:48:32 +02:00
38 changed files with 785 additions and 354 deletions

1
.gitignore vendored
View File

@@ -15,6 +15,7 @@
/.idea/deploymentTargetDropDown.xml
/.idea/androidTestResultsUserPreferences.xml
/.idea/render.experimental.xml
/.idea/inspectionProfiles/
.DS_Store
/build
/captures

View File

@@ -1,17 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="BooleanLiteralArgument" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="Destructure" enabled="true" level="INFO" enabled_by_default="true" />
<inspection_tool class="FillClass" enabled="true" level="INFORMATION" enabled_by_default="true">
<option name="withoutDefaultValues" value="true" />
</inspection_tool>
<inspection_tool class="KeySetIterationMayUseEntrySet" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="KotlinFunctionArgumentsHelper" enabled="true" level="INFORMATION" enabled_by_default="true">
<option name="withoutDefaultValues" value="true" />
</inspection_tool>
<inspection_tool class="ReplaceCollectionCountWithSize" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="TrailingComma" enabled="true" level="INFORMATION" enabled_by_default="true" />
<inspection_tool class="ZeroLengthArrayInitialization" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

View File

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

View File

@@ -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<B : ViewBinding> :
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<B : ViewBinding> :
} 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)

View File

@@ -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<B : ViewBinding> : BottomSheetDialogFragment() {
@@ -27,6 +27,9 @@ abstract class BaseBottomSheet<B : ViewBinding> : 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?,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<ColorScheme> {
val list = enumValues<ColorScheme>().toMutableList()
if (!DynamicColors.isDynamicColorAvailable()) {
list.remove(MONET)
}
return list
}
fun safeValueOf(name: String): ColorScheme? {
return enumValues<ColorScheme>().find { it.name == name }
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<ScrobblerManga>,
stateHolderListener: ListStateHolderListener,
) : AsyncListDifferDelegationAdapter<ListModel>(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<ListModel>() {

View File

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

View File

@@ -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<Preference>(AppSettings.KEY_DYNAMIC_THEME)?.isVisible = DynamicColors.isDynamicColorAvailable()
findPreference<ListPreference>(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()
}

View File

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

View File

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

View File

@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
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="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:orientation="vertical"
android:padding="6dp"
tools:theme="@style/Theme.Kotatsu.Mint">
<com.google.android.material.card.MaterialCardView
android:id="@+id/card"
style="?materialCardViewFilledStyle"
android:layout_width="@dimen/widget_cover_width"
android:layout_height="@dimen/widget_cover_height"
android:focusableInTouchMode="false">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="6dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Abc"
android:textSize="12sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="HardcodedText" />
<org.koitharu.kotatsu.base.ui.widgets.ShapeView
android:id="@+id/shape_1"
android:layout_width="0dp"
android:layout_height="6dp"
android:layout_marginBottom="6dp"
android:background="?colorSecondary"
app:cornerSize="4dp"
app:layout_constraintBottom_toTopOf="@id/shape_2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintWidth_percent="0.4" />
<org.koitharu.kotatsu.base.ui.widgets.ShapeView
android:id="@+id/shape_2"
android:layout_width="0dp"
android:layout_height="6dp"
android:background="?colorSecondary"
app:cornerSize="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.65"
app:layout_constraintWidth_percent="0.7" />
<org.koitharu.kotatsu.base.ui.widgets.ShapeView
android:layout_width="16dp"
android:layout_height="16dp"
android:background="?colorPrimary"
app:cornerSize="6dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<ImageView
android:id="@+id/imageView_check"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:layout_margin="6dp"
android:visibility="gone"
app:srcCompat="@drawable/ic_mtrl_checked_circle"
app:tint="?colorPrimary"
tools:visibility="visible" />
</com.google.android.material.card.MaterialCardView>
<TextView
android:id="@+id/textView_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elegantTextHeight="false"
android:ellipsize="end"
android:paddingTop="4dp"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?android:attr/textColorPrimary"
tools:text="@string/theme_name_mint" />
</LinearLayout>

View File

@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:baselineAligned="false"
android:clipChildren="false"
android:clipToPadding="false"
android:gravity="center_vertical"
android:minHeight="?android:attr/listPreferredItemHeightSmall"
android:orientation="horizontal"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
tools:ignore="PrivateResource">
<include layout="@layout/image_frame" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:clipChildren="false"
android:clipToPadding="false"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:baselineAligned="true"
android:baselineAlignedChildIndex="0"
android:orientation="horizontal">
<TextView
android:id="@android:id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="marquee"
android:labelFor="@id/seekbar"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceListItem"
tools:ignore="LabelFor" />
<TextView
android:id="@android:id/summary"
style="@style/PreferenceSummaryTextStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:textAlignment="viewStart"
android:textColor="?android:attr/textColorSecondary" />
</LinearLayout>
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:clipToPadding="false"
android:paddingStart="0dp"
android:paddingEnd="16dp"
android:scrollIndicators="start|end"
android:scrollbars="none"
tools:ignore="UnusedAttribute">
<LinearLayout
android:id="@+id/linear"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal" />
</HorizontalScrollView>
</LinearLayout>
</LinearLayout>

View File

@@ -8,7 +8,8 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:paddingBottom="16dp">
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:id="@+id/dragHandle"
@@ -35,6 +36,17 @@
tools:background="@sample/covers[9]"
tools:ignore="ContentDescription,UnusedAttribute" />
<ImageView
android:id="@+id/imageView_logo"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_margin="@dimen/card_indicator_offset"
app:layout_constraintBottom_toBottomOf="@id/imageView_cover"
app:layout_constraintEnd_toEndOf="@id/imageView_cover"
app:tint="?colorControlLight"
tools:ignore="ContentDescription"
tools:src="@drawable/ic_shikimori" />
<TextView
android:id="@+id/textView_title"
android:layout_width="0dp"
@@ -103,7 +115,6 @@
android:layout_marginStart="16dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:lineSpacingMultiplier="1.2"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textIsSelectable="true"

View File

@@ -24,11 +24,13 @@
</org.koitharu.kotatsu.base.ui.widgets.BottomSheetHeaderBar>
<Spinner
android:id="@+id/spinner_scrobblers"
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:listitem="@android:layout/simple_spinner_item" />
android:visibility="gone"
app:tabGravity="start"
tools:visibility="visible" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
@@ -36,7 +38,6 @@
android:layout_height="wrap_content"
android:clipToPadding="false"
android:padding="@dimen/grid_spacing"
android:scrollbarStyle="outsideOverlay"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_manga_list" />

View File

@@ -32,7 +32,7 @@
app:showAsAction="never" />
<item
android:id="@+id/action_shiki_track"
android:id="@+id/action_scrobbling"
android:orderInCategory="50"
android:title="@string/tracking"
app:showAsAction="never" />

View File

@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Colored themes -->
<style name="Theme.Kotatsu.Mint">
<item name="colorPrimary">#4CDBCE</item>
<item name="colorOnPrimary">#003733</item>
<item name="colorPrimaryContainer">#00504A</item>
<item name="colorOnPrimaryContainer">#6EF8EA</item>
<item name="colorSecondary">#B1CCC8</item>
<item name="colorOnSecondary">#1C3532</item>
<item name="colorSecondaryContainer">#324B48</item>
<item name="colorOnSecondaryContainer">#CCE8E4</item>
<item name="colorTertiary">#AFC9E7</item>
<item name="colorOnTertiary">#17324A</item>
<item name="colorTertiaryContainer">#2F4961</item>
<item name="colorOnTertiaryContainer">#CEE5FF</item>
<item name="colorError">#FFB4AB</item>
<item name="colorErrorContainer">#93000A</item>
<item name="colorOnError">#690005</item>
<item name="colorOnErrorContainer">#FFDAD6</item>
<item name="android:colorBackground">#191C1C</item>
<item name="colorOnBackground">#E0E3E1</item>
<item name="colorSurface">#191C1C</item>
<item name="colorOnSurface">#E0E3E1</item>
<item name="colorSurfaceVariant">#3F4947</item>
<item name="colorOnSurfaceVariant">#BEC9C6</item>
<item name="colorOutline">#899391</item>
<item name="colorOnSurfaceInverse">#191C1C</item>
<item name="colorSurfaceInverse">#E0E3E1</item>
<item name="colorPrimaryInverse">#006A63</item>
</style>
<style name="Theme.Kotatsu.October">
<item name="colorPrimary">#FFB3AF</item>
<item name="colorOnPrimary">#68000E</item>
<item name="colorPrimaryContainer">#930018</item>
<item name="colorOnPrimaryContainer">#FFDAD7</item>
<item name="colorSecondary">#FFB783</item>
<item name="colorOnSecondary">#4F2500</item>
<item name="colorSecondaryContainer">#713700</item>
<item name="colorOnSecondaryContainer">#FFDCC5</item>
<item name="colorTertiary">#E2C28C</item>
<item name="colorOnTertiary">#412D05</item>
<item name="colorTertiaryContainer">#594319</item>
<item name="colorOnTertiaryContainer">#FFDEA9</item>
<item name="colorError">#FFB4AB</item>
<item name="colorErrorContainer">#93000A</item>
<item name="colorOnError">#690005</item>
<item name="colorOnErrorContainer">#FFDAD6</item>
<item name="android:colorBackground">#201A1A</item>
<item name="colorOnBackground">#EDE0DE</item>
<item name="colorSurface">#201A1A</item>
<item name="colorOnSurface">#EDE0DE</item>
<item name="colorSurfaceVariant">#534342</item>
<item name="colorOnSurfaceVariant">#D8C1C0</item>
<item name="colorOutline">#A08C8B</item>
<item name="colorOnSurfaceInverse">#201A1A</item>
<item name="colorSurfaceInverse">#EDE0DE</item>
<item name="colorPrimaryInverse">#BA1928</item>
</style>
</resources>

View File

@@ -3,14 +3,9 @@
<style name="ThemeOverlay.Kotatsu" parent="ThemeOverlay.Material3.Dark" />
<style name="Theme.Kotatsu.Amoled">
<style name="ThemeOverlay.Kotatsu.Amoled" parent="">
<item name="colorSurface">@color/surface_amoled</item>
<item name="android:colorBackground">@color/background_amoled</item>
</style>
<style name="Theme.Kotatsu.Monet.Amoled">
<item name="colorSurface">@color/surface_amoled</item>
<item name="android:colorBackground">@color/background_amoled</item>
</style>
</resources>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="is_color_themes_available">true</bool>
</resources>

View File

@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Colored themes -->
<style name="Theme.Kotatsu.Mint">
<item name="colorPrimary">#006A63</item>
<item name="colorOnPrimary">#FFFFFF</item>
<item name="colorPrimaryContainer">#6EF8EA</item>
<item name="colorOnPrimaryContainer">#00201D</item>
<item name="colorSecondary">#4A6360</item>
<item name="colorOnSecondary">#FFFFFF</item>
<item name="colorSecondaryContainer">#CCE8E4</item>
<item name="colorOnSecondaryContainer">#051F1D</item>
<item name="colorTertiary">#47617A</item>
<item name="colorOnTertiary">#FFFFFF</item>
<item name="colorTertiaryContainer">#CEE5FF</item>
<item name="colorOnTertiaryContainer">#001D33</item>
<item name="colorError">#BA1A1A</item>
<item name="colorErrorContainer">#FFDAD6</item>
<item name="colorOnError">#FFFFFF</item>
<item name="colorOnErrorContainer">#410002</item>
<item name="android:colorBackground">#FAFDFB</item>
<item name="colorOnBackground">#191C1C</item>
<item name="colorSurface">#FAFDFB</item>
<item name="colorOnSurface">#191C1C</item>
<item name="colorSurfaceVariant">#DAE5E2</item>
<item name="colorOnSurfaceVariant">#3F4947</item>
<item name="colorOutline">#6F7977</item>
<item name="colorOnSurfaceInverse">#EFF1F0</item>
<item name="colorSurfaceInverse">#2D3130</item>
<item name="colorPrimaryInverse">#4CDBCE</item>
</style>
<style name="Theme.Kotatsu.October">
<item name="colorPrimary">#BA1928</item>
<item name="colorOnPrimary">#FFFFFF</item>
<item name="colorPrimaryContainer">#FFDAD7</item>
<item name="colorOnPrimaryContainer">#410005</item>
<item name="colorSecondary">#944B00</item>
<item name="colorOnSecondary">#FFFFFF</item>
<item name="colorSecondaryContainer">#FFDCC5</item>
<item name="colorOnSecondaryContainer">#301400</item>
<item name="colorTertiary">#735B2E</item>
<item name="colorOnTertiary">#FFFFFF</item>
<item name="colorTertiaryContainer">#FFDEA9</item>
<item name="colorOnTertiaryContainer">#271900</item>
<item name="colorError">#BA1A1A</item>
<item name="colorErrorContainer">#FFDAD6</item>
<item name="colorOnError">#FFFFFF</item>
<item name="colorOnErrorContainer">#410002</item>
<item name="android:colorBackground">#FFFBFF</item>
<item name="colorOnBackground">#201A1A</item>
<item name="colorSurface">#FFFBFF</item>
<item name="colorOnSurface">#201A1A</item>
<item name="colorSurfaceVariant">#F4DDDB</item>
<item name="colorOnSurfaceVariant">#534342</item>
<item name="colorOutline">#857372</item>
<item name="colorOnSurfaceInverse">#FBEEEC</item>
<item name="colorSurfaceInverse">#362F2E</item>
<item name="colorPrimaryInverse">#FFB3AF</item>
</style>
</resources>

View File

@@ -4,6 +4,7 @@
<attr name="sliderPreferenceStyle" />
<attr name="multiAutoCompleteTextViewPreferenceStyle" />
<attr name="autoCompleteTextViewPreferenceStyle" />
<attr name="themeChooserPreferenceStyle" />
<attr name="listItemTextViewStyle" />
<attr name="fastScrollerStyle" />
@@ -75,4 +76,14 @@
<attr name="fitStatusBar" format="boolean" />
</declare-styleable>
<declare-styleable name="ShapeView">
<attr name="strokeWidth" />
<attr name="strokeColor" />
<attr name="cornerSize" />
<attr name="cornerSizeTopLeft" />
<attr name="cornerSizeTopRight" />
<attr name="cornerSizeBottomLeft" />
<attr name="cornerSizeBottomRight" />
</declare-styleable>
</resources>

View File

@@ -4,4 +4,5 @@
<bool name="light_status_bar">true</bool>
<bool name="light_navigation_bar">false</bool>
<bool name="com_samsung_android_icon_container_has_icon_container">true</bool>
<bool name="is_color_themes_available">false</bool>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Kotatsu.Mint" />
<style name="Theme.Kotatsu.October" />
</resources>

View File

@@ -408,4 +408,8 @@
<string name="enable_logging">Enable logging</string>
<string name="enable_logging_summary">Record some actions for debug purposes</string>
<string name="show_suspicious_content">Show suspicious content</string>
<string name="theme_name_mint">Mint</string>
<string name="theme_name_dynamic">Dynamic</string>
<string name="color_theme">Color scheme</string>
<string name="theme_name_october">October</string>
</resources>

View File

@@ -255,6 +255,11 @@
<item name="android:widgetLayout">@layout/preference_widget_material_switch</item>
</style>
<style name="Preference.ThemeChooser" parent="Preference.Material">
<item name="android:layout">@layout/preference_theme</item>
<item name="android:selectable">false</item>
</style>
<!-- Progress drawable -->
<style name="ProgressDrawable">

View File

@@ -87,12 +87,10 @@
<!-- Monet theme only support S+ -->
<style name="Theme.Kotatsu.Monet" />
<style name="Theme.Kotatsu.Amoled" />
<style name="Theme.Kotatsu.Monet.Amoled" />
<style name="ThemeOverlay.Kotatsu" parent="ThemeOverlay.Material3.Light" />
<style name="ThemeOverlay.Kotatsu.Amoled" parent="" />
<style name="Theme.Kotatsu.Dialog" parent="">
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsFloating">true</item>

View File

@@ -11,12 +11,10 @@
android:title="@string/theme"
app:useSimpleSummaryProvider="true" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="dynamic_theme"
android:summary="@string/dynamic_theme_summary"
android:title="@string/dynamic_theme"
app:isPreferenceVisible="false" />
<org.koitharu.kotatsu.settings.utils.ThemeChooserPreference
android:key="color_theme"
android:title="@string/color_theme"
app:isPreferenceVisible="@bool/is_color_themes_available" />
<SwitchPreferenceCompat
android:defaultValue="false"
@@ -26,7 +24,8 @@
<org.koitharu.kotatsu.settings.utils.ActivityListPreference
android:key="app_locale"
android:title="@string/language" />
android:title="@string/language"
app:allowDividerAbove="true" />
<ListPreference
android:key="date_format"
@@ -36,7 +35,6 @@
android:entries="@array/list_modes"
android:key="list_mode_2"
android:title="@string/list_mode"
app:allowDividerAbove="true"
app:useSimpleSummaryProvider="true" />
<org.koitharu.kotatsu.settings.utils.SliderPreference