Initial commit
1
app/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
77
app/build.gradle
Normal file
@@ -0,0 +1,77 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
def gitCommits = 'git rev-list --all --count'.execute([], rootDir).text.trim().toInteger()
|
||||
def gitBranch = 'git branch --show-current'.execute([], rootDir).text.trim()
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
|
||||
defaultConfig {
|
||||
applicationId "org.koitharu.kotatsu"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode gitCommits
|
||||
versionName "0.1"
|
||||
|
||||
buildConfigField 'String', 'GIT_BRANCH', "\"${gitBranch}\""
|
||||
}
|
||||
archivesBaseName = "kotatsu_${gitCommits}"
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
lintOptions {
|
||||
disable 'MissingTranslation'
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
}
|
||||
dependencies {
|
||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3'
|
||||
|
||||
implementation 'androidx.core:core-ktx:1.1.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha03'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha01'
|
||||
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0-alpha01'
|
||||
implementation 'androidx.preference:preference:1.1.0'
|
||||
implementation 'com.google.android.material:material:1.2.0-alpha04'
|
||||
|
||||
implementation 'androidx.room:room-runtime:2.2.3'
|
||||
implementation 'androidx.room:room-ktx:2.2.3'
|
||||
kapt 'androidx.room:room-compiler:2.2.3'
|
||||
|
||||
implementation 'com.github.moxy-community:moxy:2.0.2'
|
||||
implementation 'com.github.moxy-community:moxy-androidx:2.0.2'
|
||||
implementation 'com.github.moxy-community:moxy-material:2.0.2'
|
||||
implementation 'com.github.moxy-community:moxy-ktx:2.0.2'
|
||||
kapt 'com.github.moxy-community:moxy-compiler:2.0.2'
|
||||
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.3.1'
|
||||
implementation 'com.squareup.okio:okio:2.4.3'
|
||||
implementation 'org.jsoup:jsoup:1.12.1'
|
||||
|
||||
implementation 'org.koin:koin-android:2.0.1'
|
||||
implementation 'io.coil-kt:coil:0.9.2'
|
||||
implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0'
|
||||
|
||||
testImplementation 'junit:junit:4.13'
|
||||
}
|
||||
21
app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
26
app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.koitharu.kotatsu">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:name="org.koitharu.kotatsu.KotatsuApp"
|
||||
android:theme="@style/AppTheme"
|
||||
android:fullBackupContent="@xml/backup_descriptor">
|
||||
<activity android:name="org.koitharu.kotatsu.ui.main.MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
53
app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt
Normal file
@@ -0,0 +1,53 @@
|
||||
package org.koitharu.kotatsu
|
||||
|
||||
import android.app.Application
|
||||
import androidx.room.Room
|
||||
import okhttp3.OkHttpClient
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.android.ext.koin.androidLogger
|
||||
import org.koin.core.context.startKoin
|
||||
import org.koin.dsl.module
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.domain.MangaLoaderContext
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class KotatsuApp : Application() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
initKoin()
|
||||
}
|
||||
|
||||
private fun initKoin() {
|
||||
startKoin {
|
||||
androidLogger()
|
||||
androidContext(applicationContext)
|
||||
modules(listOf(
|
||||
module {
|
||||
factory {
|
||||
okHttp().build()
|
||||
}
|
||||
}, module {
|
||||
single {
|
||||
MangaLoaderContext(applicationContext)
|
||||
}
|
||||
}, module {
|
||||
single {
|
||||
mangaDb().build()
|
||||
}
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
private fun okHttp() = OkHttpClient.Builder()
|
||||
.connectTimeout(20, TimeUnit.SECONDS)
|
||||
.readTimeout(60, TimeUnit.SECONDS)
|
||||
.writeTimeout(20, TimeUnit.SECONDS)
|
||||
|
||||
private fun mangaDb() = Room.databaseBuilder(
|
||||
applicationContext,
|
||||
MangaDatabase::class.java,
|
||||
"kotatsu-db"
|
||||
)
|
||||
}
|
||||
12
app/src/main/java/org/koitharu/kotatsu/core/db/HistoryDao.kt
Normal file
@@ -0,0 +1,12 @@
|
||||
package org.koitharu.kotatsu.core.db
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import org.koitharu.kotatsu.core.db.entity.HistoryEntity
|
||||
|
||||
@Dao
|
||||
interface HistoryDao {
|
||||
|
||||
@Query("SELECT * FROM history")
|
||||
suspend fun getAll(): List<HistoryEntity>
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.koitharu.kotatsu.core.db
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import org.koitharu.kotatsu.core.db.entity.HistoryEntity
|
||||
|
||||
@Database(entities = [HistoryEntity::class], version = 1)
|
||||
abstract class MangaDatabase : RoomDatabase()
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.koitharu.kotatsu.core.db.entity
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "history")
|
||||
data class HistoryEntity(
|
||||
@PrimaryKey val id: Long
|
||||
)
|
||||
21
app/src/main/java/org/koitharu/kotatsu/core/model/Manga.kt
Normal file
@@ -0,0 +1,21 @@
|
||||
package org.koitharu.kotatsu.core.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class Manga(
|
||||
val id: Long,
|
||||
val title: String,
|
||||
val localizedTitle: String? = null,
|
||||
val url: String,
|
||||
val rating: Float = -1f, //normalized value [0..1] or -1
|
||||
val coverUrl: String,
|
||||
val largeCoverUrl: String? = null,
|
||||
val summary: String,
|
||||
val description: CharSequence? = null,
|
||||
val tags: Set<MangaTag> = emptySet(),
|
||||
val state: MangaState? = null,
|
||||
val chapters: List<MangaChapter>? = null,
|
||||
val source: MangaSource
|
||||
) : Parcelable
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.koitharu.kotatsu.core.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class MangaChapter(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val number: Int,
|
||||
val url: String,
|
||||
val source: MangaSource
|
||||
) : Parcelable
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.koitharu.kotatsu.core.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class MangaPage(
|
||||
val id: Long,
|
||||
val url: String,
|
||||
val preview: String? = null,
|
||||
val source: MangaSource
|
||||
) : Parcelable
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.koitharu.kotatsu.core.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import org.koitharu.kotatsu.domain.MangaRepository
|
||||
import org.koitharu.kotatsu.domain.repository.ReadmangaRepository
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
@Parcelize
|
||||
enum class MangaSource(val title: String, val cls: Class<out MangaRepository>): Parcelable {
|
||||
READMANGA_RU("ReadManga", ReadmangaRepository::class.java)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.koitharu.kotatsu.core.model
|
||||
|
||||
enum class MangaState {
|
||||
ONGOING, FINISHED
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.koitharu.kotatsu.core.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class MangaTag(
|
||||
val title: String,
|
||||
val key: String
|
||||
) : Parcelable
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.koitharu.kotatsu.core.model
|
||||
|
||||
enum class SortOrder {
|
||||
ALPHABETICAL, POPULARITY, UPDATED, NEWEST
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.koitharu.kotatsu.domain
|
||||
|
||||
import android.content.Context
|
||||
import okhttp3.*
|
||||
import org.koin.core.KoinComponent
|
||||
import org.koin.core.inject
|
||||
import org.koitharu.kotatsu.utils.ext.await
|
||||
|
||||
class MangaLoaderContext(context: Context) : KoinComponent {
|
||||
|
||||
private val okHttp by inject<OkHttpClient>()
|
||||
private val preferences = context.getSharedPreferences("sources", Context.MODE_PRIVATE)
|
||||
|
||||
suspend fun get(url: String, block: (Request.Builder.() -> Unit)? = null): Response {
|
||||
val request = Request.Builder()
|
||||
.get()
|
||||
.url(url)
|
||||
if (block != null) {
|
||||
request.block()
|
||||
}
|
||||
return okHttp.newCall(request.build()).await()
|
||||
}
|
||||
|
||||
suspend fun post(
|
||||
url: String,
|
||||
form: Map<String, String>,
|
||||
block: (Request.Builder.() -> Unit)? = null
|
||||
): Response {
|
||||
val body = FormBody.Builder()
|
||||
form.forEach { (k, v) ->
|
||||
body.addEncoded(k, v)
|
||||
}
|
||||
val request = Request.Builder()
|
||||
.post(body.build())
|
||||
.url(url)
|
||||
if (block != null) {
|
||||
request.block()
|
||||
}
|
||||
return okHttp.newCall(request.build()).await()
|
||||
}
|
||||
|
||||
fun getStringOption(name: String, default: String? = null) =
|
||||
preferences.getString(name, default)
|
||||
|
||||
fun getIntOption(name: String, default: Int) = preferences.getInt(name, default)
|
||||
|
||||
fun getBooleanOption(name: String, default: Boolean) = preferences.getBoolean(name, default)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.koitharu.kotatsu.domain
|
||||
|
||||
import org.koin.core.KoinComponent
|
||||
import org.koin.core.get
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
|
||||
object MangaProviderFactory : KoinComponent {
|
||||
|
||||
private val loaderContext get() = get<MangaLoaderContext>()
|
||||
|
||||
fun create(source: MangaSource): MangaRepository {
|
||||
val constructor = source.cls.getConstructor(MangaLoaderContext::class.java)
|
||||
return constructor.newInstance(loaderContext)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.koitharu.kotatsu.domain
|
||||
|
||||
import org.koitharu.kotatsu.core.model.*
|
||||
|
||||
abstract class MangaRepository(protected val loaderContext: MangaLoaderContext) {
|
||||
|
||||
open val sortOrders: Set<SortOrder> get() = emptySet()
|
||||
|
||||
open val isSearchAvailable get() = true
|
||||
|
||||
abstract suspend fun getList(offset: Int, query: String? = null, sortOrder: SortOrder? = null, tags: Set<String>? = null): List<Manga>
|
||||
|
||||
abstract suspend fun getDetails(manga: Manga) : Manga
|
||||
|
||||
abstract suspend fun getPages(chapter: MangaChapter) : List<MangaPage>
|
||||
|
||||
open suspend fun getPageFullUrl(page: MangaPage) : String = page.url
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package org.koitharu.kotatsu.domain.exceptions
|
||||
|
||||
class ParseException(message: String? = null, cause: Throwable? = null) : RuntimeException(message, cause)
|
||||
@@ -0,0 +1,70 @@
|
||||
package org.koitharu.kotatsu.domain.repository
|
||||
|
||||
import org.koitharu.kotatsu.core.model.*
|
||||
import org.koitharu.kotatsu.domain.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.domain.MangaRepository
|
||||
import org.koitharu.kotatsu.domain.exceptions.ParseException
|
||||
import org.koitharu.kotatsu.utils.ext.longHashCode
|
||||
import org.koitharu.kotatsu.utils.ext.parseHtml
|
||||
import org.koitharu.kotatsu.utils.ext.safe
|
||||
|
||||
class ReadmangaRepository(loaderContext: MangaLoaderContext) : MangaRepository(loaderContext) {
|
||||
|
||||
override suspend fun getList(
|
||||
offset: Int,
|
||||
query: String?,
|
||||
sortOrder: SortOrder?,
|
||||
tags: Set<String>?
|
||||
): List<Manga> {
|
||||
val doc = loaderContext.get("https://readmanga.me/list?sortType=updated&offset=$offset")
|
||||
.parseHtml()
|
||||
val root = doc.body().getElementById("mangaBox")
|
||||
?.selectFirst("div.tiles.row") ?: throw ParseException("Cannot find root")
|
||||
return root.select("div.tile").mapNotNull { node ->
|
||||
val imgDiv = node.selectFirst("div.img") ?: return@mapNotNull null
|
||||
val descDiv = node.selectFirst("div.desc") ?: return@mapNotNull null
|
||||
val href = imgDiv.selectFirst("a").attr("href") ?: return@mapNotNull null
|
||||
val title = descDiv.selectFirst("h3")?.selectFirst("a")?.text()
|
||||
?: return@mapNotNull null
|
||||
Manga(
|
||||
id = href.longHashCode(),
|
||||
url = href,
|
||||
localizedTitle = title,
|
||||
title = descDiv.selectFirst("h4")?.text() ?: title,
|
||||
coverUrl = imgDiv.selectFirst("img.lazy")?.attr("data-original").orEmpty(),
|
||||
summary = "",
|
||||
rating = safe {
|
||||
node.selectFirst("div.rating")
|
||||
?.attr("title")
|
||||
?.substringBefore(' ')
|
||||
?.toFloatOrNull()
|
||||
?.div(10f)
|
||||
} ?: -1f,
|
||||
tags = safe {
|
||||
descDiv.selectFirst("div.tile-info")
|
||||
?.select("a.element-link")
|
||||
?.map {
|
||||
MangaTag(
|
||||
title = it.text(),
|
||||
key = it.attr("href").substringAfterLast('/')
|
||||
)
|
||||
}?.toSet()
|
||||
}.orEmpty(),
|
||||
state = when {
|
||||
node.selectFirst("div.tags")
|
||||
?.selectFirst("span.mangaCompleted") != null -> MangaState.FINISHED
|
||||
else -> null
|
||||
},
|
||||
source = MangaSource.READMANGA_RU
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.koitharu.kotatsu.ui.common
|
||||
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import moxy.MvpAppCompatActivity
|
||||
import org.koin.core.KoinComponent
|
||||
import org.koitharu.kotatsu.R
|
||||
|
||||
abstract class BaseActivity : MvpAppCompatActivity(), KoinComponent {
|
||||
|
||||
override fun setContentView(layoutResID: Int) {
|
||||
super.setContentView(layoutResID)
|
||||
setupToolbar()
|
||||
}
|
||||
|
||||
override fun setContentView(view: View?) {
|
||||
super.setContentView(view)
|
||||
setupToolbar()
|
||||
}
|
||||
|
||||
private fun setupToolbar() {
|
||||
(findViewById<View>(R.id.toolbar) as? Toolbar)?.let(this::setSupportActionBar)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = if (item.itemId == android.R.id.home) {
|
||||
onBackPressed()
|
||||
true
|
||||
} else super.onOptionsItemSelected(item)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.koitharu.kotatsu.ui.common
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import moxy.MvpAppCompatFragment
|
||||
import org.koitharu.kotatsu.utils.delegates.ParcelableArgumentDelegate
|
||||
import org.koitharu.kotatsu.utils.delegates.StringArgumentDelegate
|
||||
|
||||
abstract class BaseFragment(@LayoutRes contentLayoutId: Int) :
|
||||
MvpAppCompatFragment(contentLayoutId) {
|
||||
|
||||
fun stringArg(name: String) = StringArgumentDelegate(name)
|
||||
|
||||
fun <T : Parcelable> arg(name: String) = ParcelableArgumentDelegate<T>(name)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.koitharu.kotatsu.ui.common
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancelChildren
|
||||
import moxy.MvpPresenter
|
||||
import moxy.MvpView
|
||||
import org.koin.core.KoinComponent
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
abstract class BasePresenter<V : MvpView> : MvpPresenter<V>(), KoinComponent, CoroutineScope {
|
||||
|
||||
private val job = SupervisorJob()
|
||||
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = Dispatchers.Main + job
|
||||
|
||||
override fun onDestroy() {
|
||||
coroutineContext.cancelChildren()
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.koitharu.kotatsu.ui.common.list
|
||||
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import java.util.*
|
||||
|
||||
class AdapterUpdater<T>(oldList: List<T>, newList: List<T>, getId: (T) -> Long) {
|
||||
|
||||
private val diff = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
|
||||
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
||||
getId(oldList[oldItemPosition]) == getId(newList[newItemPosition])
|
||||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
||||
Objects.equals(oldList[oldItemPosition], newList[newItemPosition])
|
||||
|
||||
override fun getOldListSize() = oldList.size
|
||||
|
||||
override fun getNewListSize() = newList.size
|
||||
})
|
||||
|
||||
operator fun invoke(adapter: RecyclerView.Adapter<*>) {
|
||||
diff.dispatchUpdatesTo(adapter)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package org.koitharu.kotatsu.ui.common.list
|
||||
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.koin.core.KoinComponent
|
||||
import org.koitharu.kotatsu.utils.ext.replaceWith
|
||||
|
||||
abstract class BaseRecyclerAdapter<T>(private val onItemClickListener: ((T) -> Unit)? = null) :
|
||||
RecyclerView.Adapter<BaseViewHolder<T>>(),
|
||||
KoinComponent {
|
||||
|
||||
private val dataSet = ArrayList<T>()
|
||||
|
||||
init {
|
||||
@Suppress("LeakingThis")
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: BaseViewHolder<T>, position: Int) {
|
||||
holder.bind(dataSet[position])
|
||||
}
|
||||
|
||||
fun getItem(position: Int) = dataSet[position]
|
||||
|
||||
override fun getItemId(position: Int) = onGetItemId(dataSet[position])
|
||||
|
||||
protected fun findItemById(id: Long) = dataSet.find { x -> onGetItemId(x) == id }
|
||||
|
||||
protected fun findItemPositionById(id: Long) =
|
||||
dataSet.indexOfFirst { x -> onGetItemId(x) == id }
|
||||
|
||||
fun replaceData(newData: List<T>) {
|
||||
val updater = AdapterUpdater(dataSet, newData, this::onGetItemId)
|
||||
dataSet.replaceWith(newData)
|
||||
updater(this)
|
||||
}
|
||||
|
||||
fun appendData(newData: List<T>) {
|
||||
val pos = dataSet.size
|
||||
dataSet.addAll(newData)
|
||||
notifyItemRangeInserted(pos, newData.size)
|
||||
}
|
||||
|
||||
fun appendItem(newItem: T) {
|
||||
dataSet.add(newItem)
|
||||
notifyItemInserted(dataSet.lastIndex)
|
||||
}
|
||||
|
||||
fun removeItem(item: T) {
|
||||
removeItemAt(dataSet.indexOf(item))
|
||||
}
|
||||
|
||||
fun removeItemAt(position: Int) {
|
||||
if (position in dataSet.indices) {
|
||||
dataSet.removeAt(position)
|
||||
notifyItemRemoved(position)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearData() {
|
||||
dataSet.clear()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
final override fun getItemCount() = dataSet.size
|
||||
|
||||
final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<T> {
|
||||
return onCreateViewHolder(parent).also { holder ->
|
||||
if (onItemClickListener != null) {
|
||||
holder.itemView.setOnClickListener {
|
||||
onItemClickListener.invoke(holder.requireData())
|
||||
}
|
||||
}
|
||||
}.also(this::onViewHolderCreated)
|
||||
}
|
||||
|
||||
protected open fun onViewHolderCreated(holder: BaseViewHolder<T>) = Unit
|
||||
|
||||
protected abstract fun onCreateViewHolder(parent: ViewGroup): BaseViewHolder<T>
|
||||
|
||||
protected abstract fun onGetItemId(item: T): Long
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.koitharu.kotatsu.ui.common.list
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.extensions.LayoutContainer
|
||||
import org.koin.core.KoinComponent
|
||||
import org.koitharu.kotatsu.utils.ext.inflate
|
||||
|
||||
abstract class BaseViewHolder<T> protected constructor(view: View) :
|
||||
RecyclerView.ViewHolder(view), LayoutContainer, KoinComponent {
|
||||
|
||||
constructor(parent: ViewGroup, @LayoutRes resId: Int) : this(parent.inflate(resId))
|
||||
|
||||
override val containerView: View?
|
||||
get() = itemView
|
||||
|
||||
protected var boundData: T? = null
|
||||
private set
|
||||
|
||||
val context get() = itemView.context!!
|
||||
|
||||
fun bind(data: T) {
|
||||
boundData = data
|
||||
onBind(data)
|
||||
}
|
||||
|
||||
fun requireData() = boundData ?: throw IllegalStateException("Calling requireData() before bind()")
|
||||
|
||||
abstract fun onBind(data: T)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.koitharu.kotatsu.ui.common.list
|
||||
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
abstract class BoundsScrollListener(private val offsetTop: Int, private val offsetBottom: Int) :
|
||||
RecyclerView.OnScrollListener() {
|
||||
|
||||
constructor(offset: Int = 0) : this(offset, offset)
|
||||
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
val layoutManager = (recyclerView.layoutManager as? LinearLayoutManager) ?: return
|
||||
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
|
||||
if (firstVisibleItemPosition <= offsetTop) {
|
||||
onScrolledToTop(recyclerView)
|
||||
return
|
||||
}
|
||||
val visibleItemCount = layoutManager.childCount
|
||||
val totalItemCount = layoutManager.itemCount
|
||||
if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - offsetBottom && firstVisibleItemPosition >= 0) {
|
||||
onScrolledToEnd(recyclerView)
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun onScrolledToTop(recyclerView: RecyclerView)
|
||||
|
||||
abstract fun onScrolledToEnd(recyclerView: RecyclerView)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.koitharu.kotatsu.ui.common.list
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class PaginationScrollListener(offset: Int, private val callback: Callback) : BoundsScrollListener(0, offset) {
|
||||
|
||||
private var lastTotalCount = 0
|
||||
|
||||
override fun onScrolledToTop(recyclerView: RecyclerView) = Unit
|
||||
|
||||
override fun onScrolledToEnd(recyclerView: RecyclerView) {
|
||||
val total = recyclerView.adapter?.itemCount ?: 0
|
||||
if (total > lastTotalCount) {
|
||||
callback.onRequestMoreItems(total)
|
||||
lastTotalCount = total
|
||||
} else if (total < lastTotalCount) {
|
||||
lastTotalCount = total
|
||||
}
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
|
||||
fun onRequestMoreItems(offset: Int)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.koitharu.kotatsu.ui.common.list
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.view.View
|
||||
import androidx.annotation.Px
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class SpacingItemDecoration(@Px private val spacing: Int) : RecyclerView.ItemDecoration() {
|
||||
|
||||
private val halfSpacing = spacing / 2
|
||||
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
val spans = (parent.layoutManager as? GridLayoutManager)?.spanCount ?: 1
|
||||
val position = parent.getChildAdapterPosition(view)
|
||||
outRect.set(
|
||||
if (position % spans == 0) spacing else halfSpacing,
|
||||
if (position < spans) spacing else halfSpacing,
|
||||
if ((position + 1) % spans == 0) spacing else halfSpacing,
|
||||
spacing //TODO check bottom
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.koitharu.kotatsu.ui.common.widgets
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.appcompat.widget.AppCompatImageView
|
||||
|
||||
|
||||
class CoverImageView @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
|
||||
) : AppCompatImageView(context, attrs, defStyleAttr) {
|
||||
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val originalWidth = MeasureSpec.getSize(widthMeasureSpec)
|
||||
val calculatedHeight: Int = (originalWidth * ASPECT_RATIO_HEIGHT / ASPECT_RATIO_WIDTH).toInt()
|
||||
|
||||
super.onMeasure(
|
||||
MeasureSpec.makeMeasureSpec(originalWidth, MeasureSpec.EXACTLY),
|
||||
MeasureSpec.makeMeasureSpec(calculatedHeight, MeasureSpec.EXACTLY)
|
||||
)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
const val ASPECT_RATIO_HEIGHT = 18f
|
||||
const val ASPECT_RATIO_WIDTH = 13f
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.koitharu.kotatsu.ui.common.widgets
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.FrameLayout
|
||||
|
||||
class RectFrameLayout @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
|
||||
) : FrameLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
public override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
super.onMeasure(widthMeasureSpec, widthMeasureSpec)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.koitharu.kotatsu.ui.main
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||
import kotlinx.android.synthetic.main.activity_main.*
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.ui.common.BaseActivity
|
||||
import org.koitharu.kotatsu.ui.main.list.MangaListFragment
|
||||
|
||||
class MainActivity : BaseActivity() {
|
||||
|
||||
private lateinit var drawerToggle: ActionBarDrawerToggle
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
drawerToggle = ActionBarDrawerToggle(this, drawer, toolbar, R.string.open_menu, R.string.close_menu)
|
||||
drawer.addDrawerListener(drawerToggle)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setHomeButtonEnabled(true)
|
||||
|
||||
if (!supportFragmentManager.isStateSaved) {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.container, MangaListFragment.newInstance(MangaSource.READMANGA_RU))
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPostCreate(savedInstanceState: Bundle?) {
|
||||
super.onPostCreate(savedInstanceState)
|
||||
drawerToggle.syncState()
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
drawerToggle.onConfigurationChanged(newConfig)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return drawerToggle.onOptionsItemSelected(item) || super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.koitharu.kotatsu.ui.main.list
|
||||
|
||||
import android.view.ViewGroup
|
||||
import coil.api.load
|
||||
import coil.request.RequestDisposable
|
||||
import kotlinx.android.synthetic.main.item_manga_grid.*
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
|
||||
|
||||
class MangaGridHolder(parent: ViewGroup) : BaseViewHolder<Manga>(parent, R.layout.item_manga_grid) {
|
||||
|
||||
private var coverRequest: RequestDisposable? = null
|
||||
|
||||
override fun onBind(data: Manga) {
|
||||
coverRequest?.dispose()
|
||||
textView_title.text = data.title
|
||||
coverRequest = imageView_cover.load(data.coverUrl) {
|
||||
crossfade(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.koitharu.kotatsu.ui.main.list
|
||||
|
||||
import android.view.ViewGroup
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.ui.common.list.BaseRecyclerAdapter
|
||||
|
||||
class MangaListAdapter(onItemClickListener: ((Manga) -> Unit)?) :
|
||||
BaseRecyclerAdapter<Manga>(onItemClickListener) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup) = MangaGridHolder(parent)
|
||||
|
||||
override fun onGetItemId(item: Manga) = item.id
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package org.koitharu.kotatsu.ui.main.list
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.fragment_list.*
|
||||
import moxy.ktx.moxyPresenter
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.ui.common.BaseFragment
|
||||
import org.koitharu.kotatsu.ui.common.list.PaginationScrollListener
|
||||
import org.koitharu.kotatsu.ui.common.list.SpacingItemDecoration
|
||||
import org.koitharu.kotatsu.utils.ext.hasItems
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
|
||||
class MangaListFragment : BaseFragment(R.layout.fragment_list), MangaListView,
|
||||
PaginationScrollListener.Callback {
|
||||
|
||||
private val presenter by moxyPresenter(factory = ::MangaListPresenter)
|
||||
|
||||
private val source by arg<MangaSource>(ARG_SOURCE)
|
||||
|
||||
private lateinit var adapter: MangaListAdapter
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
adapter = MangaListAdapter {
|
||||
|
||||
}
|
||||
recyclerView.addItemDecoration(SpacingItemDecoration(resources.getDimensionPixelOffset(R.dimen.grid_spacing)))
|
||||
recyclerView.adapter = adapter
|
||||
recyclerView.addOnScrollListener(PaginationScrollListener(4, this))
|
||||
swipeRefreshLayout.setOnRefreshListener {
|
||||
presenter.loadList(source, 0)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
presenter.loadList(source, 0)
|
||||
}
|
||||
|
||||
override fun onRequestMoreItems(offset: Int) {
|
||||
presenter.loadList(source, offset)
|
||||
}
|
||||
|
||||
override fun onListChanged(list: List<Manga>) {
|
||||
adapter.replaceData(list)
|
||||
}
|
||||
|
||||
override fun onListAppended(list: List<Manga>) {
|
||||
adapter.appendData(list)
|
||||
}
|
||||
|
||||
override fun onLoadingChanged(isLoading: Boolean) {
|
||||
val hasItems = recyclerView.hasItems
|
||||
progressBar.isVisible = isLoading && !hasItems
|
||||
swipeRefreshLayout.isRefreshing = isLoading && hasItems
|
||||
swipeRefreshLayout.isEnabled = !progressBar.isVisible
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val ARG_SOURCE = "provider"
|
||||
|
||||
fun newInstance(provider: MangaSource) = MangaListFragment().withArgs(1) {
|
||||
putParcelable(ARG_SOURCE, provider)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.koitharu.kotatsu.ui.main.list
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import moxy.InjectViewState
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.domain.MangaProviderFactory
|
||||
import org.koitharu.kotatsu.ui.common.BasePresenter
|
||||
|
||||
@InjectViewState
|
||||
class MangaListPresenter : BasePresenter<MangaListView>() {
|
||||
|
||||
fun loadList(source: MangaSource, offset: Int) {
|
||||
launch {
|
||||
viewState.onLoadingChanged(true)
|
||||
try {
|
||||
val list = withContext(Dispatchers.IO) {
|
||||
MangaProviderFactory.create(source)
|
||||
.getList(offset)
|
||||
}
|
||||
if (offset == 0) {
|
||||
viewState.onListChanged(list)
|
||||
} else {
|
||||
viewState.onListAppended(list)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
} finally {
|
||||
viewState.onLoadingChanged(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.koitharu.kotatsu.ui.main.list
|
||||
|
||||
import moxy.MvpView
|
||||
import moxy.viewstate.strategy.AddToEndSingleStrategy
|
||||
import moxy.viewstate.strategy.AddToEndSingleTagStrategy
|
||||
import moxy.viewstate.strategy.AddToEndStrategy
|
||||
import moxy.viewstate.strategy.StateStrategyType
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
|
||||
interface MangaListView : MvpView {
|
||||
|
||||
@StateStrategyType(AddToEndSingleTagStrategy::class, tag = "content")
|
||||
fun onListChanged(list: List<Manga>)
|
||||
|
||||
@StateStrategyType(AddToEndStrategy::class, tag = "content")
|
||||
fun onListAppended(list: List<Manga>)
|
||||
|
||||
@StateStrategyType(AddToEndSingleStrategy::class)
|
||||
fun onLoadingChanged(isLoading: Boolean)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.koitharu.kotatsu.utils.delegates
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.fragment.app.Fragment
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
class ParcelableArgumentDelegate<T : Parcelable>(private val name: String) : ReadOnlyProperty<Fragment, T> {
|
||||
|
||||
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
|
||||
return thisRef.requireArguments().getParcelable(name)!!
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.koitharu.kotatsu.utils.delegates
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
class StringArgumentDelegate(private val name: String) : ReadOnlyProperty<Fragment, String?> {
|
||||
|
||||
override fun getValue(thisRef: Fragment, property: KProperty<*>): String? {
|
||||
return thisRef.arguments?.getString(name)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package org.koitharu.kotatsu.utils.ext
|
||||
|
||||
fun <T> MutableCollection<T>.replaceWith(subject: Iterable<T>) {
|
||||
clear()
|
||||
addAll(subject)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.koitharu.kotatsu.utils.ext
|
||||
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
|
||||
inline fun <T, R> T.safe(action: T.() -> R?) = try {
|
||||
this.action()
|
||||
} catch (e: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
null
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.koitharu.kotatsu.utils.ext
|
||||
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.Response
|
||||
import java.io.IOException
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
suspend fun Call.await() = suspendCancellableCoroutine<Response> { cont ->
|
||||
this.enqueue(object : Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
if (!cont.isCancelled) {
|
||||
cont.resumeWithException(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
cont.resume(response)
|
||||
}
|
||||
})
|
||||
cont.invokeOnCancellation {
|
||||
safe {
|
||||
this.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
13
app/src/main/java/org/koitharu/kotatsu/utils/ext/DateExt.kt
Normal file
@@ -0,0 +1,13 @@
|
||||
package org.koitharu.kotatsu.utils.ext
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import org.intellij.lang.annotations.PrintFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
fun Date.format(pattern: String): String = SimpleDateFormat(pattern).format(this)
|
||||
|
||||
fun Date.calendar(): Calendar = Calendar.getInstance().also {
|
||||
it.time = this
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.koitharu.kotatsu.utils.ext
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
fun <T : Fragment> T.withArgs(size: Int, block: Bundle.() -> Unit): T {
|
||||
val b = Bundle(size)
|
||||
b.block()
|
||||
this.arguments = b
|
||||
return this
|
||||
}
|
||||
18
app/src/main/java/org/koitharu/kotatsu/utils/ext/JsoupExt.kt
Normal file
@@ -0,0 +1,18 @@
|
||||
package org.koitharu.kotatsu.utils.ext
|
||||
|
||||
import okhttp3.Response
|
||||
import okhttp3.internal.closeQuietly
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
|
||||
fun Response.parseHtml(): Document {
|
||||
val stream = body?.byteStream() ?: throw NullPointerException("Response body is null")
|
||||
val charset = body!!.contentType()?.charset()?.name()
|
||||
val doc = Jsoup.parse(
|
||||
stream,
|
||||
charset,
|
||||
this.request.url.toString()
|
||||
)
|
||||
closeQuietly()
|
||||
return doc
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.koitharu.kotatsu.utils.ext
|
||||
|
||||
import java.text.DecimalFormat
|
||||
import java.text.NumberFormat
|
||||
import java.util.*
|
||||
|
||||
fun Number?.asBoolean() = (this?.toInt() ?: 0) > 0
|
||||
|
||||
fun Number.format(decimals: Int = 0, decPoint: Char = '.', thousandsSep: Char? = ' '): String {
|
||||
val formatter = NumberFormat.getInstance(Locale.US) as DecimalFormat
|
||||
val symbols = formatter.decimalFormatSymbols
|
||||
if (thousandsSep != null) {
|
||||
symbols.groupingSeparator = thousandsSep
|
||||
formatter.isGroupingUsed = true
|
||||
} else {
|
||||
formatter.isGroupingUsed = false
|
||||
}
|
||||
symbols.decimalSeparator = decPoint
|
||||
formatter.decimalFormatSymbols = symbols
|
||||
formatter.minimumFractionDigits = decimals
|
||||
formatter.maximumFractionDigits = decimals
|
||||
return when (this) {
|
||||
is Float,
|
||||
is Double -> formatter.format(this.toDouble())
|
||||
else -> formatter.format(this.toLong())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.koitharu.kotatsu.utils.ext
|
||||
|
||||
import android.content.res.Resources
|
||||
import androidx.annotation.Px
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Px
|
||||
fun Resources.resolveDp(dp: Int) = (dp * displayMetrics.density).roundToInt()
|
||||
|
||||
@Px
|
||||
fun Resources.resolveDp(dp: Float) = dp * displayMetrics.density
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.koitharu.kotatsu.utils.ext
|
||||
|
||||
fun String.longHashCode(): Long {
|
||||
var h = 1125899906842597L
|
||||
val len: Int = this.length
|
||||
for (i in 0 until len) {
|
||||
h = 31 * h + this[i].toLong()
|
||||
}
|
||||
return h
|
||||
}
|
||||
22
app/src/main/java/org/koitharu/kotatsu/utils/ext/ThemeExt.kt
Normal file
@@ -0,0 +1,22 @@
|
||||
package org.koitharu.kotatsu.utils.ext
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.Px
|
||||
import androidx.core.content.res.use
|
||||
|
||||
@Px
|
||||
fun Context.getThemeDimen(@AttrRes resId: Int) = obtainStyledAttributes(intArrayOf(resId)).use {
|
||||
it.getDimension(0, 0f)
|
||||
}
|
||||
|
||||
fun Context.getThemeDrawable(@AttrRes resId: Int) = obtainStyledAttributes(intArrayOf(resId)).use {
|
||||
it.getDrawable(0)
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun Context.getThemeColor(@AttrRes resId: Int, @ColorInt default: Int = Color.TRANSPARENT) = obtainStyledAttributes(intArrayOf(resId)).use {
|
||||
it.getColor(0, default)
|
||||
}
|
||||
55
app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt
Normal file
@@ -0,0 +1,55 @@
|
||||
package org.koitharu.kotatsu.utils.ext
|
||||
|
||||
import android.app.Activity
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
fun View.hideKeyboard() {
|
||||
val imm = context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.hideSoftInputFromWindow(this.windowToken, 0)
|
||||
}
|
||||
|
||||
fun View.showKeyboard() {
|
||||
val imm = context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.showSoftInput(this, 0)
|
||||
}
|
||||
|
||||
val EditText.plainText
|
||||
get() = text?.toString().orEmpty()
|
||||
|
||||
inline fun <reified T : View> ViewGroup.inflate(@LayoutRes resId: Int) =
|
||||
LayoutInflater.from(context).inflate(resId, this, false) as T
|
||||
|
||||
val TextView.hasText get() = !text.isNullOrEmpty()
|
||||
|
||||
fun RecyclerView.lookupSpanSize(callback: (Int) -> Int) {
|
||||
(layoutManager as? GridLayoutManager)?.spanSizeLookup =
|
||||
object : GridLayoutManager.SpanSizeLookup() {
|
||||
override fun getSpanSize(position: Int) = callback(position)
|
||||
}
|
||||
}
|
||||
|
||||
val RecyclerView.hasItems: Boolean
|
||||
get() = (adapter?.itemCount ?: 0) > 0
|
||||
|
||||
var TextView.drawableStart: Drawable?
|
||||
get() = compoundDrawablesRelative[0]
|
||||
set(value) {
|
||||
val old = compoundDrawablesRelative
|
||||
setCompoundDrawablesRelativeWithIntrinsicBounds(value, old[1], old[2], old[3])
|
||||
}
|
||||
|
||||
var TextView.drawableEnd: Drawable?
|
||||
get() = compoundDrawablesRelative[2]
|
||||
set(value) {
|
||||
val old = compoundDrawablesRelative
|
||||
setCompoundDrawablesRelativeWithIntrinsicBounds(old[0], old[1], value, old[3])
|
||||
}
|
||||
31
app/src/main/res/drawable-v24/ic_launcher_foreground.xml
Normal file
@@ -0,0 +1,31 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_history.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z" />
|
||||
</vector>
|
||||
171
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
@@ -0,0 +1,171 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
14
app/src/main/res/drawable/ic_star_half.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group>
|
||||
<clip-path android:pathData="M0,0h24v24H0V0z M 0,0" />
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4V6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z" />
|
||||
</group>
|
||||
</vector>
|
||||
|
||||
10
app/src/main/res/drawable/ic_storage.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M18,4v16L6,20L6,8.83L10.83,4L18,4m0,-2h-8L4,8v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,4c0,-1.1 -0.9,-2 -2,-2zM9,7h2v4L9,11zM12,7h2v4h-2zM15,7h2v4h-2z" />
|
||||
</vector>
|
||||
45
app/src/main/res/layout/activity_main.xml
Normal file
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.drawerlayout.widget.DrawerLayout
|
||||
android:id="@+id/drawer"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ui.main.MainActivity"
|
||||
tools:openDrawer="start">
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_scrollFlags="scroll|enterAlways"
|
||||
app:popupTheme="@style/ThemeOverlay.MaterialComponents.Light" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
<com.google.android.material.navigation.NavigationView
|
||||
android:layout_width="260dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start"
|
||||
app:menu="@menu/nav_drawer" />
|
||||
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
||||
49
app/src/main/res/layout/fragment_list.xml
Normal file
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<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:orientation="vertical"
|
||||
android:scrollbars="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||
app:spanCount="3" />
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_holder"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_holder"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
tools:text="@tools:sample/lorem" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="true" />
|
||||
|
||||
</FrameLayout>
|
||||
33
app/src/main/res/layout/item_manga_grid.xml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<org.koitharu.kotatsu.ui.common.widgets.CoverImageView
|
||||
android:id="@+id/imageView_cover"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:scaleType="centerCrop" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_vertical|start"
|
||||
android:lines="2"
|
||||
android:padding="6dp"
|
||||
android:text="?android:textColorPrimary"
|
||||
tools:text="@tools:sample/lorem" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
36
app/src/main/res/menu/nav_drawer.xml
Normal file
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<group>
|
||||
<item
|
||||
android:id="@+id/nav_local_storage"
|
||||
android:icon="@drawable/ic_storage"
|
||||
android:title="@string/local_storage" />
|
||||
<item
|
||||
android:id="@+id/nav_favourites"
|
||||
android:icon="@drawable/ic_star_half"
|
||||
android:title="@string/favourites" />
|
||||
<item
|
||||
android:id="@+id/nav_history"
|
||||
android:icon="@drawable/ic_history"
|
||||
android:title="@string/history" />
|
||||
</group>
|
||||
|
||||
<group android:id="@+id/group_menu">
|
||||
<item
|
||||
android:id="@+id/nav_test"
|
||||
android:title="Test" />
|
||||
</group>
|
||||
<!--
|
||||
<item android:title="Title 1">
|
||||
<menu>
|
||||
<item
|
||||
android:id="@+id/nav_item_six"
|
||||
android:icon="@drawable/ic_drafts_black_24dp"
|
||||
android:title="Item 6" />
|
||||
<item
|
||||
android:id="@+id/nav_item_seven"
|
||||
android:icon="@drawable/ic_drafts_black_24dp"
|
||||
android:title="Item 7" />
|
||||
</menu>
|
||||
</item>-->
|
||||
</menu>
|
||||
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
6
app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="colorPrimary">#6200EE</color>
|
||||
<color name="colorPrimaryDark">#3700B3</color>
|
||||
<color name="colorAccent">#03DAC5</color>
|
||||
</resources>
|
||||
4
app/src/main/res/values/dimens.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<dimen name="grid_spacing">5dp</dimen>
|
||||
</resources>
|
||||
5
app/src/main/res/values/ids.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<item name="toolbar" type="id" />
|
||||
<item name="container" type="id" />
|
||||
</resources>
|
||||
8
app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<resources>
|
||||
<string name="app_name" translatable="false">Kotatsu</string>
|
||||
<string name="close_menu">Close menu</string>
|
||||
<string name="open_menu">Open menu</string>
|
||||
<string name="local_storage">Local storage</string>
|
||||
<string name="favourites">Favourites</string>
|
||||
<string name="history">History</string>
|
||||
</resources>
|
||||
10
app/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<resources>
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
5
app/src/main/res/xml/backup_descriptor.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<full-backup-content>
|
||||
<!-- TODO: Exclude specific shared preferences that contain GCM registration Id -->
|
||||
<!-- https://developer.android.com/guide/topics/data/autobackup -->
|
||||
</full-backup-content>
|
||||