Compare commits

...

19 Commits

Author SHA1 Message Date
f669a1ca0f Update version name 2021-03-25 19:38:50 +02:00
5a921ea862 Update readme 2021-03-25 19:30:28 +02:00
810efde0b0 Add metadata 2021-03-25 19:17:46 +02:00
03510a1f19 Update dependencies 2021-03-25 07:47:43 +02:00
049f32d2f0 Fix smooth page switching 2021-03-21 18:33:13 +02:00
11a9db3cc2 Fix "Unsupported image format" error 2021-03-21 18:18:57 +02:00
57dd5743f0 Fix tabs on landscape 2021-03-21 18:16:38 +02:00
5f37e76c85 Update dialogs 2021-03-19 21:11:36 +02:00
fc51d49505 Fix lists padding 2021-03-19 20:42:14 +02:00
3dde254452 Refresh tabs style 2021-03-19 20:14:08 +02:00
aa21dd9721 Fixes 2021-03-19 19:38:29 +02:00
71f5ee8cb1 Support for multiple manga branches (translations, etc) 2021-03-08 10:30:15 +02:00
40f27ae634 Fix page saving 2021-02-25 20:10:38 +02:00
0dfba47d85 Misc small fixes 2021-02-19 20:21:44 +02:00
69e44b10e9 Fix tags on details screen 2021-02-18 20:25:01 +02:00
4cd0cb04a3 Fix reader state restoration 2021-02-16 20:22:18 +02:00
d9d5595bde Ellipsize chapters in feed 2021-02-16 20:03:25 +02:00
ed70ca4e18 Information toast in reader 2021-02-11 19:29:17 +02:00
a371bb6514 Fix activity leak on Android Q 2021-02-08 20:29:32 +02:00
95 changed files with 765 additions and 285 deletions

1
.gitignore vendored
View File

@@ -4,6 +4,7 @@
/.idea/caches /.idea/caches
/.idea/libraries /.idea/libraries
/.idea/modules.xml /.idea/modules.xml
/.idea/misc.xml
/.idea/workspace.xml /.idea/workspace.xml
/.idea/navEditor.xml /.idea/navEditor.xml
/.idea/assetWizardSettings.xml /.idea/assetWizardSettings.xml

17
.idea/deploymentTargetDropDown.xml generated Normal file
View 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>

31
.idea/misc.xml generated
View File

@@ -1,6 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK"> <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="11" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>
<component name="ProjectType"> <component name="ProjectType">

View File

@@ -15,20 +15,20 @@ Legacy build (Android 4.1+): [available here](https://github.com/nv95/Kotatsu/re
* Online manga catalogues * Online manga catalogues
* Search manga by name and genre * Search manga by name and genre
* Reading history * Reading history
* Favourites with custom categories * Favourites organized by user-defined categories
* Saving manga and reading it offline * Downloading manga and reading it offline. Third-party CBZ archives also supported
* Tablet-optimized modern UI * Tablet-optimized material design UI
* Reading third-party comics from CBZ
* Standard and Webtoon-optimized reader * Standard and Webtoon-optimized reader
* Notifications about new chapters * Notifications about new chapters with updates feed
* Updates feed
* Global search
### Screenshots ### Screenshots
| ![Screenshot_20200226-210337](https://user-images.githubusercontent.com/8948226/80315102-3478db00-87fe-11ea-9ce8-4bbd1c254b2b.png) | ![Screenshot_20200226-210310](https://user-images.githubusercontent.com/8948226/80315110-3f337000-87fe-11ea-95df-944c196b6667.png) | ![Screenshot_20200226-210232](https://user-images.githubusercontent.com/8948226/80315121-49ee0500-87fe-11ea-8d9b-537a041bbf2f.png) | | ![Screenshot_20200226-210337](https://github.com/nv95/Kotatsu/raw/devel/metadata/en-US/images/phoneScreenshots/1.png) | ![](https://github.com/nv95/Kotatsu/raw/devel/metadata/en-US/images/phoneScreenshots/2.png) | ![Screenshot_20200226-210232](https://github.com/nv95/Kotatsu/raw/devel/metadata/en-US/images/phoneScreenshots/3.png) |
|---|---|---| |---|---|---|
| ![Screenshot_20200226-210405](https://user-images.githubusercontent.com/8948226/80315130-55d9c700-87fe-11ea-8350-2c8452906eb7.png) | ![Screenshot_20200226-210151](https://user-images.githubusercontent.com/8948226/80315135-612cf280-87fe-11ea-984c-aa18567d5bbc.png) | ![Screenshot_20200226-210223](https://user-images.githubusercontent.com/8948226/80315146-6be78780-87fe-11ea-8439-ca1ca578172b.png) | | ![Screenshot_20200226-210405](https://github.com/nv95/Kotatsu/raw/devel/metadata/en-US/images/phoneScreenshots/4.png) | ![Screenshot_20200226-210151](https://github.com/nv95/Kotatsu/raw/devel/metadata/en-US/images/phoneScreenshots/5.png) | ![Screenshot_20200226-210223](https://github.com/nv95/Kotatsu/raw/devel/metadata/en-US/images/phoneScreenshots/6.png) |
| ![](https://github.com/nv95/Kotatsu/raw/devel/metadata/en-US/images/tenInchScreenshots/1.png) | ![](https://github.com/nv95/Kotatsu/raw/devel/metadata/en-US/images/tenInchScreenshots/2.png) |
|---|---|
### License ### License
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html) [![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html)

View File

@@ -16,7 +16,7 @@ android {
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 30 targetSdkVersion 30
versionCode gitCommits versionCode gitCommits
versionName '1.0-rc1' versionName '1.0'
kapt { kapt {
arguments { arguments {
@@ -62,27 +62,26 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
} }
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) 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-core:1.4.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2'
implementation 'androidx.core:core-ktx:1.5.0-beta01' implementation 'androidx.core:core-ktx:1.5.0-rc01'
implementation 'androidx.activity:activity-ktx:1.2.0-rc01' implementation 'androidx.activity:activity-ktx:1.2.2'
implementation 'androidx.fragment:fragment-ktx:1.3.0-rc02' implementation 'androidx.fragment:fragment-ktx:1.3.2'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-rc01' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0-rc01' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.0-rc01' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1'
implementation 'androidx.lifecycle:lifecycle-service:2.3.0-rc01' implementation 'androidx.lifecycle:lifecycle-service:2.3.1'
implementation 'androidx.lifecycle:lifecycle-process:2.3.0-rc01' implementation 'androidx.lifecycle:lifecycle-process:2.3.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.2.0-beta01' implementation 'androidx.recyclerview:recyclerview:1.2.0-rc01'
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01' implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
implementation 'androidx.preference:preference-ktx:1.1.1' implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'androidx.work:work-runtime-ktx:2.5.0' 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 //noinspection LifecycleAnnotationProcessorWithJava8
kapt 'androidx.lifecycle:lifecycle-compiler:2.3.0-rc01' kapt 'androidx.lifecycle:lifecycle-compiler:2.3.1'
implementation 'androidx.room:room-runtime:2.2.6' implementation 'androidx.room:room-runtime:2.2.6'
implementation 'androidx.room:room-ktx:2.2.6' implementation 'androidx.room:room-ktx:2.2.6'
@@ -103,7 +102,7 @@ dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.6' 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.json:json:20201115'
testImplementation 'org.koin:koin-test:2.2.2' testImplementation 'org.koin:koin-test:2.2.2'
} }

View File

@@ -106,6 +106,7 @@
<receiver <receiver
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetProvider" android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetProvider"
android:exported="true"
android:label="@string/manga_shelf"> android:label="@string/manga_shelf">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@@ -116,6 +117,7 @@
</receiver> </receiver>
<receiver <receiver
android:name="org.koitharu.kotatsu.widget.recent.RecentWidgetProvider" android:name="org.koitharu.kotatsu.widget.recent.RecentWidgetProvider"
android:exported="true"
android:label="@string/recent_manga"> android:label="@string/recent_manga">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@@ -125,6 +127,11 @@
android:resource="@xml/widget_recent" /> android:resource="@xml/widget_recent" />
</receiver> </receiver>
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
android:value="false" />
<meta-data android:name="android.webkit.WebView.MetricsOptOut"
android:value="true" />
</application> </application>
</manifest> </manifest>

View File

@@ -8,6 +8,7 @@ import androidx.annotation.CallSuper
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.google.android.material.dialog.MaterialAlertDialogBuilder
abstract class AlertDialogFragment<B : ViewBinding> : DialogFragment() { abstract class AlertDialogFragment<B : ViewBinding> : DialogFragment() {
@@ -20,7 +21,7 @@ abstract class AlertDialogFragment<B : ViewBinding> : DialogFragment() {
val inflater = activity?.layoutInflater ?: LayoutInflater.from(requireContext()) val inflater = activity?.layoutInflater ?: LayoutInflater.from(requireContext())
val binding = onInflateView(inflater, null) val binding = onInflateView(inflater, null)
viewBinding = binding viewBinding = binding
return AlertDialog.Builder(requireContext(), theme) return MaterialAlertDialogBuilder(requireContext(), theme)
.setView(binding.root) .setView(binding.root)
.also(::onBuildDialog) .also(::onBuildDialog)
.create() .create()

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.base.ui package org.koitharu.kotatsu.base.ui
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.KeyEvent import android.view.KeyEvent
import android.view.MenuItem import android.view.MenuItem
@@ -97,4 +98,16 @@ abstract class BaseActivity<B : ViewBinding> : AppCompatActivity(), OnApplyWindo
super.onSupportActionModeFinished(mode) super.onSupportActionModeFinished(mode)
window?.statusBarColor = getThemeColor(android.R.attr.statusBarColor) 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()
}
}
} }

View File

@@ -6,6 +6,7 @@ import android.view.LayoutInflater
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.databinding.DialogCheckboxBinding import org.koitharu.kotatsu.databinding.DialogCheckboxBinding
class CheckBoxAlertDialog private constructor(private val delegate: AlertDialog) : 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 binding = DialogCheckboxBinding.inflate(LayoutInflater.from(context))
private val delegate = AlertDialog.Builder(context) private val delegate = MaterialAlertDialogBuilder(context)
.setView(binding.root) .setView(binding.root)
fun setTitle(@StringRes titleResId: Int): Builder { fun setTitle(@StringRes titleResId: Int): Builder {

View File

@@ -7,6 +7,7 @@ import android.view.ViewGroup
import android.widget.BaseAdapter import android.widget.BaseAdapter
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.ItemStorageBinding import org.koitharu.kotatsu.databinding.ItemStorageBinding
import org.koitharu.kotatsu.local.domain.LocalMangaRepository 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) { class Builder(context: Context, defaultValue: File?, listener: OnStorageSelectListener) {
private val adapter = VolumesAdapter(context) private val adapter = VolumesAdapter(context)
private val delegate = AlertDialog.Builder(context) private val delegate = MaterialAlertDialogBuilder(context)
init { init {
if (adapter.isEmpty) { if (adapter.isEmpty) {

View File

@@ -6,6 +6,7 @@ import android.text.InputFilter
import android.view.LayoutInflater import android.view.LayoutInflater
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.databinding.DialogInputBinding import org.koitharu.kotatsu.databinding.DialogInputBinding
class TextInputDialog private constructor( class TextInputDialog private constructor(
@@ -18,7 +19,7 @@ class TextInputDialog private constructor(
private val binding = DialogInputBinding.inflate(LayoutInflater.from(context)) private val binding = DialogInputBinding.inflate(LayoutInflater.from(context))
private val delegate = AlertDialog.Builder(context) private val delegate = MaterialAlertDialogBuilder(context)
.setView(binding.root) .setView(binding.root)
fun setTitle(@StringRes titleResId: Int): Builder { fun setTitle(@StringRes titleResId: Int): Builder {

View File

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

View File

@@ -9,5 +9,6 @@ data class MangaChapter(
val name: String, val name: String,
val number: Int, val number: Int,
val url: String, val url: String,
val branch: String? = null,
val source: MangaSource val source: MangaSource
) : Parcelable ) : Parcelable

View File

@@ -90,6 +90,8 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) :
chapters = ArrayList(total) chapters = ArrayList(total)
for (i in 0 until total) { for (i in 0 until total) {
val item = list.getJSONObject(i) val item = list.getJSONObject(i)
val chapterId = item.getLong("chapter_id")
val branchName = item.getStringOrNull("username")
val url = buildString { val url = buildString {
append(manga.url) append(manga.url)
append("/v") append("/v")
@@ -106,9 +108,10 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) :
} }
chapters.add( chapters.add(
MangaChapter( MangaChapter(
id = generateUid(url), id = generateUid(chapterId),
url = url, url = url,
source = source, source = source,
branch = branchName,
number = total - i, number = total - i,
name = name name = name
) )

View File

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

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.details.ui
import android.app.ActivityOptions import android.app.ActivityOptions
import android.os.Bundle import android.os.Bundle
import android.view.* import android.view.*
import android.widget.AdapterView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.core.graphics.Insets 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.MangaChapter
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.databinding.FragmentChaptersBinding 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.ChaptersAdapter
import org.koitharu.kotatsu.details.ui.adapter.ChaptersSelectionDecoration import org.koitharu.kotatsu.details.ui.adapter.ChaptersSelectionDecoration
import org.koitharu.kotatsu.details.ui.model.ChapterListItem 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 import org.koitharu.kotatsu.reader.ui.ReaderState
class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(), class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
OnListItemClickListener<MangaChapter>, ActionMode.Callback { OnListItemClickListener<MangaChapter>, ActionMode.Callback, AdapterView.OnItemSelectedListener {
private val viewModel by sharedViewModel<DetailsViewModel>() private val viewModel by sharedViewModel<DetailsViewModel>()
@@ -48,14 +50,31 @@ class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
chaptersAdapter = ChaptersAdapter(this) chaptersAdapter = ChaptersAdapter(this)
selectionDecoration = ChaptersSelectionDecoration(view.context) selectionDecoration = ChaptersSelectionDecoration(view.context)
with(binding.recyclerViewChapters) { with(binding.recyclerViewChapters) {
addItemDecoration(DividerItemDecoration(view.context, RecyclerView.VERTICAL)) addItemDecoration(
DividerItemDecoration(
view.context,
RecyclerView.VERTICAL
)
)
addItemDecoration(selectionDecoration!!) addItemDecoration(selectionDecoration!!)
setHasFixedSize(true) setHasFixedSize(true)
adapter = chaptersAdapter adapter = chaptersAdapter
} }
val branchesAdapter = BranchesAdapter()
binding.spinnerBranches.adapter = branchesAdapter
binding.spinnerBranches.onItemSelectedListener = this
viewModel.isLoading.observe(viewLifecycleOwner, this::onLoadingStateChanged) viewModel.isLoading.observe(viewLifecycleOwner, this::onLoadingStateChanged)
viewModel.chapters.observe(viewLifecycleOwner, this::onChaptersChanged) 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) { viewModel.isChaptersReversed.observe(viewLifecycleOwner) {
activity?.invalidateOptionsMenu() activity?.invalidateOptionsMenu()
} }
@@ -64,6 +83,7 @@ class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
override fun onDestroyView() { override fun onDestroyView() {
chaptersAdapter = null chaptersAdapter = null
selectionDecoration = null selectionDecoration = null
binding.spinnerBranches.adapter = null
super.onDestroyView() 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 { override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
val manga = viewModel.manga.value val manga = viewModel.manga.value
mode.menuInflater.inflate(R.menu.mode_chapters, menu) mode.menuInflater.inflate(R.menu.mode_chapters, menu)
@@ -174,7 +200,7 @@ class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
binding.recyclerViewChapters.updatePadding( binding.recyclerViewChapters.updatePadding(
left = insets.left, left = insets.left,
right = insets.right, right = insets.right,
bottom = insets.bottom bottom = insets.bottom + binding.spinnerBranches.height
) )
} }

View File

@@ -7,7 +7,6 @@ import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
@@ -15,6 +14,7 @@ import androidx.core.graphics.Insets
import androidx.core.net.toFile import androidx.core.net.toFile
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
@@ -144,7 +144,7 @@ class DetailsActivity : BaseActivity<ActivityDetailsBinding>(),
} }
R.id.action_delete -> { R.id.action_delete -> {
viewModel.manga.value?.let { m -> viewModel.manga.value?.let { m ->
AlertDialog.Builder(this) MaterialAlertDialogBuilder(this)
.setTitle(R.string.delete_manga) .setTitle(R.string.delete_manga)
.setMessage(getString(R.string.text_delete_local_manga, m.title)) .setMessage(getString(R.string.text_delete_local_manga, m.title))
.setPositiveButton(R.string.delete) { _, _ -> .setPositiveButton(R.string.delete) { _, _ ->
@@ -159,7 +159,7 @@ class DetailsActivity : BaseActivity<ActivityDetailsBinding>(),
viewModel.manga.value?.let { viewModel.manga.value?.let {
val chaptersCount = it.chapters?.size ?: 0 val chaptersCount = it.chapters?.size ?: 0
if (chaptersCount > 5) { if (chaptersCount > 5) {
AlertDialog.Builder(this) MaterialAlertDialogBuilder(this)
.setTitle(R.string.save_manga) .setTitle(R.string.save_manga)
.setMessage( .setMessage(
getString( getString(

View File

@@ -13,12 +13,14 @@ import androidx.core.view.updatePadding
import coil.ImageLoader import coil.ImageLoader
import coil.util.CoilUtils import coil.util.CoilUtils
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.sharedViewModel import org.koin.androidx.viewmodel.ext.android.sharedViewModel
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment 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.Manga
import org.koitharu.kotatsu.core.model.MangaHistory import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.databinding.FragmentDetailsBinding import org.koitharu.kotatsu.databinding.FragmentDetailsBinding
@@ -34,6 +36,7 @@ class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickList
private val viewModel by sharedViewModel<DetailsViewModel>() private val viewModel by sharedViewModel<DetailsViewModel>()
private val coil by inject<ImageLoader>() private val coil by inject<ImageLoader>()
private var tagsJob: Job? = null
override fun onInflateView( override fun onInflateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@@ -67,47 +70,11 @@ class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickList
ratingBar.progress = (ratingBar.max * manga.rating).roundToInt() ratingBar.progress = (ratingBar.max * manga.rating).roundToInt()
ratingBar.isVisible = true 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) imageViewFavourite.setOnClickListener(this@DetailsFragment)
buttonRead.setOnClickListener(this@DetailsFragment) buttonRead.setOnClickListener(this@DetailsFragment)
buttonRead.setOnLongClickListener(this@DetailsFragment) buttonRead.setOnLongClickListener(this@DetailsFragment)
buttonRead.isEnabled = !manga.chapters.isNullOrEmpty() buttonRead.isEnabled = !manga.chapters.isNullOrEmpty()
bindTags(manga)
} }
} }
@@ -191,4 +158,39 @@ class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickList
bottom = insets.bottom 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)
}
}
} }

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.details.ui package org.koitharu.kotatsu.details.ui
import androidx.lifecycle.asFlow
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers 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.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.mapToSet
import java.io.IOException import java.io.IOException
class DetailsViewModel( class DetailsViewModel(
@@ -31,6 +33,7 @@ class DetailsViewModel(
) : BaseViewModel() { ) : BaseViewModel() {
private val mangaData = MutableStateFlow<Manga?>(intent.manga) private val mangaData = MutableStateFlow<Manga?>(intent.manga)
private val selectedBranch = MutableStateFlow<String?>(null)
private val history = mangaData.mapNotNull { it?.id } private val history = mangaData.mapNotNull { it?.id }
.distinctUntilChanged() .distinctUntilChanged()
@@ -69,12 +72,24 @@ class DetailsViewModel(
val onMangaRemoved = SingleLiveEvent<Manga>() 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( val chapters = combine(
mangaData.map { it?.chapters.orEmpty() }, mangaData.map { it?.chapters.orEmpty() },
history.map { it?.chapterId }, history.map { it?.chapterId },
newChapters, newChapters,
chaptersReversed chaptersReversed,
) { chapters, currentId, newCount, reversed -> selectedBranch
) { chapters, currentId, newCount, reversed, branch ->
val currentIndex = chapters.indexOfFirst { it.id == currentId } val currentIndex = chapters.indexOfFirst { it.id == currentId }
val firstNewIndex = chapters.size - newCount val firstNewIndex = chapters.size - newCount
val res = chapters.mapIndexed { index, chapter -> val res = chapters.mapIndexed { index, chapter ->
@@ -86,7 +101,7 @@ class DetailsViewModel(
else -> ChapterExtra.UNREAD else -> ChapterExtra.UNREAD
} }
) )
} }.filter { it.chapter.branch == branch }
if (reversed) res.asReversed() else res if (reversed) res.asReversed() else res
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default) }.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
@@ -96,6 +111,15 @@ class DetailsViewModel(
?: throw MangaNotFoundException("Cannot find manga") ?: throw MangaNotFoundException("Cannot find manga")
mangaData.value = manga mangaData.value = manga
manga = manga.source.repository.getDetails(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 mangaData.value = manga
} }
} }
@@ -114,4 +138,8 @@ class DetailsViewModel(
fun setChaptersReversed(newValue: Boolean) { fun setChaptersReversed(newValue: Boolean) {
settings.chaptersReverse = newValue settings.chaptersReverse = newValue
} }
fun setSelectedBranch(branch: String?) {
selectedBranch.value = branch
}
} }

View File

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

View File

@@ -25,6 +25,14 @@ class FavouritesPagerAdapter(
return FavouritesListFragment.newInstance(item.id) 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) { override fun onConfigureTab(tab: TabLayout.Tab, position: Int) {
val item = differ.currentList[position] val item = differ.currentList[position]
tab.text = item.title tab.text = item.title

View File

@@ -2,7 +2,7 @@ package org.koitharu.kotatsu.favourites.ui.categories
import android.content.Context import android.content.Context
import android.text.InputType 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.R
import org.koitharu.kotatsu.base.ui.dialog.TextInputDialog import org.koitharu.kotatsu.base.ui.dialog.TextInputDialog
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
@@ -13,7 +13,7 @@ class CategoriesEditDelegate(
) { ) {
fun deleteCategory(category: FavouriteCategory) { fun deleteCategory(category: FavouriteCategory) {
AlertDialog.Builder(context) MaterialAlertDialogBuilder(context)
.setMessage(context.getString(R.string.category_delete_confirm, category.title)) .setMessage(context.getString(R.string.category_delete_confirm, category.title))
.setTitle(R.string.remove_category) .setTitle(R.string.remove_category)
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)

View File

@@ -5,7 +5,7 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.appcompat.app.AlertDialog import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
@@ -42,7 +42,7 @@ class HistoryListFragment : MangaListFragment() {
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) { return when (item.itemId) {
R.id.action_clear_history -> { R.id.action_clear_history -> {
AlertDialog.Builder(context ?: return false) MaterialAlertDialogBuilder(context ?: return false)
.setTitle(R.string.clear_history) .setTitle(R.string.clear_history)
.setMessage(R.string.text_clear_history_prompt) .setMessage(R.string.text_clear_history_prompt)
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)

View File

@@ -55,6 +55,7 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
spanSizeLookup.invalidateCache() spanSizeLookup.invalidateCache()
} }
open val isSwipeRefreshEnabled = true open val isSwipeRefreshEnabled = true
private var drawer: DrawerLayout? = null
protected abstract val viewModel: MangaListViewModel protected abstract val viewModel: MangaListViewModel
@@ -70,7 +71,8 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) 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) listAdapter = MangaListAdapter(get(), viewLifecycleOwner, this, ::resolveException)
paginationListener = PaginationScrollListener(4, this) paginationListener = PaginationScrollListener(4, this)
with(binding.recyclerView) { with(binding.recyclerView) {
@@ -101,6 +103,7 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
} }
override fun onDestroyView() { override fun onDestroyView() {
drawer = null
listAdapter = null listAdapter = null
paginationListener = null paginationListener = null
spanSizeLookup.invalidateCache() spanSizeLookup.invalidateCache()
@@ -118,15 +121,15 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
true true
} }
R.id.action_filter -> { R.id.action_filter -> {
binding.drawer?.toggleDrawer(GravityCompat.END) drawer?.toggleDrawer(GravityCompat.END)
true true
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
override fun onPrepareOptionsMenu(menu: Menu) { override fun onPrepareOptionsMenu(menu: Menu) {
menu.findItem(R.id.action_filter).isVisible = binding.drawer != null && menu.findItem(R.id.action_filter).isVisible = drawer != null &&
binding.drawer?.getDrawerLockMode(GravityCompat.END) != DrawerLayout.LOCK_MODE_LOCKED_CLOSED drawer?.getDrawerLockMode(GravityCompat.END) != DrawerLayout.LOCK_MODE_LOCKED_CLOSED
super.onPrepareOptionsMenu(menu) super.onPrepareOptionsMenu(menu)
} }
@@ -199,7 +202,7 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
state = config.currentFilter, state = config.currentFilter,
listener = this listener = this
) )
binding.drawer?.setDrawerLockMode( drawer?.setDrawerLockMode(
if (config.sortOrders.isEmpty() && config.tags.isEmpty()) { if (config.sortOrders.isEmpty() && config.tags.isEmpty()) {
DrawerLayout.LOCK_MODE_LOCKED_CLOSED DrawerLayout.LOCK_MODE_LOCKED_CLOSED
} else { } else {
@@ -214,7 +217,7 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
@CallSuper @CallSuper
override fun onFilterChanged(filter: MangaFilter) { override fun onFilterChanged(filter: MangaFilter) {
binding.drawer?.closeDrawers() drawer?.closeDrawers()
} }
override fun onWindowInsetsChanged(insets: Insets) { override fun onWindowInsetsChanged(insets: Insets) {

View File

@@ -72,6 +72,7 @@ class MangaIndex(source: String?) {
jo.put("number", chapter.number) jo.put("number", chapter.number)
jo.put("url", chapter.url) jo.put("url", chapter.url)
jo.put("name", chapter.name) jo.put("name", chapter.name)
jo.put("branch", chapter.branch)
jo.put("entries", "%03d\\d{3}".format(chapter.number)) jo.put("entries", "%03d\\d{3}".format(chapter.number))
chapters.put(chapter.id.toString(), jo) chapters.put(chapter.id.toString(), jo)
} }
@@ -97,6 +98,7 @@ class MangaIndex(source: String?) {
name = v.getString("name"), name = v.getString("name"),
url = v.getString("url"), url = v.getString("url"),
number = v.getInt("number"), number = v.getInt("number"),
branch = v.getStringOrNull("branch"),
source = source source = source
) )
) )

View File

@@ -7,6 +7,8 @@ import android.webkit.MimeTypeMap
import androidx.collection.ArraySet import androidx.collection.ArraySet
import androidx.core.net.toFile import androidx.core.net.toFile
import androidx.core.net.toUri import androidx.core.net.toUri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.core.model.* import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.local.data.CbzFilter 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 { val file = runCatching {
Uri.parse(localManga.url).toFile() Uri.parse(localManga.url).toFile()
}.getOrNull() ?: return null }.getOrNull() ?: return null
val zip = ZipFile(file) return withContext(Dispatchers.IO) {
val entry = zip.getEntry(MangaZip.INDEX_ENTRY) val zip = ZipFile(file)
val index = entry?.let(zip::readText)?.let(::MangaIndex) ?: return null val entry = zip.getEntry(MangaZip.INDEX_ENTRY)
return index.getMangaInfo() val index = entry?.let(zip::readText)?.let(::MangaIndex) ?: return@withContext null
index.getMangaInfo()
}
} }
private fun zipUri(file: File, entryName: String) = private fun zipUri(file: File, entryName: String) =

View File

@@ -9,7 +9,7 @@ import android.view.MenuItem
import android.view.View import android.view.View
import androidx.activity.result.ActivityResultCallback import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.contract.ActivityResultContracts 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 com.google.android.material.snackbar.Snackbar
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
@@ -77,7 +77,7 @@ class LocalListFragment : MangaListFragment(), ActivityResultCallback<Uri> {
override fun onPopupMenuItemSelected(item: MenuItem, data: Manga): Boolean { override fun onPopupMenuItemSelected(item: MenuItem, data: Manga): Boolean {
return when (item.itemId) { return when (item.itemId) {
R.id.action_delete -> { R.id.action_delete -> {
AlertDialog.Builder(context ?: return false) MaterialAlertDialogBuilder(context ?: return false)
.setTitle(R.string.delete_manga) .setTitle(R.string.delete_manga)
.setMessage(getString(R.string.text_delete_local_manga, data.title)) .setMessage(getString(R.string.text_delete_local_manga, data.title))
.setPositiveButton(R.string.delete) { _, _ -> .setPositiveButton(R.string.delete) { _, _ ->

View File

@@ -99,6 +99,7 @@ class LocalListViewModel(
runCatching { runCatching {
historyRepository.deleteOrSwap(manga, original) historyRepository.deleteOrSwap(manga, original)
} }
mangaList.value = mangaList.value?.filterNot { it.id == manga.id }
} }
shortcutsRepository.updateShortcuts() shortcutsRepository.updateShortcuts()
onMangaRemoved.call(manga) onMangaRemoved.call(manga)

View File

@@ -88,12 +88,12 @@ class PageLoader(
} }
suspend fun convertInPlace(file: File) { suspend fun convertInPlace(file: File) {
convertLock.withLock(file) { convertLock.withLock(Lock) {
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
val image = BitmapFactory.decodeFile(file.absolutePath) val image = BitmapFactory.decodeFile(file.absolutePath)
try { try {
file.outputStream().use { out -> file.outputStream().use { out ->
image.compress(Bitmap.CompressFormat.WEBP, 100, out) image.compress(Bitmap.CompressFormat.PNG, 100, out)
} }
} finally { } finally {
image.recycle() image.recycle()
@@ -101,4 +101,6 @@ class PageLoader(
} }
} }
} }
private companion object Lock
} }

View File

@@ -10,12 +10,12 @@ import android.view.*
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.ActivityResultCallback import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.* import androidx.core.view.*
import androidx.fragment.app.commit import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@@ -86,7 +86,7 @@ class ReaderActivity : BaseFullscreenActivity<ActivityReaderBinding>(),
viewModel.onError.observe(this, this::onError) viewModel.onError.observe(this, this::onError)
viewModel.readerMode.observe(this, this::onInitReader) viewModel.readerMode.observe(this, this::onInitReader)
viewModel.onPageSaved.observe(this, this::onPageSaved) 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.isLoading.observe(this, this::onLoadingStateChanged)
viewModel.content.observe(this) { viewModel.content.observe(this) {
onLoadingStateChanged(viewModel.isLoading.value == true) 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 { override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.opt_reader_top, menu) menuInflater.inflate(R.menu.opt_reader_top, menu)
return super.onCreateOptionsMenu(menu) return super.onCreateOptionsMenu(menu)
@@ -204,11 +199,15 @@ class ReaderActivity : BaseFullscreenActivity<ActivityReaderBinding>(),
private fun onLoadingStateChanged(isLoading: Boolean) { private fun onLoadingStateChanged(isLoading: Boolean) {
val hasPages = !viewModel.content.value?.pages.isNullOrEmpty() val hasPages = !viewModel.content.value?.pages.isNullOrEmpty()
binding.layoutLoading.isVisible = isLoading && !hasPages 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) { private fun onError(e: Throwable) {
val dialog = AlertDialog.Builder(this) val dialog = MaterialAlertDialogBuilder(this)
.setTitle(R.string.error_occurred) .setTitle(R.string.error_occurred)
.setMessage(e.getDisplayMessage(resources)) .setMessage(e.getDisplayMessage(resources))
.setPositiveButton(R.string.close, null) .setPositiveButton(R.string.close, null)
@@ -336,19 +335,25 @@ class ReaderActivity : BaseFullscreenActivity<ActivityReaderBinding>(),
setUiIsVisible(!binding.appbarTop.isVisible) 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_) title = uiState.chapterName ?: uiState.mangaName ?: getString(R.string.loading_)
supportActionBar?.subtitle = if (uiState.chapterNumber in 1..uiState.chaptersTotal) { supportActionBar?.subtitle = if (uiState.chapterNumber in 1..uiState.chaptersTotal) {
getString(R.string.chapter_d_of_d, uiState.chapterNumber, uiState.chaptersTotal) getString(R.string.chapter_d_of_d, uiState.chapterNumber, uiState.chaptersTotal)
} else { } else {
null null
} }
if (previous?.chapterName != null && uiState.chapterName != previous.chapterName) {
if (!uiState.chapterName.isNullOrEmpty()) {
binding.toastView.showTemporary(uiState.chapterName, TOAST_DURATION)
}
}
} }
companion object { companion object {
const val ACTION_MANGA_READ = "${BuildConfig.APPLICATION_ID}.action.READ_MANGA" const val ACTION_MANGA_READ = "${BuildConfig.APPLICATION_ID}.action.READ_MANGA"
private const val EXTRA_STATE = "state" private const val EXTRA_STATE = "state"
private const val TOAST_DURATION = 1500L
fun newIntent(context: Context, manga: Manga, state: ReaderState?): Intent { fun newIntent(context: Context, manga: Manga, state: ReaderState?): Intent {
return Intent(context, ReaderActivity::class.java) return Intent(context, ReaderActivity::class.java)

View File

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

View File

@@ -8,8 +8,6 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import okhttp3.OkHttpClient
import okhttp3.Request
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.get import org.koin.core.component.get
import org.koitharu.kotatsu.base.domain.MangaDataRepository 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.Manga
import org.koitharu.kotatsu.core.model.MangaChapter import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.core.model.MangaPage 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.os.ShortcutsRepository
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ReaderMode import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.history.domain.HistoryRepository 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.ReaderPage
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
import org.koitharu.kotatsu.utils.MediaStoreCompat import org.koitharu.kotatsu.utils.MediaStoreCompat
@@ -42,7 +40,7 @@ class ReaderViewModel(
private var loadingJob: Job? = null private var loadingJob: Job? = null
private val currentState = MutableStateFlow<ReaderState?>(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>() private val chapters = LongSparseArray<MangaChapter>()
val readerMode = MutableLiveData<ReaderMode>() val readerMode = MutableLiveData<ReaderMode>()
@@ -60,7 +58,7 @@ class ReaderViewModel(
) )
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default) }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
val content = MutableLiveData<ReaderContent>(ReaderContent(emptyList(), null)) val content = MutableLiveData(ReaderContent(emptyList(), null))
val manga: Manga? val manga: Manga?
get() = mangaData.value get() = mangaData.value
@@ -82,7 +80,6 @@ class ReaderViewModel(
manga.chapters?.forEach { manga.chapters?.forEach {
chapters.put(it.id, it) chapters.put(it.id, it)
} }
mangaData.value = manga
// determine mode // determine mode
val mode = val mode =
dataRepository.getReaderMode(manga.id) ?: manga.chapters?.randomOrNull()?.let { dataRepository.getReaderMode(manga.id) ?: manga.chapters?.randomOrNull()?.let {
@@ -98,6 +95,9 @@ class ReaderViewModel(
currentState.value = state ?: historyRepository.getOne(manga)?.let { currentState.value = state ?: historyRepository.getOne(manga)?.let {
ReaderState.from(it) ReaderState.from(it)
} ?: ReaderState.initial(manga) } ?: 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) readerMode.postValue(mode)
val pages = loadChapter(requireNotNull(currentState.value).chapterId) val pages = loadChapter(requireNotNull(currentState.value).chapterId)
@@ -156,20 +156,11 @@ class ReaderViewModel(
}?.toMangaPage() ?: error("Page not found") }?.toMangaPage() ?: error("Page not found")
val repo = page.source.repository val repo = page.source.repository
val pageUrl = repo.getPageUrl(page) val pageUrl = repo.getPageUrl(page)
val request = Request.Builder() val file = get<PagesCache>()[pageUrl] ?: error("Page not found in cache")
.url(pageUrl) val uri = file.inputStream().use { input ->
.header(CommonHeaders.REFERER, page.referer) val fileName = URLUtil.guessFileName(pageUrl, null, null)
.get()
.build()
val uri = get<OkHttpClient>().newCall(request).await().use { response ->
val fileName =
URLUtil.guessFileName(
pageUrl,
response.contentDisposition,
response.mimeType
)
MediaStoreCompat(resolver).insertImage(fileName) { MediaStoreCompat(resolver).insertImage(fileName) {
checkNotNull(response.body).byteStream().copyTo(it) input.copyTo(it)
} }
} }
onPageSaved.postCall(uri) onPageSaved.postCall(uri)

View File

@@ -18,29 +18,34 @@ abstract class BaseReader<B : ViewBinding> : BaseFragment<B>() {
protected val loader by lazy(LazyThreadSafetyMode.NONE) { protected val loader by lazy(LazyThreadSafetyMode.NONE) {
PageLoader(lifecycleScope, get(), get()) PageLoader(lifecycleScope, get(), get())
} }
private var lastReaderState: ReaderState? = null private var stateToSave: ReaderState? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
lastReaderState = savedInstanceState?.getParcelable(KEY_STATE) ?: lastReaderState var restoredState = savedInstanceState?.getParcelable<ReaderState?>(KEY_STATE)
viewModel.content.observe(viewLifecycleOwner) { viewModel.content.observe(viewLifecycleOwner) {
onPagesChanged(it.pages, lastReaderState ?: it.state) onPagesChanged(it.pages, restoredState ?: it.state)
lastReaderState = null restoredState = null
} }
} }
override fun onPause() {
super.onPause()
viewModel.saveCurrentState(getCurrentState())
}
override fun onDestroyView() { override fun onDestroyView() {
lastReaderState = getCurrentState() stateToSave = getCurrentState()
super.onDestroyView() super.onDestroyView()
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
getCurrentState()?.let { getCurrentState()?.let {
lastReaderState = it stateToSave = it
} }
outState.putParcelable(KEY_STATE, lastReaderState) outState.putParcelable(KEY_STATE, stateToSave)
} }
override fun onWindowInsetsChanged(insets: Insets) = Unit override fun onWindowInsetsChanged(insets: Insets) = Unit

View File

@@ -21,6 +21,7 @@ abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
init { init {
setHasStableIds(true) setHasStableIds(true)
stateRestorationPolicy = StateRestorationPolicy.PREVENT
} }
override fun onBindViewHolder(holder: H, position: Int) { override fun onBindViewHolder(holder: H, position: Int) {

View File

@@ -12,7 +12,9 @@ import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.reader.ui.pager.BaseReader import org.koitharu.kotatsu.reader.ui.pager.BaseReader
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
import kotlin.math.absoluteValue
class ReversedReaderFragment : BaseReader<FragmentReaderStandardBinding>() { class ReversedReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
@@ -58,7 +60,10 @@ class ReversedReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
} }
override fun switchPageTo(position: Int, smooth: Boolean) { override fun switchPageTo(position: Int, smooth: Boolean) {
binding.pager.setCurrentItem(reversed(position), smooth) binding.pager.setCurrentItem(
reversed(position),
smooth && (binding.pager.currentItem - position).absoluteValue < PagerReaderFragment.SMOOTH_SCROLL_LIMIT
)
} }
override fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) { override fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) {

View File

@@ -13,6 +13,7 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReader
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
import kotlin.math.absoluteValue
class PagerReaderFragment : BaseReader<FragmentReaderStandardBinding>() { class PagerReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
@@ -78,7 +79,10 @@ class PagerReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
} }
override fun switchPageTo(position: Int, smooth: Boolean) { override fun switchPageTo(position: Int, smooth: Boolean) {
binding.pager.setCurrentItem(position, smooth) binding.pager.setCurrentItem(
position,
smooth && (binding.pager.currentItem - position).absoluteValue < SMOOTH_SCROLL_LIMIT
)
} }
override fun getCurrentState(): ReaderState? = bindingOrNull()?.run { override fun getCurrentState(): ReaderState? = bindingOrNull()?.run {
@@ -94,4 +98,9 @@ class PagerReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
private fun notifyPageChanged(page: Int) { private fun notifyPageChanged(page: Int) {
viewModel.onCurrentPageChanged(page) viewModel.onCurrentPageChanged(page)
} }
companion object {
const val SMOOTH_SCROLL_LIMIT = 3
}
} }

View File

@@ -88,10 +88,6 @@ class WebtoonReaderFragment : BaseReader<FragmentReaderWebtoonBinding>() {
} }
override fun switchPageTo(position: Int, smooth: Boolean) { override fun switchPageTo(position: Int, smooth: Boolean) {
if (smooth) { binding.recyclerView.firstItem = position
binding.recyclerView.smoothScrollToPosition(position)
} else {
binding.recyclerView.firstItem = position
}
} }
} }

View File

@@ -7,8 +7,8 @@ import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.annotation.MainThread import androidx.annotation.MainThread
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -77,7 +77,7 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
@MainThread @MainThread
private fun showUpdateDialog(version: AppVersion) { private fun showUpdateDialog(version: AppVersion) {
AlertDialog.Builder(activity) MaterialAlertDialogBuilder(activity)
.setTitle(R.string.app_update_available) .setTitle(R.string.app_update_available)
.setMessage(buildString { .setMessage(buildString {
append(activity.getString(R.string.new_version_s, version.name)) append(activity.getString(R.string.new_version_s, version.name))

View File

@@ -6,6 +6,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.AlertDialogFragment import org.koitharu.kotatsu.base.ui.AlertDialogFragment
@@ -40,7 +41,7 @@ class BackupDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
} }
private fun onError(e: Throwable) { private fun onError(e: Throwable) {
AlertDialog.Builder(context ?: return) MaterialAlertDialogBuilder(context ?: return)
.setNegativeButton(R.string.close, null) .setNegativeButton(R.string.close, null)
.setTitle(R.string.error) .setTitle(R.string.error)
.setMessage(e.getDisplayMessage(resources)) .setMessage(e.getDisplayMessage(resources))

View File

@@ -7,6 +7,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf import org.koin.core.parameter.parametersOf
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
@@ -44,7 +45,7 @@ class RestoreDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
} }
private fun onError(e: Throwable) { private fun onError(e: Throwable) {
AlertDialog.Builder(context ?: return) MaterialAlertDialogBuilder(context ?: return)
.setNegativeButton(R.string.close, null) .setNegativeButton(R.string.close, null)
.setTitle(R.string.error) .setTitle(R.string.error)
.setMessage(e.getDisplayMessage(resources)) .setMessage(e.getDisplayMessage(resources))
@@ -64,7 +65,7 @@ class RestoreDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
} }
private fun onRestoreDone(result: CompositeResult) { private fun onRestoreDone(result: CompositeResult) {
val builder = AlertDialog.Builder(context ?: return) val builder = MaterialAlertDialogBuilder(context ?: return)
when { when {
result.isAllSuccess -> builder.setTitle(R.string.data_restored) result.isAllSuccess -> builder.setTitle(R.string.data_restored)
.setMessage(R.string.data_restored_success) .setMessage(R.string.data_restored_success)

View File

@@ -28,13 +28,8 @@ class TrackingRepository(
} }
val tracks = db.tracksDao.findAll().groupBy { it.mangaId } val tracks = db.tracksDao.findAll().groupBy { it.mangaId }
return mangaList return mangaList
.mapNotNull { .filterNot { it.source == MangaSource.LOCAL }
if (it.source == MangaSource.LOCAL) { .distinctBy { it.id }
localMangaRepository.getRemoteManga(it)
} else {
it
}
}.distinctBy { it.id }
.map { manga -> .map { manga ->
val track = tracks[manga.id]?.singleOrNull() val track = tracks[manga.id]?.singleOrNull()
MangaTracking( MangaTracking(

View File

@@ -2,9 +2,9 @@ package org.koitharu.kotatsu.tracker.ui
import android.os.Bundle import android.os.Bundle
import android.view.* import android.view.*
import androidx.appcompat.app.AlertDialog
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import org.koin.android.ext.android.get import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
@@ -80,7 +80,7 @@ class FeedFragment : BaseFragment<FragmentFeedBinding>(), PaginationScrollListen
true true
} }
R.id.action_clear_feed -> { R.id.action_clear_feed -> {
AlertDialog.Builder(context ?: return false) MaterialAlertDialogBuilder(context ?: return false)
.setTitle(R.string.clear_updates_feed) .setTitle(R.string.clear_updates_feed)
.setMessage(R.string.text_clear_updates_feed_prompt) .setMessage(R.string.text_clear_updates_feed_prompt)
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)

View File

@@ -6,21 +6,37 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.TrackingLogItem import org.koitharu.kotatsu.core.model.TrackingLogItem
import org.koitharu.kotatsu.utils.ext.formatRelative import org.koitharu.kotatsu.utils.ext.formatRelative
fun TrackingLogItem.toFeedItem(resources: Resources) = FeedItem( fun TrackingLogItem.toFeedItem(resources: Resources): FeedItem {
id = id, val chaptersString = if (chapters.size > MAX_CHAPTERS) {
imageUrl = manga.coverUrl, chapters.joinToString(
title = manga.title, separator = "\n",
subtitle = buildString { limit = MAX_CHAPTERS - 1,
append(createdAt.formatRelative(DateUtils.DAY_IN_MILLIS)) truncated = resources.getString(
append(" ") R.string._and_x_more,
append( chapters.size - MAX_CHAPTERS + 1
resources.getQuantityString(
R.plurals.new_chapters,
chapters.size,
chapters.size
) )
) )
}, } else {
chapters = chapters.joinToString("\n"), chapters.joinToString("\n")
manga = manga }
) 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

View File

@@ -2,7 +2,8 @@ package org.koitharu.kotatsu.utils.anim
import android.view.View import android.view.View
import android.view.ViewPropertyAnimator 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 import org.koitharu.kotatsu.utils.ext.measureHeight
sealed class Motion { sealed class Motion {
@@ -69,27 +70,25 @@ sealed class Motion {
} }
} }
object CheckEffect : Motion() { object Toast : Motion() {
override fun reset(v: View) { override fun reset(v: View) {
v.scaleX = 1f v.alpha = 1f
v.scaleY = 1f
} }
override fun hideView(v: View) { override fun hideView(v: View) {
v.scaleX = 0f v.alpha = 0f
v.scaleY = 0f
} }
override fun hide(v: View, anim: ViewPropertyAnimator) { override fun hide(v: View, anim: ViewPropertyAnimator) {
anim.scaleX(0f) anim.alpha(0f)
anim.scaleY(0f) anim.translationY(v.measureHeight().toFloat())
anim.interpolator = AccelerateDecelerateInterpolator() anim.interpolator = AccelerateInterpolator()
} }
override fun show(v: View, anim: ViewPropertyAnimator) { override fun show(v: View, anim: ViewPropertyAnimator) {
anim.scaleY(1f) anim.alpha(1f)
anim.scaleX(1f) anim.translationY(0f)
anim.interpolator = AccelerateDecelerateInterpolator() anim.interpolator = DecelerateInterpolator()
} }
} }
} }

View File

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

View File

@@ -22,7 +22,6 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup import com.google.android.material.chip.ChipGroup
import org.koitharu.kotatsu.core.ui.ChipsFactory
fun View.hideKeyboard() { fun View.hideKeyboard() {
val imm = context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager 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 val RecyclerView.hasItems: Boolean
get() = (adapter?.itemCount ?: 0) > 0 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() { fun RecyclerView.clearItemDecorations() {
while (itemDecorationCount > 0) { while (itemDecorationCount > 0) {
removeItemDecorationAt(0) removeItemDecorationAt(0)

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

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

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

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

View File

@@ -23,13 +23,14 @@
<com.google.android.material.tabs.TabLayout <com.google.android.material.tabs.TabLayout
android:id="@+id/tabs" android:id="@+id/tabs"
style="@style/Widget.MaterialComponents.TabLayout.Colored" style="@style/Widget.MaterialComponents.TabLayout.PrimarySurface"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="end" android:layout_gravity="end"
android:background="@android:color/transparent" android:background="@android:color/transparent"
app:tabGravity="start" app:tabGravity="start"
app:tabMode="scrollable" /> app:tabMode="scrollable"
app:tabSelectedTextColor="?colorOnPrimarySurface" />
</com.google.android.material.appbar.MaterialToolbar> </com.google.android.material.appbar.MaterialToolbar>

View File

@@ -23,13 +23,14 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.3" /> app:layout_constraintWidth_percent="0.3" />
<org.koitharu.kotatsu.base.ui.widgets.ChipsView
<com.google.android.material.chip.ChipGroup
android:id="@+id/chips_tags" android:id="@+id/chips_tags"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:padding="6dp" android:padding="6dp"
app:chipSpacingHorizontal="4dp"
app:chipSpacingVertical="6dp"
app:layout_constraintEnd_toEndOf="@id/imageView_cover" app:layout_constraintEnd_toEndOf="@id/imageView_cover"
app:layout_constraintStart_toStartOf="@id/imageView_cover" app:layout_constraintStart_toStartOf="@id/imageView_cover"
app:layout_constraintTop_toBottomOf="@id/imageView_cover" /> app:layout_constraintTop_toBottomOf="@id/imageView_cover" />

View File

@@ -19,6 +19,8 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical"
android:padding="@dimen/grid_spacing_outer"
android:scrollbarStyle="outsideOverlay"
android:scrollbars="vertical" android:scrollbars="vertical"
app:fastScrollEnabled="true" app:fastScrollEnabled="true"
app:fastScrollHorizontalThumbDrawable="@drawable/list_thumb" app:fastScrollHorizontalThumbDrawable="@drawable/list_thumb"

View File

@@ -2,6 +2,7 @@
<FrameLayout <FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:keepScreenOn="true"> android:keepScreenOn="true">
@@ -11,14 +12,19 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />
<ProgressBar <org.koitharu.kotatsu.reader.ui.ReaderToastView
android:id="@+id/progressBar_bottom" android:id="@+id/toastView"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:indeterminate="true" android:layout_gravity="bottom|center_horizontal"
android:layout_gravity="bottom"
android:layout_marginBottom="20dp" 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 <com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_top" android:id="@+id/appbar_top"
@@ -45,7 +51,8 @@
android:background="@color/dim" android:background="@color/dim"
android:elevation="0dp" android:elevation="0dp"
android:theme="@style/AppToolbarTheme" android:theme="@style/AppToolbarTheme"
app:elevation="0dp"> app:elevation="0dp"
tools:visibility="gone">
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_bottom" android:id="@+id/toolbar_bottom"

View File

@@ -10,7 +10,6 @@
<TextView <TextView
style="@style/MaterialAlertDialog.MaterialComponents.Title.Text" style="@style/MaterialAlertDialog.MaterialComponents.Title.Text"
android:padding="16dp" android:padding="16dp"
android:textColor="?android:textColorSecondary"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/add_to_favourites" /> android:text="@string/add_to_favourites" />

View File

@@ -44,7 +44,7 @@
<TextView <TextView
android:id="@+id/textView_grid_title" android:id="@+id/textView_grid_title"
style="?android:attr/windowTitleStyle" style="@style/MaterialAlertDialog.MaterialComponents.Title.Text"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingLeft="?attr/dialogPreferredPadding" android:paddingLeft="?attr/dialogPreferredPadding"

View File

@@ -1,18 +1,34 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout <RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="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 <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView_chapters" android:id="@+id/recyclerView_chapters"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="match_parent" 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:orientation="vertical"
android:scrollbars="vertical" android:scrollbars="vertical"
app:fastScrollEnabled="true" app:fastScrollEnabled="true"
android:scrollbarStyle="outsideOverlay"
app:fastScrollHorizontalThumbDrawable="@drawable/list_thumb" app:fastScrollHorizontalThumbDrawable="@drawable/list_thumb"
app:fastScrollHorizontalTrackDrawable="@drawable/list_track" app:fastScrollHorizontalTrackDrawable="@drawable/list_track"
app:fastScrollVerticalThumbDrawable="@drawable/list_thumb" app:fastScrollVerticalThumbDrawable="@drawable/list_thumb"
@@ -25,8 +41,9 @@
style="@style/Widget.AppCompat.ProgressBar" style="@style/Widget.AppCompat.ProgressBar"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_gravity="center" android:layout_gravity="center"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />
</FrameLayout> </RelativeLayout>

View File

@@ -113,7 +113,7 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/barrier_title" /> 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:id="@+id/chips_tags"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@@ -10,7 +10,6 @@
android:id="@+id/tabs" android:id="@+id/tabs"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:elevation="4dp"
app:tabGravity="center" app:tabGravity="center"
app:tabMode="scrollable" /> app:tabMode="scrollable" />

View File

@@ -8,6 +8,8 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:clipToPadding="false" android:clipToPadding="false"
android:orientation="vertical" android:orientation="vertical"
android:padding="@dimen/grid_spacing_outer"
android:scrollbarStyle="outsideOverlay"
android:scrollbars="vertical" android:scrollbars="vertical"
app:fastScrollEnabled="true" app:fastScrollEnabled="true"
app:fastScrollHorizontalThumbDrawable="@drawable/list_thumb" app:fastScrollHorizontalThumbDrawable="@drawable/list_thumb"

View File

@@ -1,50 +1,45 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout <androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:orientation="vertical">
<androidx.drawerlayout.widget.DrawerLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/drawer" android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="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 <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView_filter" android:id="@+id/recyclerView"
android:layout_width="240dp" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="end"
android:background="?android:windowBackground"
android:clipToPadding="false" android:clipToPadding="false"
android:orientation="vertical" android:orientation="vertical"
android:padding="@dimen/grid_spacing_outer"
android:scrollbarStyle="outsideOverlay"
android:scrollbars="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" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_category_checkable" /> tools:listitem="@layout/item_manga_list" />
</androidx.drawerlayout.widget.DrawerLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</LinearLayout>
<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>

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

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

View File

@@ -23,6 +23,7 @@
android:ellipsize="end" android:ellipsize="end"
android:gravity="center_vertical|start" android:gravity="center_vertical|start"
android:lines="2" android:lines="2"
android:elegantTextHeight="true"
android:padding="6dp" android:padding="6dp"
android:textColor="?android:textColorPrimary" android:textColor="?android:textColorPrimary"
tools:text="@tools:sample/lorem" /> tools:text="@tools:sample/lorem" />

View File

@@ -43,8 +43,8 @@
android:id="@+id/recyclerView" android:id="@+id/recyclerView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:scrollbars="vertical"
android:padding="@dimen/grid_spacing" android:padding="@dimen/grid_spacing"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="3" app:spanCount="3"
tools:listitem="@layout/item_page_thumb" /> tools:listitem="@layout/item_page_thumb" />

View File

@@ -6,4 +6,5 @@
<color name="red_accent">#FF8A65</color> <color name="red_accent">#FF8A65</color>
<color name="dim">#99000000</color> <color name="dim">#99000000</color>
<color name="error">#E57373</color> <color name="error">#E57373</color>
<color name="tabs_line">#5E636A</color>
</resources> </resources>

View File

@@ -200,4 +200,5 @@
<string name="sign_in">Войти</string> <string name="sign_in">Войти</string>
<string name="auth_required">Для просмотра этого контента требуется авторизация</string> <string name="auth_required">Для просмотра этого контента требуется авторизация</string>
<string name="default_s">По умолчанию: %s</string> <string name="default_s">По умолчанию: %s</string>
<string name="_and_x_more">…и ещё %1$d</string>
</resources> </resources>

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="BaseAppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar"> <style name="BaseAppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="windowActionModeOverlay">true</item> <item name="windowActionModeOverlay">true</item>
<item name="actionModeCloseDrawable">@drawable/ic_cross</item> <item name="actionModeCloseDrawable">@drawable/ic_cross</item>
@@ -8,5 +9,7 @@
<item name="badgeStyle">@style/Widget.MaterialComponents.Badge</item> <item name="badgeStyle">@style/Widget.MaterialComponents.Badge</item>
<item name="android:statusBarColor">@color/status_bar</item> <item name="android:statusBarColor">@color/status_bar</item>
<item name="android:navigationBarColor">@android:color/transparent</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> </style>
</resources> </resources>

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<dimen name="grid_spacing">4dp</dimen> <dimen name="grid_spacing">4dp</dimen>
<dimen name="grid_spacing_outer">2dp</dimen>
<dimen name="preferred_grid_width">140dp</dimen> <dimen name="preferred_grid_width">140dp</dimen>
</resources> </resources>

View File

@@ -9,4 +9,5 @@
<color name="dim">#99000000</color> <color name="dim">#99000000</color>
<color name="error">#D32F2F</color> <color name="error">#D32F2F</color>
<color name="status_bar">#33000000</color> <color name="status_bar">#33000000</color>
<color name="tabs_line">#C3CFDD</color>
</resources> </resources>

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<dimen name="grid_spacing">2.5dp</dimen> <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_item_height">84dp</dimen>
<dimen name="manga_list_details_item_height">120dp</dimen> <dimen name="manga_list_details_item_height">120dp</dimen>
<dimen name="chapter_list_item_height">46dp</dimen> <dimen name="chapter_list_item_height">46dp</dimen>

View File

@@ -202,4 +202,5 @@
<string name="sign_in">Sign in</string> <string name="sign_in">Sign in</string>
<string name="auth_required">You should authorize to view this content</string> <string name="auth_required">You should authorize to view this content</string>
<string name="default_s">Default: %s</string> <string name="default_s">Default: %s</string>
<string name="_and_x_more">…and %1$d more</string>
</resources> </resources>

View File

@@ -25,4 +25,19 @@
<item name="background">@color/grey</item> <item name="background">@color/grey</item>
</style> </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> </resources>

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="BaseAppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar"> <style name="BaseAppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="android:windowActionModeOverlay">true</item> <item name="android:windowActionModeOverlay">true</item>
<item name="windowActionModeOverlay">true</item> <item name="windowActionModeOverlay">true</item>
@@ -8,6 +9,8 @@
<item name="preferenceTheme">@style/PreferenceThemeOverlay</item> <item name="preferenceTheme">@style/PreferenceThemeOverlay</item>
<item name="badgeStyle">@style/Widget.MaterialComponents.Badge</item> <item name="badgeStyle">@style/Widget.MaterialComponents.Badge</item>
<item name="android:statusBarColor">@color/status_bar</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>
<style name="AppTheme" parent="BaseAppTheme"> <style name="AppTheme" parent="BaseAppTheme">

View File

@@ -1,13 +1,12 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = '1.4.21'
repositories { repositories {
google() google()
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:4.1.2' 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 // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files

View File

@@ -0,0 +1,12 @@
Kotatsu is a free and open source manga reader for Android.<br>
Main Features:
<ul>
<li>Online manga catalogues</li>
<li>Search manga by name and genre</li>
<li>Reading history</li>
<li>Favourites organized by user-defined categories</li>
<li>Downloading manga and reading it offline. Third-party CBZ archives also supported</li>
<li>Tablet-optimized material design UI</li>
<li>Standard and Webtoon-optimized reader</li>
<li>Notifications about new chapters with updates feed</li>
</ul>

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

View File

@@ -0,0 +1 @@
Manga reader with online catalogues

View File

@@ -0,0 +1,12 @@
Kotatsu - приложения для чтения манги с открытым исходным кодом.<br>
Основные возможности:
<ul>
<li>Онлайн каталоги с мангой</li>
<li>Поиск манги по имени и жанрам</li>
<li>История чтения</li>
<li>Избранное с пользовательскими категориями</li>
<li>Возможность сохранять мангу и читать её оффлайн. Поддержка сторонних комиксов в формате CBZ</li>
<li>Интерфейс также оптимизирован для планшетов</li>
<li>Поддержка манхвы (Webtoon)</li>
<li>Уведомления о новых главах и лента обновлений</li>
</ul>

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

View File

@@ -0,0 +1 @@
Приложение для чтения манги с онлайн каталогами