Merge branch 'devel' into feature/mal

pull/302/head
Koitharu 3 years ago
commit 00617d5c64
No known key found for this signature in database
GPG Key ID: 8E861F8CE6E7CE27

1
.gitignore vendored

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -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 onTabUnselected(tab: TabLayout.Tab?) = Unit
override fun onTabReselected(tab: TabLayout.Tab?) {
if (!isExpanded) {
setExpanded(isExpanded = true, isLocked = behavior?.isDraggable == false)
}
binding.recyclerView.firstVisibleItemPosition = 0
}
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
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)
}
}

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

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

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

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

@ -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)
} else {
getString(R.string.disabled)
private fun bindScrobblerSummary(key: String, repository: ScrobblerRepository) {
val pref = findPreference<Preference>(key) ?: return
if (!repository.isAuthorized) {
pref.setSummary(R.string.disabled)
return
}
}
private fun bindAniListSummary() {
findPreference<Preference>(AppSettings.KEY_ANILIST)?.summary = if (aniListRepository.isAuthorized) {
getString(R.string.logged_in_as, aniListRepository.cachedUser?.nickname)
val username = repository.cachedUser?.nickname
if (username != null) {
pref.summary = getString(R.string.logged_in_as, username)
} else {
getString(R.string.disabled)
}
}
private fun launchShikimoriAuth() {
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()
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 launchAniListAuth() {
private fun launchScrobblerAuth(repository: ScrobblerRepository) {
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()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save