Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f37e76c85 | |||
| fc51d49505 | |||
| 3dde254452 | |||
| aa21dd9721 | |||
| 71f5ee8cb1 | |||
| 40f27ae634 | |||
| 0dfba47d85 | |||
| 69e44b10e9 | |||
| 4cd0cb04a3 | |||
| d9d5595bde | |||
| ed70ca4e18 | |||
| a371bb6514 |
17
.idea/deploymentTargetDropDown.xml
generated
Normal file
17
.idea/deploymentTargetDropDown.xml
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetDropDown">
|
||||
<targetSelectedWithDropDown>
|
||||
<Target>
|
||||
<type value="QUICK_BOOT_TARGET" />
|
||||
<deviceKey>
|
||||
<Key>
|
||||
<type value="VIRTUAL_DEVICE_PATH" />
|
||||
<value value="$USER_HOME$/.android/avd/Pixel_API_S.avd" />
|
||||
</Key>
|
||||
</deviceKey>
|
||||
</Target>
|
||||
</targetSelectedWithDropDown>
|
||||
<timeTargetWasSelectedWithDropDown value="2021-02-19T19:02:37.198775Z" />
|
||||
</component>
|
||||
</project>
|
||||
29
.idea/misc.xml
generated
29
.idea/misc.xml
generated
@@ -1,5 +1,34 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DesignSurface">
|
||||
<option name="filePathToZoomLevelMap">
|
||||
<map>
|
||||
<entry key="../../../../../../layout/custom_preview.xml" value="0.1" />
|
||||
<entry key="../../../../../../opt/usr/android-sdk/platforms/android-30/data/res/drawable/list_divider_material.xml" value="0.28512820512820514" />
|
||||
<entry key="../../../../../../opt/usr/android-sdk/platforms/android-30/data/res/layout/simple_dropdown_item_1line.xml" value="0.24739583333333334" />
|
||||
<entry key="app/src/main/res/drawable/tab_indicator.xml" value="0.28512820512820514" />
|
||||
<entry key="app/src/main/res/drawable/tabs_background.xml" value="0.28512820512820514" />
|
||||
<entry key="app/src/main/res/layout-w600dp/fragment_details.xml" value="0.14583333333333334" />
|
||||
<entry key="app/src/main/res/layout-w600dp/fragment_list.xml" value="0.14635416666666667" />
|
||||
<entry key="app/src/main/res/layout/dialog_favorite_categories.xml" value="0.2601851851851852" />
|
||||
<entry key="app/src/main/res/layout/dialog_list_mode.xml" value="0.2601851851851852" />
|
||||
<entry key="app/src/main/res/layout/fragment_chapters.xml" value="0.24739583333333334" />
|
||||
<entry key="app/src/main/res/layout/fragment_details.xml" value="0.26145833333333335" />
|
||||
<entry key="app/src/main/res/layout/fragment_favourites.xml" value="0.26296296296296295" />
|
||||
<entry key="app/src/main/res/layout/fragment_feed.xml" value="0.2601851851851852" />
|
||||
<entry key="app/src/main/res/layout/fragment_list.xml" value="0.2601851851851852" />
|
||||
<entry key="app/src/main/res/layout/item_branch.xml" value="0.24739583333333334" />
|
||||
<entry key="app/src/main/res/layout/item_branch_dropdown.xml" value="0.25743589743589745" />
|
||||
<entry key="app/src/main/res/layout/item_category_checkable.xml" value="0.2601851851851852" />
|
||||
<entry key="app/src/main/res/layout/item_manga_grid.xml" value="0.26042632066728455" />
|
||||
<entry key="app/src/main/res/layout/item_manga_list_details.xml" value="0.2601851851851852" />
|
||||
<entry key="app/src/main/res/layout/item_page_thumb.xml" value="0.2601851851851852" />
|
||||
<entry key="app/src/main/res/layout/item_recent.xml" value="0.2601851851851852" />
|
||||
<entry key="app/src/main/res/layout/sheet_pages.xml" value="0.2601851851851852" />
|
||||
<entry key="app/src/main/res/menu/popup_category.xml" value="0.2601851851851852" />
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
|
||||
@@ -16,7 +16,7 @@ android {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 30
|
||||
versionCode gitCommits
|
||||
versionName '1.0-rc1'
|
||||
versionName '1.0-rc3'
|
||||
|
||||
kapt {
|
||||
arguments {
|
||||
@@ -62,27 +62,26 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
|
||||
}
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2'
|
||||
|
||||
implementation 'androidx.core:core-ktx:1.5.0-beta01'
|
||||
implementation 'androidx.activity:activity-ktx:1.2.0-rc01'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.3.0-rc02'
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-rc01'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0-rc01'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.0-rc01'
|
||||
implementation 'androidx.lifecycle:lifecycle-service:2.3.0-rc01'
|
||||
implementation 'androidx.lifecycle:lifecycle-process:2.3.0-rc01'
|
||||
implementation 'androidx.core:core-ktx:1.5.0-beta03'
|
||||
implementation 'androidx.activity:activity-ktx:1.2.1'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.3.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-service:2.3.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-process:2.3.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.0-beta01'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.0-beta02'
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
|
||||
implementation 'androidx.preference:preference-ktx:1.1.1'
|
||||
implementation 'androidx.work:work-runtime-ktx:2.5.0'
|
||||
implementation 'com.google.android.material:material:1.3.0-rc01'
|
||||
implementation 'com.google.android.material:material:1.3.0'
|
||||
//noinspection LifecycleAnnotationProcessorWithJava8
|
||||
kapt 'androidx.lifecycle:lifecycle-compiler:2.3.0-rc01'
|
||||
kapt 'androidx.lifecycle:lifecycle-compiler:2.3.0'
|
||||
|
||||
implementation 'androidx.room:room-runtime:2.2.6'
|
||||
implementation 'androidx.room:room-ktx:2.2.6'
|
||||
@@ -103,7 +102,7 @@ dependencies {
|
||||
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.6'
|
||||
|
||||
testImplementation 'junit:junit:4.13.1'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.json:json:20201115'
|
||||
testImplementation 'org.koin:koin-test:2.2.2'
|
||||
}
|
||||
@@ -106,6 +106,7 @@
|
||||
|
||||
<receiver
|
||||
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetProvider"
|
||||
android:exported="true"
|
||||
android:label="@string/manga_shelf">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
@@ -116,6 +117,7 @@
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name="org.koitharu.kotatsu.widget.recent.RecentWidgetProvider"
|
||||
android:exported="true"
|
||||
android:label="@string/recent_manga">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
@@ -125,6 +127,11 @@
|
||||
android:resource="@xml/widget_recent" />
|
||||
</receiver>
|
||||
|
||||
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
|
||||
android:value="false" />
|
||||
<meta-data android:name="android.webkit.WebView.MetricsOptOut"
|
||||
android:value="true" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -8,6 +8,7 @@ import androidx.annotation.CallSuper
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
|
||||
abstract class AlertDialogFragment<B : ViewBinding> : DialogFragment() {
|
||||
|
||||
@@ -20,7 +21,7 @@ abstract class AlertDialogFragment<B : ViewBinding> : DialogFragment() {
|
||||
val inflater = activity?.layoutInflater ?: LayoutInflater.from(requireContext())
|
||||
val binding = onInflateView(inflater, null)
|
||||
viewBinding = binding
|
||||
return AlertDialog.Builder(requireContext(), theme)
|
||||
return MaterialAlertDialogBuilder(requireContext(), theme)
|
||||
.setView(binding.root)
|
||||
.also(::onBuildDialog)
|
||||
.create()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.koitharu.kotatsu.base.ui
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.MenuItem
|
||||
@@ -97,4 +98,16 @@ abstract class BaseActivity<B : ViewBinding> : AppCompatActivity(), OnApplyWindo
|
||||
super.onSupportActionModeFinished(mode)
|
||||
window?.statusBarColor = getThemeColor(android.R.attr.statusBarColor)
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if ( // https://issuetracker.google.com/issues/139738913
|
||||
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q &&
|
||||
isTaskRoot &&
|
||||
supportFragmentManager.backStackEntryCount == 0
|
||||
) {
|
||||
finishAfterTransition()
|
||||
} else {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import android.view.LayoutInflater
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.koitharu.kotatsu.databinding.DialogCheckboxBinding
|
||||
|
||||
class CheckBoxAlertDialog private constructor(private val delegate: AlertDialog) :
|
||||
@@ -17,7 +18,7 @@ class CheckBoxAlertDialog private constructor(private val delegate: AlertDialog)
|
||||
|
||||
private val binding = DialogCheckboxBinding.inflate(LayoutInflater.from(context))
|
||||
|
||||
private val delegate = AlertDialog.Builder(context)
|
||||
private val delegate = MaterialAlertDialogBuilder(context)
|
||||
.setView(binding.root)
|
||||
|
||||
fun setTitle(@StringRes titleResId: Int): Builder {
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.view.ViewGroup
|
||||
import android.widget.BaseAdapter
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.databinding.ItemStorageBinding
|
||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||
@@ -23,7 +24,7 @@ class StorageSelectDialog private constructor(private val delegate: AlertDialog)
|
||||
class Builder(context: Context, defaultValue: File?, listener: OnStorageSelectListener) {
|
||||
|
||||
private val adapter = VolumesAdapter(context)
|
||||
private val delegate = AlertDialog.Builder(context)
|
||||
private val delegate = MaterialAlertDialogBuilder(context)
|
||||
|
||||
init {
|
||||
if (adapter.isEmpty) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.text.InputFilter
|
||||
import android.view.LayoutInflater
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.koitharu.kotatsu.databinding.DialogInputBinding
|
||||
|
||||
class TextInputDialog private constructor(
|
||||
@@ -18,7 +19,7 @@ class TextInputDialog private constructor(
|
||||
|
||||
private val binding = DialogInputBinding.inflate(LayoutInflater.from(context))
|
||||
|
||||
private val delegate = AlertDialog.Builder(context)
|
||||
private val delegate = MaterialAlertDialogBuilder(context)
|
||||
.setView(binding.root)
|
||||
|
||||
fun setTitle(@StringRes titleResId: Int): Builder {
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package org.koitharu.kotatsu.base.ui.widgets
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View.OnClickListener
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.core.view.children
|
||||
import com.google.android.material.R
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.chip.ChipGroup
|
||||
import org.koitharu.kotatsu.utils.ext.getThemeColor
|
||||
|
||||
class ChipsView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = R.attr.chipGroupStyle
|
||||
) : ChipGroup(context, attrs, defStyleAttr) {
|
||||
|
||||
private var isLayoutSuppressedCompat = false
|
||||
private var isLayoutCalledOnSuppressed = false
|
||||
private var chipOnClickListener = OnClickListener {
|
||||
onChipClickListener?.onChipClick(it as Chip, it.tag)
|
||||
}
|
||||
var onChipClickListener: OnChipClickListener? = null
|
||||
set(value) {
|
||||
field = value
|
||||
val isChipClickable = value != null
|
||||
children.forEach { it.isClickable = isChipClickable }
|
||||
}
|
||||
|
||||
override fun requestLayout() {
|
||||
if (isLayoutSuppressedCompat) {
|
||||
isLayoutCalledOnSuppressed = true
|
||||
} else {
|
||||
super.requestLayout()
|
||||
}
|
||||
}
|
||||
|
||||
fun setChips(items: List<ChipModel>) {
|
||||
suppressLayoutCompat(true)
|
||||
try {
|
||||
for ((i, model) in items.withIndex()) {
|
||||
val chip = getChildAt(i) as Chip? ?: addChip()
|
||||
bindChip(chip, model)
|
||||
}
|
||||
for (i in items.size until childCount) {
|
||||
removeViewAt(i)
|
||||
}
|
||||
} finally {
|
||||
suppressLayoutCompat(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindChip(chip: Chip, model: ChipModel) {
|
||||
chip.text = model.title
|
||||
if (model.icon == 0) {
|
||||
chip.isChipIconVisible = false
|
||||
} else {
|
||||
chip.isCheckedIconVisible = true
|
||||
chip.setChipIconResource(model.icon)
|
||||
}
|
||||
chip.tag = model.data
|
||||
}
|
||||
|
||||
private fun addChip(): Chip {
|
||||
val chip = Chip(context)
|
||||
chip.setTextColor(context.getThemeColor(android.R.attr.textColorPrimary))
|
||||
chip.isCloseIconVisible = false
|
||||
chip.setEnsureMinTouchTargetSize(false)
|
||||
chip.setOnClickListener(chipOnClickListener)
|
||||
chip.isClickable = onChipClickListener != null
|
||||
addView(chip)
|
||||
return chip
|
||||
}
|
||||
|
||||
private fun suppressLayoutCompat(suppress: Boolean) {
|
||||
isLayoutSuppressedCompat = suppress
|
||||
if (!suppress) {
|
||||
if (isLayoutCalledOnSuppressed) {
|
||||
requestLayout()
|
||||
isLayoutCalledOnSuppressed = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class ChipModel(
|
||||
@DrawableRes val icon: Int,
|
||||
val title: CharSequence,
|
||||
val data: Any? = null
|
||||
)
|
||||
|
||||
fun interface OnChipClickListener {
|
||||
|
||||
fun onChipClick(chip: Chip, data: Any?)
|
||||
}
|
||||
}
|
||||
@@ -9,5 +9,6 @@ data class MangaChapter(
|
||||
val name: String,
|
||||
val number: Int,
|
||||
val url: String,
|
||||
val branch: String? = null,
|
||||
val source: MangaSource
|
||||
) : Parcelable
|
||||
@@ -90,6 +90,8 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) :
|
||||
chapters = ArrayList(total)
|
||||
for (i in 0 until total) {
|
||||
val item = list.getJSONObject(i)
|
||||
val chapterId = item.getLong("chapter_id")
|
||||
val branchName = item.getStringOrNull("username")
|
||||
val url = buildString {
|
||||
append(manga.url)
|
||||
append("/v")
|
||||
@@ -106,9 +108,10 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) :
|
||||
}
|
||||
chapters.add(
|
||||
MangaChapter(
|
||||
id = generateUid(url),
|
||||
id = generateUid(chapterId),
|
||||
url = url,
|
||||
source = source,
|
||||
branch = branchName,
|
||||
number = total - i,
|
||||
name = name
|
||||
)
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.google.android.material.chip.Chip
|
||||
import org.koitharu.kotatsu.utils.ext.getThemeColor
|
||||
|
||||
class ChipsFactory(val context: Context) {
|
||||
|
||||
fun create(
|
||||
convertView: Chip? = null,
|
||||
text: CharSequence,
|
||||
@DrawableRes iconRes: Int = 0,
|
||||
tag: Any? = null,
|
||||
onClickListener: View.OnClickListener? = null
|
||||
): Chip {
|
||||
val chip = convertView ?: Chip(context).apply {
|
||||
setTextColor(context.getThemeColor(android.R.attr.textColorPrimary))
|
||||
isCloseIconVisible = false
|
||||
}
|
||||
chip.text = text
|
||||
if (iconRes == 0) {
|
||||
chip.isChipIconVisible = false
|
||||
} else {
|
||||
chip.isCheckedIconVisible = true
|
||||
chip.setChipIconResource(iconRes)
|
||||
}
|
||||
chip.tag = tag
|
||||
chip.setEnsureMinTouchTargetSize(false)
|
||||
chip.setOnClickListener(onClickListener)
|
||||
return chip
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.details.ui
|
||||
import android.app.ActivityOptions
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import android.widget.AdapterView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.graphics.Insets
|
||||
@@ -17,6 +18,7 @@ import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.model.MangaChapter
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.databinding.FragmentChaptersBinding
|
||||
import org.koitharu.kotatsu.details.ui.adapter.BranchesAdapter
|
||||
import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter
|
||||
import org.koitharu.kotatsu.details.ui.adapter.ChaptersSelectionDecoration
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||
@@ -25,7 +27,7 @@ import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
|
||||
class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
|
||||
OnListItemClickListener<MangaChapter>, ActionMode.Callback {
|
||||
OnListItemClickListener<MangaChapter>, ActionMode.Callback, AdapterView.OnItemSelectedListener {
|
||||
|
||||
private val viewModel by sharedViewModel<DetailsViewModel>()
|
||||
|
||||
@@ -48,14 +50,31 @@ class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
|
||||
chaptersAdapter = ChaptersAdapter(this)
|
||||
selectionDecoration = ChaptersSelectionDecoration(view.context)
|
||||
with(binding.recyclerViewChapters) {
|
||||
addItemDecoration(DividerItemDecoration(view.context, RecyclerView.VERTICAL))
|
||||
addItemDecoration(
|
||||
DividerItemDecoration(
|
||||
view.context,
|
||||
RecyclerView.VERTICAL
|
||||
)
|
||||
)
|
||||
addItemDecoration(selectionDecoration!!)
|
||||
setHasFixedSize(true)
|
||||
adapter = chaptersAdapter
|
||||
}
|
||||
val branchesAdapter = BranchesAdapter()
|
||||
binding.spinnerBranches.adapter = branchesAdapter
|
||||
binding.spinnerBranches.onItemSelectedListener = this
|
||||
|
||||
viewModel.isLoading.observe(viewLifecycleOwner, this::onLoadingStateChanged)
|
||||
viewModel.chapters.observe(viewLifecycleOwner, this::onChaptersChanged)
|
||||
viewModel.branches.observe(viewLifecycleOwner) {
|
||||
branchesAdapter.setItems(it)
|
||||
binding.spinnerBranches.isVisible = it.size > 1
|
||||
}
|
||||
viewModel.selectedBranchIndex.observe(viewLifecycleOwner) {
|
||||
if (it != -1 && it != binding.spinnerBranches.selectedItemPosition) {
|
||||
binding.spinnerBranches.setSelection(it)
|
||||
}
|
||||
}
|
||||
viewModel.isChaptersReversed.observe(viewLifecycleOwner) {
|
||||
activity?.invalidateOptionsMenu()
|
||||
}
|
||||
@@ -64,6 +83,7 @@ class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
|
||||
override fun onDestroyView() {
|
||||
chaptersAdapter = null
|
||||
selectionDecoration = null
|
||||
binding.spinnerBranches.adapter = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
@@ -145,6 +165,12 @@ class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||
viewModel.setSelectedBranch(binding.spinnerBranches.selectedItem as String?)
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
|
||||
|
||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
val manga = viewModel.manga.value
|
||||
mode.menuInflater.inflate(R.menu.mode_chapters, menu)
|
||||
@@ -174,7 +200,7 @@ class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
|
||||
binding.recyclerViewChapters.updatePadding(
|
||||
left = insets.left,
|
||||
right = insets.right,
|
||||
bottom = insets.bottom
|
||||
bottom = insets.bottom + binding.spinnerBranches.height
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
@@ -15,6 +14,7 @@ import androidx.core.graphics.Insets
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
@@ -144,7 +144,7 @@ class DetailsActivity : BaseActivity<ActivityDetailsBinding>(),
|
||||
}
|
||||
R.id.action_delete -> {
|
||||
viewModel.manga.value?.let { m ->
|
||||
AlertDialog.Builder(this)
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.delete_manga)
|
||||
.setMessage(getString(R.string.text_delete_local_manga, m.title))
|
||||
.setPositiveButton(R.string.delete) { _, _ ->
|
||||
@@ -159,7 +159,7 @@ class DetailsActivity : BaseActivity<ActivityDetailsBinding>(),
|
||||
viewModel.manga.value?.let {
|
||||
val chaptersCount = it.chapters?.size ?: 0
|
||||
if (chaptersCount > 5) {
|
||||
AlertDialog.Builder(this)
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.save_manga)
|
||||
.setMessage(
|
||||
getString(
|
||||
|
||||
@@ -13,12 +13,14 @@ import androidx.core.view.updatePadding
|
||||
import coil.ImageLoader
|
||||
import coil.util.CoilUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||
import org.koitharu.kotatsu.databinding.FragmentDetailsBinding
|
||||
@@ -34,6 +36,7 @@ class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickList
|
||||
|
||||
private val viewModel by sharedViewModel<DetailsViewModel>()
|
||||
private val coil by inject<ImageLoader>()
|
||||
private var tagsJob: Job? = null
|
||||
|
||||
override fun onInflateView(
|
||||
inflater: LayoutInflater,
|
||||
@@ -67,47 +70,11 @@ class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickList
|
||||
ratingBar.progress = (ratingBar.max * manga.rating).roundToInt()
|
||||
ratingBar.isVisible = true
|
||||
}
|
||||
chipsTags.removeAllViews()
|
||||
manga.author?.let { a ->
|
||||
chipsTags.addChips(listOf(a)) {
|
||||
create(
|
||||
text = it,
|
||||
iconRes = R.drawable.ic_chip_user,
|
||||
tag = it
|
||||
)
|
||||
}
|
||||
}
|
||||
chipsTags.addChips(manga.tags) {
|
||||
create(
|
||||
text = it.title,
|
||||
iconRes = R.drawable.ic_chip_tag,
|
||||
tag = it
|
||||
)
|
||||
}
|
||||
manga.url.toUri().toFileOrNull()?.let { f ->
|
||||
viewLifecycleScope.launch {
|
||||
val size = withContext(Dispatchers.IO) {
|
||||
f.length()
|
||||
}
|
||||
chipsTags.addChips(listOf(f)) {
|
||||
create(
|
||||
text = FileSizeUtils.formatBytes(context, size),
|
||||
iconRes = R.drawable.ic_chip_storage,
|
||||
tag = it
|
||||
)
|
||||
}
|
||||
}
|
||||
} ?: chipsTags.addChips(listOf(manga.source)) {
|
||||
create(
|
||||
text = it.title,
|
||||
iconRes = R.drawable.ic_chip_web,
|
||||
tag = it
|
||||
)
|
||||
}
|
||||
imageViewFavourite.setOnClickListener(this@DetailsFragment)
|
||||
buttonRead.setOnClickListener(this@DetailsFragment)
|
||||
buttonRead.setOnLongClickListener(this@DetailsFragment)
|
||||
buttonRead.isEnabled = !manga.chapters.isNullOrEmpty()
|
||||
bindTags(manga)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,4 +158,39 @@ class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickList
|
||||
bottom = insets.bottom
|
||||
)
|
||||
}
|
||||
|
||||
private fun bindTags(manga: Manga) {
|
||||
tagsJob?.cancel()
|
||||
tagsJob = viewLifecycleScope.launch {
|
||||
val tags = ArrayList<ChipsView.ChipModel>(manga.tags.size + 2)
|
||||
if (manga.author != null) {
|
||||
tags += ChipsView.ChipModel(
|
||||
title = manga.author,
|
||||
icon = R.drawable.ic_chip_user
|
||||
)
|
||||
}
|
||||
for (tag in manga.tags) {
|
||||
tags += ChipsView.ChipModel(
|
||||
title = tag.title,
|
||||
icon = R.drawable.ic_chip_tag
|
||||
)
|
||||
}
|
||||
val file = manga.url.toUri().toFileOrNull()
|
||||
if (file != null) {
|
||||
val size = withContext(Dispatchers.IO) {
|
||||
file.length()
|
||||
}
|
||||
tags += ChipsView.ChipModel(
|
||||
title = FileSizeUtils.formatBytes(requireContext(), size),
|
||||
icon = R.drawable.ic_chip_storage
|
||||
)
|
||||
} else {
|
||||
tags += ChipsView.ChipModel(
|
||||
title = manga.source.title,
|
||||
icon = R.drawable.ic_chip_web
|
||||
)
|
||||
}
|
||||
binding.chipsTags.setChips(tags)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.koitharu.kotatsu.details.ui
|
||||
|
||||
import androidx.lifecycle.asFlow
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -18,6 +19,7 @@ import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.ext.mapToSet
|
||||
import java.io.IOException
|
||||
|
||||
class DetailsViewModel(
|
||||
@@ -31,6 +33,7 @@ class DetailsViewModel(
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val mangaData = MutableStateFlow<Manga?>(intent.manga)
|
||||
private val selectedBranch = MutableStateFlow<String?>(null)
|
||||
|
||||
private val history = mangaData.mapNotNull { it?.id }
|
||||
.distinctUntilChanged()
|
||||
@@ -69,12 +72,24 @@ class DetailsViewModel(
|
||||
|
||||
val onMangaRemoved = SingleLiveEvent<Manga>()
|
||||
|
||||
val branches = mangaData.map {
|
||||
it?.chapters?.mapToSet { x -> x.branch }?.sortedBy { x -> x }.orEmpty()
|
||||
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
|
||||
val selectedBranchIndex = combine(
|
||||
branches.asFlow(),
|
||||
selectedBranch
|
||||
) { branches, selected ->
|
||||
branches.indexOf(selected)
|
||||
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
|
||||
val chapters = combine(
|
||||
mangaData.map { it?.chapters.orEmpty() },
|
||||
history.map { it?.chapterId },
|
||||
newChapters,
|
||||
chaptersReversed
|
||||
) { chapters, currentId, newCount, reversed ->
|
||||
chaptersReversed,
|
||||
selectedBranch
|
||||
) { chapters, currentId, newCount, reversed, branch ->
|
||||
val currentIndex = chapters.indexOfFirst { it.id == currentId }
|
||||
val firstNewIndex = chapters.size - newCount
|
||||
val res = chapters.mapIndexed { index, chapter ->
|
||||
@@ -86,7 +101,7 @@ class DetailsViewModel(
|
||||
else -> ChapterExtra.UNREAD
|
||||
}
|
||||
)
|
||||
}
|
||||
}.filter { it.chapter.branch == branch }
|
||||
if (reversed) res.asReversed() else res
|
||||
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
|
||||
@@ -96,6 +111,15 @@ class DetailsViewModel(
|
||||
?: throw MangaNotFoundException("Cannot find manga")
|
||||
mangaData.value = manga
|
||||
manga = manga.source.repository.getDetails(manga)
|
||||
// find default branch
|
||||
val hist = historyRepository.getOne(manga)
|
||||
selectedBranch.value = if (hist != null) {
|
||||
manga.chapters?.find { it.id == hist.chapterId }?.branch
|
||||
} else {
|
||||
manga.chapters
|
||||
?.groupBy { it.branch }
|
||||
?.maxByOrNull { it.value.size }?.key
|
||||
}
|
||||
mangaData.value = manga
|
||||
}
|
||||
}
|
||||
@@ -114,4 +138,8 @@ class DetailsViewModel(
|
||||
fun setChaptersReversed(newValue: Boolean) {
|
||||
settings.chaptersReverse = newValue
|
||||
}
|
||||
|
||||
fun setSelectedBranch(branch: String?) {
|
||||
selectedBranch.value = branch
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.koitharu.kotatsu.details.ui.adapter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.BaseAdapter
|
||||
import android.widget.TextView
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.utils.ext.replaceWith
|
||||
|
||||
class BranchesAdapter : BaseAdapter() {
|
||||
|
||||
private val dataSet = ArrayList<String?>()
|
||||
|
||||
override fun getCount(): Int {
|
||||
return dataSet.size
|
||||
}
|
||||
|
||||
override fun getItem(position: Int): Any? {
|
||||
return dataSet[position]
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return dataSet[position].hashCode().toLong()
|
||||
}
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val view = convertView ?: LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_branch, parent, false)
|
||||
(view as TextView).text = dataSet[position]
|
||||
return view
|
||||
}
|
||||
|
||||
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val view = convertView ?: LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_branch_dropdown, parent, false)
|
||||
(view as TextView).text = dataSet[position]
|
||||
return view
|
||||
}
|
||||
|
||||
fun setItems(items: Collection<String?>) {
|
||||
dataSet.replaceWith(items)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,14 @@ class FavouritesPagerAdapter(
|
||||
return FavouritesListFragment.newInstance(item.id)
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return differ.currentList[position].id
|
||||
}
|
||||
|
||||
override fun containsItem(itemId: Long): Boolean {
|
||||
return differ.currentList.any { it.id == itemId }
|
||||
}
|
||||
|
||||
override fun onConfigureTab(tab: TabLayout.Tab, position: Int) {
|
||||
val item = differ.currentList[position]
|
||||
tab.text = item.title
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.koitharu.kotatsu.favourites.ui.categories
|
||||
|
||||
import android.content.Context
|
||||
import android.text.InputType
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.dialog.TextInputDialog
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
@@ -13,7 +13,7 @@ class CategoriesEditDelegate(
|
||||
) {
|
||||
|
||||
fun deleteCategory(category: FavouriteCategory) {
|
||||
AlertDialog.Builder(context)
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setMessage(context.getString(R.string.category_delete_confirm, category.title))
|
||||
.setTitle(R.string.remove_category)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
|
||||
@@ -5,7 +5,7 @@ import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.koitharu.kotatsu.R
|
||||
@@ -42,7 +42,7 @@ class HistoryListFragment : MangaListFragment() {
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.action_clear_history -> {
|
||||
AlertDialog.Builder(context ?: return false)
|
||||
MaterialAlertDialogBuilder(context ?: return false)
|
||||
.setTitle(R.string.clear_history)
|
||||
.setMessage(R.string.text_clear_history_prompt)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
|
||||
@@ -55,6 +55,7 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
|
||||
spanSizeLookup.invalidateCache()
|
||||
}
|
||||
open val isSwipeRefreshEnabled = true
|
||||
private var drawer: DrawerLayout? = null
|
||||
|
||||
protected abstract val viewModel: MangaListViewModel
|
||||
|
||||
@@ -70,7 +71,8 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.drawer?.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
|
||||
drawer = binding.root as? DrawerLayout
|
||||
drawer?.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
|
||||
listAdapter = MangaListAdapter(get(), viewLifecycleOwner, this, ::resolveException)
|
||||
paginationListener = PaginationScrollListener(4, this)
|
||||
with(binding.recyclerView) {
|
||||
@@ -101,6 +103,7 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
drawer = null
|
||||
listAdapter = null
|
||||
paginationListener = null
|
||||
spanSizeLookup.invalidateCache()
|
||||
@@ -118,15 +121,15 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
|
||||
true
|
||||
}
|
||||
R.id.action_filter -> {
|
||||
binding.drawer?.toggleDrawer(GravityCompat.END)
|
||||
drawer?.toggleDrawer(GravityCompat.END)
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
menu.findItem(R.id.action_filter).isVisible = binding.drawer != null &&
|
||||
binding.drawer?.getDrawerLockMode(GravityCompat.END) != DrawerLayout.LOCK_MODE_LOCKED_CLOSED
|
||||
menu.findItem(R.id.action_filter).isVisible = drawer != null &&
|
||||
drawer?.getDrawerLockMode(GravityCompat.END) != DrawerLayout.LOCK_MODE_LOCKED_CLOSED
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
}
|
||||
|
||||
@@ -199,7 +202,7 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
|
||||
state = config.currentFilter,
|
||||
listener = this
|
||||
)
|
||||
binding.drawer?.setDrawerLockMode(
|
||||
drawer?.setDrawerLockMode(
|
||||
if (config.sortOrders.isEmpty() && config.tags.isEmpty()) {
|
||||
DrawerLayout.LOCK_MODE_LOCKED_CLOSED
|
||||
} else {
|
||||
@@ -214,7 +217,7 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
|
||||
|
||||
@CallSuper
|
||||
override fun onFilterChanged(filter: MangaFilter) {
|
||||
binding.drawer?.closeDrawers()
|
||||
drawer?.closeDrawers()
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
|
||||
@@ -72,6 +72,7 @@ class MangaIndex(source: String?) {
|
||||
jo.put("number", chapter.number)
|
||||
jo.put("url", chapter.url)
|
||||
jo.put("name", chapter.name)
|
||||
jo.put("branch", chapter.branch)
|
||||
jo.put("entries", "%03d\\d{3}".format(chapter.number))
|
||||
chapters.put(chapter.id.toString(), jo)
|
||||
}
|
||||
@@ -97,6 +98,7 @@ class MangaIndex(source: String?) {
|
||||
name = v.getString("name"),
|
||||
url = v.getString("url"),
|
||||
number = v.getInt("number"),
|
||||
branch = v.getStringOrNull("branch"),
|
||||
source = source
|
||||
)
|
||||
)
|
||||
|
||||
@@ -7,6 +7,8 @@ import android.webkit.MimeTypeMap
|
||||
import androidx.collection.ArraySet
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.core.model.*
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.local.data.CbzFilter
|
||||
@@ -127,14 +129,16 @@ class LocalMangaRepository(private val context: Context) : MangaRepository {
|
||||
)
|
||||
}
|
||||
|
||||
fun getRemoteManga(localManga: Manga): Manga? {
|
||||
suspend fun getRemoteManga(localManga: Manga): Manga? {
|
||||
val file = runCatching {
|
||||
Uri.parse(localManga.url).toFile()
|
||||
}.getOrNull() ?: return null
|
||||
val zip = ZipFile(file)
|
||||
val entry = zip.getEntry(MangaZip.INDEX_ENTRY)
|
||||
val index = entry?.let(zip::readText)?.let(::MangaIndex) ?: return null
|
||||
return index.getMangaInfo()
|
||||
return withContext(Dispatchers.IO) {
|
||||
val zip = ZipFile(file)
|
||||
val entry = zip.getEntry(MangaZip.INDEX_ENTRY)
|
||||
val index = entry?.let(zip::readText)?.let(::MangaIndex) ?: return@withContext null
|
||||
index.getMangaInfo()
|
||||
}
|
||||
}
|
||||
|
||||
private fun zipUri(file: File, entryName: String) =
|
||||
|
||||
@@ -9,7 +9,7 @@ import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.activity.result.ActivityResultCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
@@ -77,7 +77,7 @@ class LocalListFragment : MangaListFragment(), ActivityResultCallback<Uri> {
|
||||
override fun onPopupMenuItemSelected(item: MenuItem, data: Manga): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.action_delete -> {
|
||||
AlertDialog.Builder(context ?: return false)
|
||||
MaterialAlertDialogBuilder(context ?: return false)
|
||||
.setTitle(R.string.delete_manga)
|
||||
.setMessage(getString(R.string.text_delete_local_manga, data.title))
|
||||
.setPositiveButton(R.string.delete) { _, _ ->
|
||||
|
||||
@@ -99,6 +99,7 @@ class LocalListViewModel(
|
||||
runCatching {
|
||||
historyRepository.deleteOrSwap(manga, original)
|
||||
}
|
||||
mangaList.value = mangaList.value?.filterNot { it.id == manga.id }
|
||||
}
|
||||
shortcutsRepository.updateShortcuts()
|
||||
onMangaRemoved.call(manga)
|
||||
|
||||
@@ -10,12 +10,12 @@ import android.view.*
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.*
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@@ -86,7 +86,7 @@ class ReaderActivity : BaseFullscreenActivity<ActivityReaderBinding>(),
|
||||
viewModel.onError.observe(this, this::onError)
|
||||
viewModel.readerMode.observe(this, this::onInitReader)
|
||||
viewModel.onPageSaved.observe(this, this::onPageSaved)
|
||||
viewModel.uiState.observe(this, this::onUiStateChanged)
|
||||
viewModel.uiState.observeWithPrevious(this, this::onUiStateChanged)
|
||||
viewModel.isLoading.observe(this, this::onLoadingStateChanged)
|
||||
viewModel.content.observe(this) {
|
||||
onLoadingStateChanged(viewModel.isLoading.value == true)
|
||||
@@ -124,11 +124,6 @@ class ReaderActivity : BaseFullscreenActivity<ActivityReaderBinding>(),
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
viewModel.saveCurrentState(reader?.getCurrentState())
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
menuInflater.inflate(R.menu.opt_reader_top, menu)
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
@@ -204,11 +199,15 @@ class ReaderActivity : BaseFullscreenActivity<ActivityReaderBinding>(),
|
||||
private fun onLoadingStateChanged(isLoading: Boolean) {
|
||||
val hasPages = !viewModel.content.value?.pages.isNullOrEmpty()
|
||||
binding.layoutLoading.isVisible = isLoading && !hasPages
|
||||
binding.progressBarBottom.isVisible = isLoading && hasPages
|
||||
if (isLoading && hasPages) {
|
||||
binding.toastView.show(R.string.loading_, true)
|
||||
} else {
|
||||
binding.toastView.hide()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onError(e: Throwable) {
|
||||
val dialog = AlertDialog.Builder(this)
|
||||
val dialog = MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.error_occurred)
|
||||
.setMessage(e.getDisplayMessage(resources))
|
||||
.setPositiveButton(R.string.close, null)
|
||||
@@ -336,19 +335,25 @@ class ReaderActivity : BaseFullscreenActivity<ActivityReaderBinding>(),
|
||||
setUiIsVisible(!binding.appbarTop.isVisible)
|
||||
}
|
||||
|
||||
private fun onUiStateChanged(uiState: ReaderUiState) {
|
||||
private fun onUiStateChanged(uiState: ReaderUiState, previous: ReaderUiState?) {
|
||||
title = uiState.chapterName ?: uiState.mangaName ?: getString(R.string.loading_)
|
||||
supportActionBar?.subtitle = if (uiState.chapterNumber in 1..uiState.chaptersTotal) {
|
||||
getString(R.string.chapter_d_of_d, uiState.chapterNumber, uiState.chaptersTotal)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
if (previous?.chapterName != null && uiState.chapterName != previous.chapterName) {
|
||||
if (!uiState.chapterName.isNullOrEmpty()) {
|
||||
binding.toastView.showTemporary(uiState.chapterName, TOAST_DURATION)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val ACTION_MANGA_READ = "${BuildConfig.APPLICATION_ID}.action.READ_MANGA"
|
||||
private const val EXTRA_STATE = "state"
|
||||
private const val TOAST_DURATION = 1500L
|
||||
|
||||
fun newIntent(context: Context, manga: Manga, state: ReaderState?): Intent {
|
||||
return Intent(context, ReaderActivity::class.java)
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package org.koitharu.kotatsu.reader.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.util.AttributeSet
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
||||
import com.google.android.material.textview.MaterialTextView
|
||||
import org.koitharu.kotatsu.utils.anim.Duration
|
||||
import org.koitharu.kotatsu.utils.anim.Motion
|
||||
import org.koitharu.kotatsu.utils.ext.hideAnimated
|
||||
import org.koitharu.kotatsu.utils.ext.showAnimated
|
||||
|
||||
class ReaderToastView @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
|
||||
) : MaterialTextView(context, attrs, defStyleAttr) {
|
||||
|
||||
private var hideRunnable = Runnable {
|
||||
hide()
|
||||
}
|
||||
|
||||
fun show(message: CharSequence, isLoading: Boolean) {
|
||||
removeCallbacks(hideRunnable)
|
||||
text = message
|
||||
this.showAnimated(Motion.Toast, Duration.SHORT)
|
||||
}
|
||||
|
||||
fun show(@StringRes messageId: Int, isLoading: Boolean) {
|
||||
show(context.getString(messageId), isLoading)
|
||||
}
|
||||
|
||||
fun showTemporary(message: CharSequence, duration: Long) {
|
||||
show(message, false)
|
||||
postDelayed(hideRunnable, duration)
|
||||
}
|
||||
|
||||
fun hide() {
|
||||
removeCallbacks(hideRunnable)
|
||||
this.hideAnimated(Motion.Toast, Duration.SHORT)
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
removeCallbacks(hideRunnable)
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
// FIXME use it as compound drawable
|
||||
private fun createProgressDrawable(): CircularProgressDrawable {
|
||||
val drawable = CircularProgressDrawable(context)
|
||||
drawable.setStyle(CircularProgressDrawable.DEFAULT)
|
||||
drawable.arrowEnabled = false
|
||||
drawable.setColorSchemeColors(Color.WHITE)
|
||||
drawable.centerRadius = lineHeight / 3f
|
||||
return drawable
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,6 @@ import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import org.koitharu.kotatsu.base.domain.MangaDataRepository
|
||||
@@ -20,11 +18,11 @@ import org.koitharu.kotatsu.core.exceptions.MangaNotFoundException
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.MangaChapter
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||
import org.koitharu.kotatsu.core.os.ShortcutsRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
import org.koitharu.kotatsu.local.data.PagesCache
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
|
||||
import org.koitharu.kotatsu.utils.MediaStoreCompat
|
||||
@@ -42,7 +40,7 @@ class ReaderViewModel(
|
||||
|
||||
private var loadingJob: Job? = null
|
||||
private val currentState = MutableStateFlow<ReaderState?>(null)
|
||||
private val mangaData = MutableStateFlow<Manga?>(intent.manga)
|
||||
private val mangaData = MutableStateFlow(intent.manga)
|
||||
private val chapters = LongSparseArray<MangaChapter>()
|
||||
|
||||
val readerMode = MutableLiveData<ReaderMode>()
|
||||
@@ -60,7 +58,7 @@ class ReaderViewModel(
|
||||
)
|
||||
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
|
||||
val content = MutableLiveData<ReaderContent>(ReaderContent(emptyList(), null))
|
||||
val content = MutableLiveData(ReaderContent(emptyList(), null))
|
||||
val manga: Manga?
|
||||
get() = mangaData.value
|
||||
|
||||
@@ -82,7 +80,6 @@ class ReaderViewModel(
|
||||
manga.chapters?.forEach {
|
||||
chapters.put(it.id, it)
|
||||
}
|
||||
mangaData.value = manga
|
||||
// determine mode
|
||||
val mode =
|
||||
dataRepository.getReaderMode(manga.id) ?: manga.chapters?.randomOrNull()?.let {
|
||||
@@ -98,6 +95,9 @@ class ReaderViewModel(
|
||||
currentState.value = state ?: historyRepository.getOne(manga)?.let {
|
||||
ReaderState.from(it)
|
||||
} ?: ReaderState.initial(manga)
|
||||
|
||||
val branch = chapters[currentState.value?.chapterId ?: 0L].branch
|
||||
mangaData.value = manga.copy(chapters = manga.chapters?.filter { it.branch == branch })
|
||||
readerMode.postValue(mode)
|
||||
|
||||
val pages = loadChapter(requireNotNull(currentState.value).chapterId)
|
||||
@@ -156,20 +156,11 @@ class ReaderViewModel(
|
||||
}?.toMangaPage() ?: error("Page not found")
|
||||
val repo = page.source.repository
|
||||
val pageUrl = repo.getPageUrl(page)
|
||||
val request = Request.Builder()
|
||||
.url(pageUrl)
|
||||
.header(CommonHeaders.REFERER, page.referer)
|
||||
.get()
|
||||
.build()
|
||||
val uri = get<OkHttpClient>().newCall(request).await().use { response ->
|
||||
val fileName =
|
||||
URLUtil.guessFileName(
|
||||
pageUrl,
|
||||
response.contentDisposition,
|
||||
response.mimeType
|
||||
)
|
||||
val file = get<PagesCache>()[pageUrl] ?: error("Page not found in cache")
|
||||
val uri = file.inputStream().use { input ->
|
||||
val fileName = URLUtil.guessFileName(pageUrl, null, null)
|
||||
MediaStoreCompat(resolver).insertImage(fileName) {
|
||||
checkNotNull(response.body).byteStream().copyTo(it)
|
||||
input.copyTo(it)
|
||||
}
|
||||
}
|
||||
onPageSaved.postCall(uri)
|
||||
|
||||
@@ -18,29 +18,34 @@ abstract class BaseReader<B : ViewBinding> : BaseFragment<B>() {
|
||||
protected val loader by lazy(LazyThreadSafetyMode.NONE) {
|
||||
PageLoader(lifecycleScope, get(), get())
|
||||
}
|
||||
private var lastReaderState: ReaderState? = null
|
||||
private var stateToSave: ReaderState? = null
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
lastReaderState = savedInstanceState?.getParcelable(KEY_STATE) ?: lastReaderState
|
||||
var restoredState = savedInstanceState?.getParcelable<ReaderState?>(KEY_STATE)
|
||||
|
||||
viewModel.content.observe(viewLifecycleOwner) {
|
||||
onPagesChanged(it.pages, lastReaderState ?: it.state)
|
||||
lastReaderState = null
|
||||
onPagesChanged(it.pages, restoredState ?: it.state)
|
||||
restoredState = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
viewModel.saveCurrentState(getCurrentState())
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
lastReaderState = getCurrentState()
|
||||
stateToSave = getCurrentState()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
getCurrentState()?.let {
|
||||
lastReaderState = it
|
||||
stateToSave = it
|
||||
}
|
||||
outState.putParcelable(KEY_STATE, lastReaderState)
|
||||
outState.putParcelable(KEY_STATE, stateToSave)
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) = Unit
|
||||
|
||||
@@ -21,6 +21,7 @@ abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
|
||||
|
||||
init {
|
||||
setHasStableIds(true)
|
||||
stateRestorationPolicy = StateRestorationPolicy.PREVENT
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: H, position: Int) {
|
||||
|
||||
@@ -7,8 +7,8 @@ import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -77,7 +77,7 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
|
||||
|
||||
@MainThread
|
||||
private fun showUpdateDialog(version: AppVersion) {
|
||||
AlertDialog.Builder(activity)
|
||||
MaterialAlertDialogBuilder(activity)
|
||||
.setTitle(R.string.app_update_available)
|
||||
.setMessage(buildString {
|
||||
append(activity.getString(R.string.new_version_s, version.name))
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isVisible
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
|
||||
@@ -40,7 +41,7 @@ class BackupDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
|
||||
}
|
||||
|
||||
private fun onError(e: Throwable) {
|
||||
AlertDialog.Builder(context ?: return)
|
||||
MaterialAlertDialogBuilder(context ?: return)
|
||||
.setNegativeButton(R.string.close, null)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(e.getDisplayMessage(resources))
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isVisible
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import org.koitharu.kotatsu.R
|
||||
@@ -44,7 +45,7 @@ class RestoreDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
|
||||
}
|
||||
|
||||
private fun onError(e: Throwable) {
|
||||
AlertDialog.Builder(context ?: return)
|
||||
MaterialAlertDialogBuilder(context ?: return)
|
||||
.setNegativeButton(R.string.close, null)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(e.getDisplayMessage(resources))
|
||||
@@ -64,7 +65,7 @@ class RestoreDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
|
||||
}
|
||||
|
||||
private fun onRestoreDone(result: CompositeResult) {
|
||||
val builder = AlertDialog.Builder(context ?: return)
|
||||
val builder = MaterialAlertDialogBuilder(context ?: return)
|
||||
when {
|
||||
result.isAllSuccess -> builder.setTitle(R.string.data_restored)
|
||||
.setMessage(R.string.data_restored_success)
|
||||
|
||||
@@ -28,13 +28,8 @@ class TrackingRepository(
|
||||
}
|
||||
val tracks = db.tracksDao.findAll().groupBy { it.mangaId }
|
||||
return mangaList
|
||||
.mapNotNull {
|
||||
if (it.source == MangaSource.LOCAL) {
|
||||
localMangaRepository.getRemoteManga(it)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}.distinctBy { it.id }
|
||||
.filterNot { it.source == MangaSource.LOCAL }
|
||||
.distinctBy { it.id }
|
||||
.map { manga ->
|
||||
val track = tracks[manga.id]?.singleOrNull()
|
||||
MangaTracking(
|
||||
|
||||
@@ -2,9 +2,9 @@ package org.koitharu.kotatsu.tracker.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.updatePadding
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
@@ -80,7 +80,7 @@ class FeedFragment : BaseFragment<FragmentFeedBinding>(), PaginationScrollListen
|
||||
true
|
||||
}
|
||||
R.id.action_clear_feed -> {
|
||||
AlertDialog.Builder(context ?: return false)
|
||||
MaterialAlertDialogBuilder(context ?: return false)
|
||||
.setTitle(R.string.clear_updates_feed)
|
||||
.setMessage(R.string.text_clear_updates_feed_prompt)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
|
||||
@@ -6,21 +6,37 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.TrackingLogItem
|
||||
import org.koitharu.kotatsu.utils.ext.formatRelative
|
||||
|
||||
fun TrackingLogItem.toFeedItem(resources: Resources) = FeedItem(
|
||||
id = id,
|
||||
imageUrl = manga.coverUrl,
|
||||
title = manga.title,
|
||||
subtitle = buildString {
|
||||
append(createdAt.formatRelative(DateUtils.DAY_IN_MILLIS))
|
||||
append(" ")
|
||||
append(
|
||||
resources.getQuantityString(
|
||||
R.plurals.new_chapters,
|
||||
chapters.size,
|
||||
chapters.size
|
||||
fun TrackingLogItem.toFeedItem(resources: Resources): FeedItem {
|
||||
val chaptersString = if (chapters.size > MAX_CHAPTERS) {
|
||||
chapters.joinToString(
|
||||
separator = "\n",
|
||||
limit = MAX_CHAPTERS - 1,
|
||||
truncated = resources.getString(
|
||||
R.string._and_x_more,
|
||||
chapters.size - MAX_CHAPTERS + 1
|
||||
)
|
||||
)
|
||||
},
|
||||
chapters = chapters.joinToString("\n"),
|
||||
manga = manga
|
||||
)
|
||||
} else {
|
||||
chapters.joinToString("\n")
|
||||
}
|
||||
return FeedItem(
|
||||
id = id,
|
||||
imageUrl = manga.coverUrl,
|
||||
title = manga.title,
|
||||
subtitle = buildString {
|
||||
append(createdAt.formatRelative(DateUtils.DAY_IN_MILLIS))
|
||||
append(" ")
|
||||
append(
|
||||
resources.getQuantityString(
|
||||
R.plurals.new_chapters,
|
||||
chapters.size,
|
||||
chapters.size
|
||||
)
|
||||
)
|
||||
},
|
||||
chapters = chaptersString,
|
||||
manga = manga
|
||||
)
|
||||
}
|
||||
|
||||
private const val MAX_CHAPTERS = 6
|
||||
@@ -2,7 +2,8 @@ package org.koitharu.kotatsu.utils.anim
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewPropertyAnimator
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.view.animation.AccelerateInterpolator
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import org.koitharu.kotatsu.utils.ext.measureHeight
|
||||
|
||||
sealed class Motion {
|
||||
@@ -69,27 +70,25 @@ sealed class Motion {
|
||||
}
|
||||
}
|
||||
|
||||
object CheckEffect : Motion() {
|
||||
object Toast : Motion() {
|
||||
override fun reset(v: View) {
|
||||
v.scaleX = 1f
|
||||
v.scaleY = 1f
|
||||
v.alpha = 1f
|
||||
}
|
||||
|
||||
override fun hideView(v: View) {
|
||||
v.scaleX = 0f
|
||||
v.scaleY = 0f
|
||||
v.alpha = 0f
|
||||
}
|
||||
|
||||
override fun hide(v: View, anim: ViewPropertyAnimator) {
|
||||
anim.scaleX(0f)
|
||||
anim.scaleY(0f)
|
||||
anim.interpolator = AccelerateDecelerateInterpolator()
|
||||
anim.alpha(0f)
|
||||
anim.translationY(v.measureHeight().toFloat())
|
||||
anim.interpolator = AccelerateInterpolator()
|
||||
}
|
||||
|
||||
override fun show(v: View, anim: ViewPropertyAnimator) {
|
||||
anim.scaleY(1f)
|
||||
anim.scaleX(1f)
|
||||
anim.interpolator = AccelerateDecelerateInterpolator()
|
||||
anim.alpha(1f)
|
||||
anim.translationY(0f)
|
||||
anim.interpolator = DecelerateInterpolator()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.koitharu.kotatsu.utils.ext
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isGone
|
||||
|
||||
var TextView.textAndVisible: CharSequence?
|
||||
inline get() = text?.takeIf { visibility == View.VISIBLE }
|
||||
set(value) {
|
||||
text = value
|
||||
isGone = value.isNullOrEmpty()
|
||||
}
|
||||
|
||||
var TextView.drawableStart: Drawable?
|
||||
inline get() = compoundDrawablesRelative[0]
|
||||
set(value) {
|
||||
val dr = compoundDrawablesRelative
|
||||
setCompoundDrawablesRelativeWithIntrinsicBounds(value, dr[1], dr[2], dr[3])
|
||||
}
|
||||
@@ -22,7 +22,6 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.chip.ChipGroup
|
||||
import org.koitharu.kotatsu.core.ui.ChipsFactory
|
||||
|
||||
fun View.hideKeyboard() {
|
||||
val imm = context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
@@ -40,21 +39,6 @@ inline fun <reified T : View> ViewGroup.inflate(@LayoutRes resId: Int) =
|
||||
val RecyclerView.hasItems: Boolean
|
||||
get() = (adapter?.itemCount ?: 0) > 0
|
||||
|
||||
var TextView.textAndVisible: CharSequence?
|
||||
get() = text?.takeIf { visibility == View.VISIBLE }
|
||||
set(value) {
|
||||
text = value
|
||||
isGone = value.isNullOrEmpty()
|
||||
}
|
||||
|
||||
inline fun <T> ChipGroup.addChips(data: Iterable<T>, action: ChipsFactory.(T) -> Chip) {
|
||||
val factory = ChipsFactory(context)
|
||||
data.forEach {
|
||||
val chip = factory.action(it)
|
||||
addView(chip)
|
||||
}
|
||||
}
|
||||
|
||||
fun RecyclerView.clearItemDecorations() {
|
||||
while (itemDecorationCount > 0) {
|
||||
removeItemDecorationAt(0)
|
||||
|
||||
5
app/src/main/res/color/tab_text.xml
Normal file
5
app/src/main/res/color/tab_text.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="?colorPrimary" android:state_selected="true" />
|
||||
<item android:alpha="0.6" android:color="?colorOnSurface" />
|
||||
</selector>
|
||||
11
app/src/main/res/drawable/bg_reader_indicator.xml
Normal file
11
app/src/main/res/drawable/bg_reader_indicator.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="16dp" />
|
||||
<solid android:color="@color/dim" />
|
||||
<padding
|
||||
android:bottom="8dp"
|
||||
android:left="12dp"
|
||||
android:right="12dp"
|
||||
android:top="8dp" />
|
||||
</shape>
|
||||
9
app/src/main/res/drawable/tab_indicator.xml
Normal file
9
app/src/main/res/drawable/tab_indicator.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners
|
||||
android:topLeftRadius="3dp"
|
||||
android:topRightRadius="3dp" />
|
||||
<size android:height="3dp" />
|
||||
</shape>
|
||||
20
app/src/main/res/drawable/tabs_background.xml
Normal file
20
app/src/main/res/drawable/tabs_background.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="?colorSurface" />
|
||||
</shape>
|
||||
</item>
|
||||
<item
|
||||
android:left="-2dp"
|
||||
android:right="-2dp"
|
||||
android:top="-2dp">
|
||||
<shape>
|
||||
<solid android:color="@android:color/transparent" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="@color/tabs_line" />
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -23,13 +23,14 @@
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintWidth_percent="0.3" />
|
||||
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
<org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_tags"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:padding="6dp"
|
||||
app:chipSpacingHorizontal="4dp"
|
||||
app:chipSpacingVertical="6dp"
|
||||
app:layout_constraintEnd_toEndOf="@id/imageView_cover"
|
||||
app:layout_constraintStart_toStartOf="@id/imageView_cover"
|
||||
app:layout_constraintTop_toBottomOf="@id/imageView_cover" />
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/grid_spacing_outer"
|
||||
android:scrollbarStyle="outsideOverlay"
|
||||
android:scrollbars="vertical"
|
||||
app:fastScrollEnabled="true"
|
||||
app:fastScrollHorizontalThumbDrawable="@drawable/list_thumb"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:keepScreenOn="true">
|
||||
@@ -11,14 +12,19 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar_bottom"
|
||||
android:layout_width="match_parent"
|
||||
<org.koitharu.kotatsu.reader.ui.ReaderToastView
|
||||
android:id="@+id/toastView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_gravity="bottom|center_horizontal"
|
||||
android:layout_marginBottom="20dp"
|
||||
style="@style/Widget.AppCompat.ProgressBar.Horizontal" />
|
||||
android:background="@drawable/bg_reader_indicator"
|
||||
android:singleLine="true"
|
||||
android:drawablePadding="6dp"
|
||||
android:textAppearance="?android:textAppearanceSmall"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:theme="@style/ThemeOverlay.MaterialComponents.Dark"
|
||||
tools:text="@string/loading_" />
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar_top"
|
||||
@@ -45,7 +51,8 @@
|
||||
android:background="@color/dim"
|
||||
android:elevation="0dp"
|
||||
android:theme="@style/AppToolbarTheme"
|
||||
app:elevation="0dp">
|
||||
app:elevation="0dp"
|
||||
tools:visibility="gone">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar_bottom"
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
<TextView
|
||||
style="@style/MaterialAlertDialog.MaterialComponents.Title.Text"
|
||||
android:padding="16dp"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/add_to_favourites" />
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_grid_title"
|
||||
style="?android:attr/windowTitleStyle"
|
||||
style="@style/MaterialAlertDialog.MaterialComponents.Title.Text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="?attr/dialogPreferredPadding"
|
||||
|
||||
@@ -1,18 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner_branches"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:visibility="gone"
|
||||
tools:listitem="@layout/item_branch" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView_chapters"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_alignWithParentIfMissing="true"
|
||||
android:layout_below="@id/spinner_branches"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:orientation="vertical"
|
||||
android:scrollbars="vertical"
|
||||
app:fastScrollEnabled="true"
|
||||
android:scrollbarStyle="outsideOverlay"
|
||||
app:fastScrollHorizontalThumbDrawable="@drawable/list_thumb"
|
||||
app:fastScrollHorizontalTrackDrawable="@drawable/list_track"
|
||||
app:fastScrollVerticalThumbDrawable="@drawable/list_thumb"
|
||||
@@ -25,8 +41,9 @@
|
||||
style="@style/Widget.AppCompat.ProgressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</FrameLayout>
|
||||
</RelativeLayout>
|
||||
@@ -113,7 +113,7 @@
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/barrier_title" />
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
<org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_tags"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
android:id="@+id/tabs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:elevation="4dp"
|
||||
app:tabGravity="center"
|
||||
app:tabMode="scrollable" />
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/grid_spacing_outer"
|
||||
android:scrollbarStyle="outsideOverlay"
|
||||
android:scrollbars="vertical"
|
||||
app:fastScrollEnabled="true"
|
||||
app:fastScrollHorizontalThumbDrawable="@drawable/list_thumb"
|
||||
|
||||
@@ -1,50 +1,45 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
<androidx.drawerlayout.widget.DrawerLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.drawerlayout.widget.DrawerLayout
|
||||
android:id="@+id/drawer"
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipeRefreshLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipeRefreshLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
android:scrollbars="vertical"
|
||||
app:fastScrollEnabled="true"
|
||||
app:fastScrollHorizontalThumbDrawable="@drawable/list_thumb"
|
||||
app:fastScrollHorizontalTrackDrawable="@drawable/list_track"
|
||||
app:fastScrollVerticalThumbDrawable="@drawable/list_thumb"
|
||||
app:fastScrollVerticalTrackDrawable="@drawable/list_track"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_manga_list" />
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView_filter"
|
||||
android:layout_width="240dp"
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="end"
|
||||
android:background="?android:windowBackground"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/grid_spacing_outer"
|
||||
android:scrollbarStyle="outsideOverlay"
|
||||
android:scrollbars="vertical"
|
||||
app:fastScrollEnabled="true"
|
||||
app:fastScrollHorizontalThumbDrawable="@drawable/list_thumb"
|
||||
app:fastScrollHorizontalTrackDrawable="@drawable/list_track"
|
||||
app:fastScrollVerticalThumbDrawable="@drawable/list_thumb"
|
||||
app:fastScrollVerticalTrackDrawable="@drawable/list_track"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_category_checkable" />
|
||||
tools:listitem="@layout/item_manga_list" />
|
||||
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
||||
</LinearLayout>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView_filter"
|
||||
android:layout_width="240dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="end"
|
||||
android:background="?android:windowBackground"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
android:scrollbars="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_category_checkable" />
|
||||
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
||||
9
app/src/main/res/layout/item_branch.xml
Normal file
9
app/src/main/res/layout/item_branch.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<CheckedTextView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
style="?android:attr/spinnerItemStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?android:attr/listPreferredItemHeightSmall"
|
||||
android:gravity="center_vertical"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?android:attr/textAppearanceLargePopupMenu" />
|
||||
9
app/src/main/res/layout/item_branch_dropdown.xml
Normal file
9
app/src/main/res/layout/item_branch_dropdown.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<CheckedTextView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
style="?android:attr/spinnerDropDownItemStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?android:attr/listPreferredItemHeightSmall"
|
||||
android:drawableEnd="?android:listChoiceIndicatorSingle"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?android:attr/textAppearanceLargePopupMenu" />
|
||||
@@ -23,6 +23,7 @@
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_vertical|start"
|
||||
android:lines="2"
|
||||
android:elegantTextHeight="true"
|
||||
android:padding="6dp"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
tools:text="@tools:sample/lorem" />
|
||||
|
||||
@@ -43,8 +43,8 @@
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:scrollbars="vertical"
|
||||
android:padding="@dimen/grid_spacing"
|
||||
android:scrollbars="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||
app:spanCount="3"
|
||||
tools:listitem="@layout/item_page_thumb" />
|
||||
|
||||
@@ -6,4 +6,5 @@
|
||||
<color name="red_accent">#FF8A65</color>
|
||||
<color name="dim">#99000000</color>
|
||||
<color name="error">#E57373</color>
|
||||
<color name="tabs_line">#5E636A</color>
|
||||
</resources>
|
||||
@@ -200,4 +200,5 @@
|
||||
<string name="sign_in">Войти</string>
|
||||
<string name="auth_required">Для просмотра этого контента требуется авторизация</string>
|
||||
<string name="default_s">По умолчанию: %s</string>
|
||||
<string name="_and_x_more">…и ещё %1$d</string>
|
||||
</resources>
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="BaseAppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<item name="windowActionModeOverlay">true</item>
|
||||
<item name="actionModeCloseDrawable">@drawable/ic_cross</item>
|
||||
@@ -8,5 +9,7 @@
|
||||
<item name="badgeStyle">@style/Widget.MaterialComponents.Badge</item>
|
||||
<item name="android:statusBarColor">@color/status_bar</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
<item name="colorOnPrimary">@android:color/white</item>
|
||||
<item name="tabStyle">@style/Widget.MaterialComponents.TabLayout.New</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<dimen name="grid_spacing">4dp</dimen>
|
||||
<dimen name="grid_spacing_outer">2dp</dimen>
|
||||
<dimen name="preferred_grid_width">140dp</dimen>
|
||||
</resources>
|
||||
@@ -9,4 +9,5 @@
|
||||
<color name="dim">#99000000</color>
|
||||
<color name="error">#D32F2F</color>
|
||||
<color name="status_bar">#33000000</color>
|
||||
<color name="tabs_line">#C3CFDD</color>
|
||||
</resources>
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<dimen name="grid_spacing">2.5dp</dimen>
|
||||
<dimen name="grid_spacing_outer">2dp</dimen>
|
||||
<dimen name="manga_list_item_height">84dp</dimen>
|
||||
<dimen name="manga_list_details_item_height">120dp</dimen>
|
||||
<dimen name="chapter_list_item_height">46dp</dimen>
|
||||
|
||||
@@ -202,4 +202,5 @@
|
||||
<string name="sign_in">Sign in</string>
|
||||
<string name="auth_required">You should authorize to view this content</string>
|
||||
<string name="default_s">Default: %s</string>
|
||||
<string name="_and_x_more">…and %1$d more</string>
|
||||
</resources>
|
||||
@@ -25,4 +25,19 @@
|
||||
<item name="background">@color/grey</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.MaterialComponents.TabLayout.New">
|
||||
<item name="tabIndicatorFullWidth">false</item>
|
||||
<item name="android:background">@drawable/tabs_background</item>
|
||||
<item name="tabIndicator">@drawable/tab_indicator</item>
|
||||
<item name="tabIconTint">@color/tab_text</item>
|
||||
<item name="tabSelectedTextColor">?colorPrimary</item>
|
||||
<item name="tabTextColor">@color/tab_text</item>
|
||||
<item name="tabTextAppearance">@style/TextAppearance.Design.Tab.New</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.Design.Tab.New">
|
||||
<item name="textAllCaps">false</item>
|
||||
<item name="android:textAllCaps">false</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="BaseAppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<item name="android:windowActionModeOverlay">true</item>
|
||||
<item name="windowActionModeOverlay">true</item>
|
||||
@@ -8,6 +9,8 @@
|
||||
<item name="preferenceTheme">@style/PreferenceThemeOverlay</item>
|
||||
<item name="badgeStyle">@style/Widget.MaterialComponents.Badge</item>
|
||||
<item name="android:statusBarColor">@color/status_bar</item>
|
||||
<item name="colorOnPrimary">@android:color/white</item>
|
||||
<item name="tabStyle">@style/Widget.MaterialComponents.TabLayout.New</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme" parent="BaseAppTheme">
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.4.21'
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.1.2'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.31"
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
||||
Reference in New Issue
Block a user