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