Compare commits

...

16 Commits

Author SHA1 Message Date
5fe40ac17e Fix old api compatibility 2020-04-25 21:29:46 +03:00
0dc74f9188 Update readme 2020-04-25 18:06:11 +03:00
f95cf9b231 Reset new chapters on reading 2020-04-25 17:22:20 +03:00
0d0982b244 Check if in-app update allowed 2020-04-19 08:43:45 +03:00
ef4dd82e92 Fix TextInputDialog keyboard behavior 2020-04-19 08:06:24 +03:00
bc825681a8 Small ui fixes 2020-04-18 20:24:19 +03:00
da6204f44f Remove unused resources 2020-04-18 19:56:12 +03:00
10c68bdd72 Update readme 2020-04-18 17:33:27 +03:00
b1e90dde8f Optimize page loading 2020-04-18 12:20:18 +03:00
e0d45961f8 Add some animations 2020-04-18 11:19:20 +03:00
b732a220f6 Merge remote-tracking branch 'origin/feature/appwidget' into devel 2020-04-12 12:44:54 +03:00
582adae11f Manage favourites categories 2020-04-12 12:40:40 +03:00
c13c43c616 Recent manga app widget 2020-04-10 20:13:17 +03:00
ab1eacea3f Shelf app widget 2020-04-10 19:56:51 +03:00
ac4b97928a Merge branch 'devel' of https://github.com/nv95/Kotatsu into devel 2020-04-10 17:14:40 +03:00
aa8281678b Update settings 2020-04-10 17:14:33 +03:00
129 changed files with 1310 additions and 365 deletions

View File

@@ -23,7 +23,6 @@
</option>
</AndroidXmlCodeStyleSettings>
<JetCodeStyleSettings>
<option name="ALLOW_TRAILING_COMMA" value="true" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="CMake">

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="TrailingComma" enabled="true" level="INFORMATION" enabled_by_default="true" />
</profile>
</component>

View File

@@ -2,13 +2,11 @@
Kotatsu is a free and open source manga reader for Android.
![Android minSdk](https://img.shields.io/badge/android-5.0+-brightgreen) ![GitHub top language](https://img.shields.io/github/languages/top/nv95/Kotatsu) [![Build Status](https://travis-ci.org/nv95/Kotatsu.svg?branch=master)](https://travis-ci.org/nv95/Kotatsu) ![License](https://img.shields.io/github/license/nv95/Kotatsu) [![4pda](https://img.shields.io/badge/discuss-4pda-2982CC)](http://4pda.ru/forum/index.php?showtopic=697669)
![Android 5.0](https://img.shields.io/badge/android-5.0+-brightgreen) ![Kotlin](https://img.shields.io/github/languages/top/nv95/Kotatsu) [![Build Status](https://travis-ci.org/nv95/Kotatsu.svg?branch=master)](https://travis-ci.org/nv95/Kotatsu) ![License](https://img.shields.io/github/license/nv95/Kotatsu) [![4pda](https://img.shields.io/badge/discuss-4pda-2982CC)](http://4pda.ru/forum/index.php?showtopic=697669)
### Download
Latest unstable build: [get here](https://github.com/nv95/Kotatsu/releases/latest)
Stable release: _Coming soon_
Latest release: [get here](https://github.com/nv95/Kotatsu/releases/latest)
### Main Features
@@ -20,10 +18,7 @@ Stable release: _Coming soon_
* Tablet-optimized modern UI
* Reading third-party comics from CBZ
* Standard and Webtoon-optimized reader
### Coming Features
* Checking for new chapters
* Notifications about new chapters
### Screenshots

View File

@@ -12,13 +12,14 @@ android {
defaultConfig {
applicationId 'org.koitharu.kotatsu'
minSdkVersion 21
minSdkVersion 16
maxSdkVersion 20
targetSdkVersion 29
versionCode gitCommits
versionName '0.2-b1'
versionName '0.3'
buildConfigField 'String', 'GIT_BRANCH', "\"${gitBranch}\""
vectorDrawables.useSupportLibrary = true
kapt {
arguments {
arg('room.schemaLocation', "$projectDir/schemas".toString())
@@ -61,16 +62,16 @@ dependencies {
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.3.0-beta01'
implementation 'androidx.core:core-ktx:1.3.0-rc01'
implementation 'androidx.fragment:fragment-ktx:1.2.4'
implementation 'androidx.appcompat:appcompat:1.2.0-beta01'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-beta01'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-rc01'
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha02'
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
implementation 'androidx.preference:preference:1.1.0'
implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'androidx.work:work-runtime-ktx:2.3.4'
implementation 'com.google.android.material:material:1.2.0-alpha05'
implementation 'com.google.android.material:material:1.2.0-alpha06'
implementation 'androidx.room:room-runtime:2.2.5'
implementation 'androidx.room:room-ktx:2.2.5'
@@ -82,11 +83,11 @@ dependencies {
implementation 'com.github.moxy-community:moxy-ktx:2.1.2'
kapt 'com.github.moxy-community:moxy-compiler:2.1.2'
implementation 'com.squareup.okhttp3:okhttp:4.5.0'
implementation 'com.squareup.okhttp3:okhttp:3.12.10'
implementation 'com.squareup.okio:okio:2.5.0'
implementation 'org.jsoup:jsoup:1.13.1'
implementation 'org.koin:koin-android:2.1.4'
implementation 'org.koin:koin-android:2.1.5'
implementation 'io.coil-kt:coil:0.9.5'
implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0'
implementation 'com.tomclaw.cache:cache:1.0'

View File

@@ -46,18 +46,33 @@
android:label="@string/settings" />
<activity
android:name=".ui.reader.SimpleSettingsActivity"
android:label="@string/settings" />
android:label="@string/settings">
<intent-filter>
<action android:name="android.intent.action.MANAGE_NETWORK_USAGE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity android:name=".ui.browser.BrowserActivity" />
<activity
android:name=".ui.utils.CrashActivity"
android:label="@string/error_occurred"
android:theme="@android:style/Theme.DeviceDefault.Dialog"
android:windowSoftInputMode="stateAlwaysHidden" />
<activity
android:name=".ui.main.list.favourites.categories.CategoriesActivity"
android:windowSoftInputMode="stateAlwaysHidden"
android:label="@string/favourites_categories" />
<service
android:name=".ui.download.DownloadService"
android:foregroundServiceType="dataSync" />
<service android:name=".ui.settings.AppUpdateService" />
<service
android:name=".ui.widget.shelf.ShelfWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS" />
<service
android:name=".ui.widget.recent.RecentWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS" />
<provider
android:name=".ui.search.MangaSuggestionsProvider"
@@ -72,6 +87,23 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
<receiver android:name=".ui.widget.shelf.ShelfWidgetProvider"
android:label="@string/manga_shelf">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/widget_shelf" />
</receiver>
<receiver android:name=".ui.widget.recent.RecentWidgetProvider"
android:label="@string/recent_manga">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/widget_recent" />
</receiver>
</application>

View File

@@ -25,7 +25,10 @@ import org.koitharu.kotatsu.core.local.cookies.persistence.SharedPrefsCookiePers
import org.koitharu.kotatsu.core.parser.UserAgentInterceptor
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.domain.MangaLoaderContext
import org.koitharu.kotatsu.domain.favourites.FavouritesRepository
import org.koitharu.kotatsu.domain.history.HistoryRepository
import org.koitharu.kotatsu.ui.utils.AppCrashHandler
import org.koitharu.kotatsu.ui.widget.WidgetUpdater
import org.koitharu.kotatsu.utils.CacheUtils
import java.util.concurrent.TimeUnit
@@ -41,6 +44,7 @@ class KotatsuApp : Application() {
override fun onCreate() {
super.onCreate()
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
initKoin()
initCoil()
Thread.setDefaultUncaughtExceptionHandler(AppCrashHandler(applicationContext))
@@ -48,6 +52,9 @@ class KotatsuApp : Application() {
initErrorHandler()
}
AppCompatDelegate.setDefaultNightMode(AppSettings(this).theme)
val widgetUpdater = WidgetUpdater(applicationContext)
FavouritesRepository.subscribe(widgetUpdater)
HistoryRepository.subscribe(widgetUpdater)
}
private fun initKoin() {

View File

@@ -17,4 +17,7 @@ abstract class FavouriteCategoriesDao {
@Query("DELETE FROM favourite_categories WHERE category_id = :id")
abstract suspend fun delete(id: Long)
@Query("UPDATE favourite_categories SET title = :title WHERE category_id = :id")
abstract suspend fun update(id: Long, title: String)
}

View File

@@ -15,10 +15,10 @@
*/
package org.koitharu.kotatsu.core.local.cookies
import org.koitharu.kotatsu.core.local.cookies.persistence.CookiePersistor
import okhttp3.Cookie
import okhttp3.HttpUrl
import org.koitharu.kotatsu.core.local.cookies.cache.CookieCache
import org.koitharu.kotatsu.core.local.cookies.persistence.CookiePersistor
import java.util.*
class PersistentCookieJar(
@@ -72,7 +72,7 @@ class PersistentCookieJar(
fun filterPersistentCookies(cookies: List<Cookie>): List<Cookie> {
val persistentCookies: MutableList<Cookie> = ArrayList()
for (cookie in cookies) {
if (cookie.persistent) {
if (cookie.persistent()) {
persistentCookies.add(cookie)
}
}
@@ -81,7 +81,7 @@ class PersistentCookieJar(
@JvmStatic
fun isCookieExpired(cookie: Cookie): Boolean {
return cookie.expiresAt < System.currentTimeMillis()
return cookie.expiresAt() < System.currentTimeMillis()
}
}
}

View File

@@ -30,18 +30,18 @@ internal class IdentifiableCookie(val cookie: Cookie) {
override fun equals(other: Any?): Boolean {
if (other !is IdentifiableCookie) return false
return other.cookie.name == cookie.name && other.cookie.domain == cookie.domain
&& other.cookie.path == cookie.path && other.cookie.secure == cookie.secure
&& other.cookie.hostOnly == cookie.hostOnly
return other.cookie.name() == cookie.name() && other.cookie.domain() == cookie.domain()
&& other.cookie.path() == cookie.path() && other.cookie.secure() == cookie.secure()
&& other.cookie.hostOnly() == cookie.hostOnly()
}
override fun hashCode(): Int {
var hash = 17
hash = 31 * hash + cookie.name.hashCode()
hash = 31 * hash + cookie.domain.hashCode()
hash = 31 * hash + cookie.path.hashCode()
hash = 31 * hash + if (cookie.secure) 0 else 1
hash = 31 * hash + if (cookie.hostOnly) 0 else 1
hash = 31 * hash + cookie.name().hashCode()
hash = 31 * hash + cookie.domain().hashCode()
hash = 31 * hash + cookie.path().hashCode()
hash = 31 * hash + if (cookie.secure()) 0 else 1
hash = 31 * hash + if (cookie.hostOnly()) 0 else 1
return hash
}

View File

@@ -73,14 +73,14 @@ class SerializableCookie : Serializable {
@Throws(IOException::class)
private fun writeObject(out: ObjectOutputStream) {
out.writeObject(cookie!!.name)
out.writeObject(cookie!!.value)
out.writeLong(if (cookie!!.persistent) cookie!!.expiresAt else NON_VALID_EXPIRES_AT)
out.writeObject(cookie!!.domain)
out.writeObject(cookie!!.path)
out.writeBoolean(cookie!!.secure)
out.writeBoolean(cookie!!.httpOnly)
out.writeBoolean(cookie!!.hostOnly)
out.writeObject(cookie!!.name())
out.writeObject(cookie!!.value())
out.writeLong(if (cookie!!.persistent()) cookie!!.expiresAt() else NON_VALID_EXPIRES_AT)
out.writeObject(cookie!!.domain())
out.writeObject(cookie!!.path())
out.writeBoolean(cookie!!.secure())
out.writeBoolean(cookie!!.httpOnly())
out.writeBoolean(cookie!!.hostOnly())
}
@Throws(IOException::class, ClassNotFoundException::class)

View File

@@ -64,7 +64,7 @@ class SharedPrefsCookiePersistor(private val sharedPreferences: SharedPreference
private companion object {
fun createCookieKey(cookie: Cookie): String {
return (if (cookie.secure) "https" else "http") + "://" + cookie.domain + cookie.path + "|" + cookie.name
return (if (cookie.secure()) "https" else "http") + "://" + cookie.domain() + cookie.path() + "|" + cookie.name()
}
}

View File

@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.core.parser
import android.content.Context
import android.net.Uri
import android.os.Build
import androidx.core.net.toFile
import androidx.core.net.toUri
import org.koin.core.KoinComponent
@@ -14,6 +15,7 @@ import org.koitharu.kotatsu.utils.AlphanumComparator
import org.koitharu.kotatsu.utils.ext.longHashCode
import org.koitharu.kotatsu.utils.ext.readText
import org.koitharu.kotatsu.utils.ext.safe
import org.koitharu.kotatsu.utils.ext.sub
import java.io.File
import java.util.*
import java.util.zip.ZipEntry
@@ -29,8 +31,11 @@ class LocalMangaRepository : MangaRepository, KoinComponent {
sortOrder: SortOrder?,
tag: MangaTag?
): List<Manga> {
val files = context.getExternalFilesDirs("manga")
.flatMap { x -> x?.listFiles(CbzFilter())?.toList().orEmpty() }
val files = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
context.getExternalFilesDirs("manga") + context.filesDir.sub("manga")
} else {
arrayOf(context.getExternalFilesDir("manga"), context.filesDir.sub("manga"))
}.flatMap { x -> x?.listFiles(CbzFilter())?.toList().orEmpty() }
return files.mapNotNull { x -> safe { getFromFile(x) } }
}

View File

@@ -73,6 +73,11 @@ class AppSettings private constructor(resources: Resources, private val prefs: S
true
)
val readerAnimation by BoolPreferenceDelegate(
resources.getString(R.string.key_reader_animation),
false
)
private var sourcesOrderStr by NullableStringPreferenceDelegate(resources.getString(R.string.key_sources_order))
var sourcesOrder: List<Int>

View File

@@ -1,7 +1,7 @@
package org.koitharu.kotatsu.domain
import android.graphics.BitmapFactory
import android.util.Size
import android.graphics.Point
import okhttp3.OkHttpClient
import okhttp3.Request
import org.koin.core.KoinComponent
@@ -29,10 +29,10 @@ object MangaUtils : KoinComponent {
.get()
.build()
val size = client.newCall(request).await().use {
getBitmapSize(it.body?.byteStream())
getBitmapSize(it.body()?.byteStream())
}
return when {
size.width * 2 < size.height -> ReaderMode.WEBTOON
size.x * 2 < size.y -> ReaderMode.WEBTOON
else -> ReaderMode.STANDARD
}
} catch (e: Exception) {
@@ -44,7 +44,7 @@ object MangaUtils : KoinComponent {
}
@JvmStatic
private fun getBitmapSize(input: InputStream?): Size {
private fun getBitmapSize(input: InputStream?): Point {
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
@@ -52,6 +52,6 @@ object MangaUtils : KoinComponent {
val imageHeight: Int = options.outHeight
val imageWidth: Int = options.outWidth
check(imageHeight > 0 && imageWidth > 0)
return Size(imageWidth, imageHeight)
return Point(imageWidth, imageHeight)
}
}

View File

@@ -42,6 +42,9 @@ class FavouritesRepository : KoinComponent {
val id = db.favouriteCategoriesDao.insert(entity)
return entity.toFavouriteCategory(id)
}
suspend fun renameCategory(id: Long, title: String) {
db.favouriteCategoriesDao.update(id, title)
}
suspend fun removeCategory(id: Long) {
db.favouriteCategoriesDao.delete(id)

View File

@@ -17,6 +17,7 @@ import java.util.*
class HistoryRepository : KoinComponent {
private val db: MangaDatabase by inject()
private val trackingRepository by lazy(::TrackingRepository)
suspend fun getList(offset: Int, limit: Int = 20): List<Manga> {
val entities = db.historyDao.findAll(offset, limit)
@@ -28,19 +29,17 @@ class HistoryRepository : KoinComponent {
db.withTransaction {
db.tagsDao.upsert(tags)
db.mangaDao.upsert(MangaEntity.from(manga), tags)
if (db.historyDao.upsert(
HistoryEntity(
mangaId = manga.id,
createdAt = System.currentTimeMillis(),
updatedAt = System.currentTimeMillis(),
chapterId = chapterId,
page = page,
scroll = scroll
)
db.historyDao.upsert(
HistoryEntity(
mangaId = manga.id,
createdAt = System.currentTimeMillis(),
updatedAt = System.currentTimeMillis(),
chapterId = chapterId,
page = page,
scroll = scroll
)
) {
TrackingRepository().insertOrNothing(manga)
}
)
trackingRepository.upsert(manga)
}
notifyHistoryChanged()
}

View File

@@ -47,6 +47,9 @@ class MangaZip(val file: File) {
if (!file.exists()) {
return
}
dir.listFiles()?.forEach {
it?.deleteRecursively()
}
ZipInputStream(file.inputStream()).use { input ->
while (true) {
val entry = input.nextEntry ?: return

View File

@@ -52,7 +52,7 @@ class TrackingRepository : KoinComponent {
db.tracksDao.upsert(entity)
}
suspend fun insertOrNothing(manga: Manga) {
suspend fun upsert(manga: Manga) {
val chapters = manga.chapters ?: return
val entity = TrackEntity(
mangaId = manga.id,
@@ -62,6 +62,6 @@ class TrackingRepository : KoinComponent {
lastCheck = System.currentTimeMillis(),
lastNotifiedChapterId = 0L
)
db.tracksDao.insert(entity)
db.tracksDao.upsert(entity)
}
}

View File

@@ -1,10 +1,12 @@
package org.koitharu.kotatsu.ui.browser
import android.graphics.Bitmap
import android.os.Build
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.annotation.RequiresApi
import okhttp3.OkHttpClient
import okhttp3.Request
import org.koin.core.KoinComponent
@@ -38,6 +40,7 @@ class BrowserClient(private val callback: BrowserCallback) : WebViewClient(), Ko
return url?.let(::doRequest)
}
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
return request?.url?.toString()?.let(::doRequest)
}
@@ -47,11 +50,11 @@ class BrowserClient(private val callback: BrowserCallback) : WebViewClient(), Ko
.url(url)
.build()
val response = okHttp.newCall(request).execute()
val ct = response.body?.contentType()
val ct = response.body()?.contentType()
WebResourceResponse(
"${ct?.type}/${ct?.subtype}",
"${ct?.type()}/${ct?.subtype()}",
ct?.charset()?.name() ?: "utf-8",
response.body?.byteStream()
response.body()?.byteStream()
)
}
}

View File

@@ -62,6 +62,21 @@ abstract class BaseActivity : MvpAppCompatActivity(), KoinComponent {
}
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
val keyCode = event.keyCode
val action = event.action
val isDown = action == KeyEvent.ACTION_DOWN
return if (keyCode == KeyEvent.KEYCODE_MENU) {
if (isDown) {
onKeyDown(keyCode, event)
} else {
onKeyUp(keyCode, event)
}
} else {
super.dispatchKeyEvent(event)
}
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
//TODO remove. Just for testing
if (BuildConfig.DEBUG && keyCode == KeyEvent.KEYCODE_VOLUME_UP) {

View File

@@ -1,36 +1,37 @@
package org.koitharu.kotatsu.ui.common
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.view.View
import android.view.WindowManager
abstract class BaseFullscreenActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
with(window) {
// addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
statusBarColor = Color.TRANSPARENT
navigationBarColor = Color.TRANSPARENT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
}
}
showSystemUI()
}
protected fun hideSystemUI() {
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // прячем панель навигации
or View.SYSTEM_UI_FLAG_FULLSCREEN // прячем строку состояния
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
)
window.decorView.systemUiVisibility =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // прячем панель навигации
or View.SYSTEM_UI_FLAG_FULLSCREEN // прячем строку состояния
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
)
} else {
(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // прячем панель навигации
or View.SYSTEM_UI_FLAG_FULLSCREEN // прячем строку состояния
)
}
}

View File

@@ -2,10 +2,12 @@ package org.koitharu.kotatsu.ui.common.dialog
import android.content.Context
import android.content.DialogInterface
import android.os.Build
import android.os.Environment
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import kotlinx.android.synthetic.main.item_storage.view.*
@@ -15,6 +17,7 @@ import org.koitharu.kotatsu.utils.ext.inflate
import org.koitharu.kotatsu.utils.ext.longHashCode
import java.io.File
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
class StorageSelectDialog private constructor(private val delegate: AlertDialog) :
DialogInterface by delegate {

View File

@@ -3,28 +3,16 @@ package org.koitharu.kotatsu.ui.common.dialog
import android.annotation.SuppressLint
import android.content.Context
import android.content.DialogInterface
import android.text.InputFilter
import android.view.LayoutInflater
import android.view.inputmethod.InputMethodManager
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import kotlinx.android.synthetic.main.dialog_input.view.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.utils.ext.showKeyboard
class TextInputDialog private constructor(private val delegate: AlertDialog) :
DialogInterface by delegate {
init {
delegate.setOnShowListener {
val view = delegate.findViewById<TextView>(R.id.inputEdit)?:return@setOnShowListener
view.post {
view.requestFocus()
view.showKeyboard()
}
}
}
fun show() = delegate.show()
class Builder(context: Context) {
@@ -34,10 +22,6 @@ class TextInputDialog private constructor(private val delegate: AlertDialog) :
private val delegate = AlertDialog.Builder(context)
.setView(view)
.setOnDismissListener {
val imm = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.toggleSoftInput(InputMethodManager.HIDE_IMPLICIT_ONLY, 0)
}
fun setTitle(@StringRes titleResId: Int): Builder {
delegate.setTitle(titleResId)
@@ -54,11 +38,28 @@ class TextInputDialog private constructor(private val delegate: AlertDialog) :
return this
}
fun setMaxLength(maxLength: Int, strict: Boolean): Builder {
with(view.inputLayout) {
counterMaxLength = maxLength
isCounterEnabled = maxLength > 0
}
if (strict && maxLength > 0) {
view.inputEdit.filters += InputFilter.LengthFilter(maxLength)
}
return this
}
fun setInputType(inputType: Int): Builder {
view.inputEdit.inputType = inputType
return this
}
fun setText(text: String): Builder {
view.inputEdit.setText(text)
view.inputEdit.setSelection(text.length)
return this
}
fun setPositiveButton(@StringRes textId: Int, listener: (DialogInterface, String) -> Unit): Builder {
delegate.setPositiveButton(textId) { dialog, _ ->
listener(dialog, view.inputEdit.text?.toString().orEmpty())

View File

@@ -2,7 +2,6 @@ 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) {
@@ -12,7 +11,7 @@ class AdapterUpdater<T>(oldList: List<T>, newList: List<T>, getId: (T) -> Long)
getId(oldList[oldItemPosition]) == getId(newList[newItemPosition])
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
Objects.equals(oldList[oldItemPosition], newList[newItemPosition])
oldList[oldItemPosition]?.equals(newList[newItemPosition]) == true
override fun getOldListSize() = oldList.size

View File

@@ -2,9 +2,10 @@ package org.koitharu.kotatsu.ui.common.list
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import okhttp3.internal.toImmutableList
import org.koin.core.KoinComponent
import org.koitharu.kotatsu.utils.ext.replaceWith
import java.util.*
import kotlin.collections.ArrayList
abstract class BaseRecyclerAdapter<T, E>(private val onItemClickListener: OnRecyclerItemClickListener<T>? = null) :
RecyclerView.Adapter<BaseViewHolder<T, E>>(),
@@ -12,7 +13,8 @@ abstract class BaseRecyclerAdapter<T, E>(private val onItemClickListener: OnRecy
protected val dataSet = ArrayList<T>() //TODO make private
val items get() = dataSet.toImmutableList()
val items: List<T>
get() = Collections.unmodifiableList(dataSet)
val hasItems get() = dataSet.isNotEmpty()

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.ui.common.list
import android.os.Build
import android.view.View
import android.view.ViewGroup
import androidx.annotation.LayoutRes
@@ -31,10 +32,15 @@ abstract class BaseViewHolder<T, E> protected constructor(view: View) :
fun setOnItemClickListener(listener: OnRecyclerItemClickListener<T>?): BaseViewHolder<T, E> {
if (listener != null) {
itemView.setOnClickListener {
listener.onItemClick(boundData ?: return@setOnClickListener, adapterPosition, it)
listener.onItemClick(boundData ?: return@setOnClickListener, bindingAdapterPosition, it)
}
itemView.setOnLongClickListener {
listener.onItemLongClick(boundData ?: return@setOnLongClickListener false, adapterPosition, it)
listener.onItemLongClick(boundData ?: return@setOnLongClickListener false, bindingAdapterPosition, it)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
itemView.setOnContextClickListener {
listener.onItemLongClick(boundData ?: return@setOnContextClickListener false, bindingAdapterPosition, it)
}
}
}
return this

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.ui.details
import android.app.ActivityOptions
import android.os.Bundle
import android.view.View
import androidx.core.view.isVisible
@@ -68,12 +69,19 @@ class ChaptersFragment : BaseFragment(R.layout.fragment_chapters), MangaDetailsV
override fun onFavouriteChanged(categories: List<FavouriteCategory>) = Unit
override fun onItemClick(item: MangaChapter, position: Int, view: View) {
val options = ActivityOptions.makeScaleUpAnimation(
view,
0,
0,
view.measuredWidth,
view.measuredHeight
)
startActivity(
ReaderActivity.newIntent(
context ?: return,
manga ?: return,
item.id
)
), options.toBundle()
)
}

View File

@@ -172,7 +172,7 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView {
companion object {
private const val EXTRA_MANGA = "manga"
private const val EXTRA_MANGA_ID = "manga_id"
const val EXTRA_MANGA_ID = "manga_id"
const val ACTION_MANGA_VIEW = "${BuildConfig.APPLICATION_ID}.action.VIEW_MANGA"

View File

@@ -13,7 +13,7 @@ import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.ui.common.BaseFragment
import org.koitharu.kotatsu.ui.main.list.favourites.categories.FavouriteCategoriesDialog
import org.koitharu.kotatsu.ui.main.list.favourites.categories.select.FavouriteCategoriesDialog
import org.koitharu.kotatsu.ui.reader.ReaderActivity
import org.koitharu.kotatsu.ui.search.MangaSearchSheet
import org.koitharu.kotatsu.utils.ext.addChips

View File

@@ -44,6 +44,7 @@ class DownloadNotification(private val context: Context) {
fun fillFrom(manga: Manga) {
builder.setContentTitle(manga.title)
builder.setContentText(context.getString(R.string.manga_downloading_))
builder.setTicker(context.getString(R.string.manga_downloading_))
builder.setProgress(1, 0, true)
builder.setSmallIcon(android.R.drawable.stat_sys_download)
builder.setLargeIcon(null)
@@ -56,7 +57,7 @@ class DownloadNotification(private val context: Context) {
} else {
val intent = DownloadService.getCancelIntent(context, startId)
builder.addAction(
R.drawable.ic_cross,
R.drawable.ic_cross_compat,
context.getString(android.R.string.cancel),
PendingIntent.getService(
context,

View File

@@ -80,7 +80,10 @@ class DownloadService : BaseService() {
notification.setCancelId(startId)
startForeground(DownloadNotification.NOTIFICATION_ID, notification())
}
val destination = getExternalFilesDir("manga")!!
val destination = getExternalFilesDir("manga") ?: filesDir.sub("manga").takeIf {
it.exists() || it.mkdir()
}
checkNotNull(destination) { "Cannot find place to store file" }
var output: MangaZip? = null
try {
val repo = MangaProviderFactory.create(manga.source)
@@ -146,6 +149,7 @@ class DownloadService : BaseService() {
notification.update()
}
} catch (e: Throwable) {
e.printStackTrace()
withContext(Dispatchers.Main) {
notification.setError(e)
notification.setCancelId(0)
@@ -179,7 +183,7 @@ class DownloadService : BaseService() {
okHttp.newCall(request).await().use { response ->
val file = destination.sub("page.tmp")
file.outputStream().use { out ->
response.body!!.byteStream().copyTo(out)
response.body()!!.byteStream().copyTo(out)
}
file
}

View File

@@ -1,9 +1,11 @@
package org.koitharu.kotatsu.ui.main
import android.app.ActivityOptions
import android.content.SharedPreferences
import android.content.res.ColorStateList
import android.content.res.Configuration
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
@@ -45,7 +47,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
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)
@@ -55,7 +56,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
navigationView.setNavigationItemSelectedListener(this)
settings.subscribe(this)
fab.imageTintList = ColorStateList.valueOf(Color.WHITE)
fab.supportImageTintList = ColorStateList.valueOf(Color.WHITE)
fab.isVisible = true
fab.setOnClickListener {
presenter.openLastReader()
@@ -119,7 +120,16 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
}
override fun onOpenReader(state: ReaderState) {
startActivity(ReaderActivity.newIntent(this, state))
val options = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ActivityOptions.makeClipRevealAnimation(
fab, 0, 0, fab.measuredWidth, fab.measuredHeight
)
} else {
ActivityOptions.makeScaleUpAnimation(
fab, 0, 0, fab.measuredWidth, fab.measuredHeight
)
}
startActivity(ReaderActivity.newIntent(this, state), options?.toBundle())
}
override fun onError(e: Throwable) {

View File

@@ -160,7 +160,7 @@ abstract class MangaListFragment<E> : BaseFragment(R.layout.fragment_list), Mang
.show()
} else {
textView_holder.text = e.getDisplayMessage(resources)
textView_holder.setCompoundDrawablesRelativeWithIntrinsicBounds(
textView_holder.setCompoundDrawablesWithIntrinsicBounds(
0,
R.drawable.ic_error_large,
0,
@@ -221,7 +221,7 @@ abstract class MangaListFragment<E> : BaseFragment(R.layout.fragment_list), Mang
}
protected open fun setUpEmptyListHolder() {
textView_holder.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, null, null)
textView_holder.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null)
textView_holder.setText(R.string.nothing_found)
}
@@ -237,6 +237,7 @@ abstract class MangaListFragment<E> : BaseFragment(R.layout.fragment_list), Mang
ListMode.GRID -> GridLayoutManager(ctx, UiUtils.resolveGridSpanCount(ctx))
else -> LinearLayoutManager(ctx)
}
recyclerView.recycledViewPool.clear()
recyclerView.adapter = adapter
recyclerView.addItemDecoration(
when (mode) {

View File

@@ -55,7 +55,7 @@ abstract class MangaListSheet<E> : BaseBottomSheet(R.layout.sheet_list), MangaLi
if (dialog !is BottomSheetDialog) {
toolbar.isVisible = true
textView_title.isVisible = false
appbar.elevation = resources.getDimension(R.dimen.elevation_large)
// appbar.elevation = resources.getDimension(R.dimen.elevation_large)
}
}
@@ -93,11 +93,11 @@ abstract class MangaListSheet<E> : BaseBottomSheet(R.layout.sheet_list), MangaLi
if (newState == BottomSheetBehavior.STATE_EXPANDED) {
toolbar.isVisible = true
textView_title.isVisible = false
appbar.elevation = elevation
// appbar.elevation = elevation
} else {
toolbar.isVisible = false
textView_title.isVisible = true
appbar.elevation = 0f
// appbar.elevation = 0f
}
}
})

View File

@@ -6,11 +6,11 @@ import android.view.MenuItem
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.ui.main.list.MangaListFragment
import org.koitharu.kotatsu.ui.main.list.MangaListView
import org.koitharu.kotatsu.ui.main.list.favourites.categories.CategoriesActivity
class FavouritesListFragment : MangaListFragment<Unit>(), MangaListView<Unit>{
class FavouritesListFragment : MangaListFragment<Unit>(), MangaListView<Unit> {
private val presenter by moxyPresenter(factory = ::FavouritesListPresenter)
@@ -19,12 +19,17 @@ class FavouritesListFragment : MangaListFragment<Unit>(), MangaListView<Unit>{
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
// inflater.inflate(R.menu.opt_history, menu)
inflater.inflate(R.menu.opt_favourites, menu)
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem) = when(item.itemId) {
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.action_categories -> {
context?.let {
startActivity(CategoriesActivity.newIntent(it))
}
true
}
else -> super.onOptionsItemSelected(item)
}
@@ -34,7 +39,7 @@ class FavouritesListFragment : MangaListFragment<Unit>(), MangaListView<Unit>{
override fun setUpEmptyListHolder() {
textView_holder.setText(R.string.you_have_not_favourites_yet)
textView_holder.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
textView_holder.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
}
companion object {

View File

@@ -0,0 +1,113 @@
package org.koitharu.kotatsu.ui.main.list.favourites.categories
import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Color
import android.os.Bundle
import android.text.InputType
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_categories.*
import moxy.ktx.moxyPresenter
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.ui.common.BaseActivity
import org.koitharu.kotatsu.ui.common.dialog.TextInputDialog
import org.koitharu.kotatsu.ui.common.list.OnRecyclerItemClickListener
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.showPopupMenu
class CategoriesActivity : BaseActivity(), OnRecyclerItemClickListener<FavouriteCategory>,
FavouriteCategoriesView, View.OnClickListener {
private val presenter by moxyPresenter(factory = ::FavouriteCategoriesPresenter)
private lateinit var adapter: CategoriesAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_categories)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
fab_add.supportImageTintList = ColorStateList.valueOf(Color.WHITE)
adapter = CategoriesAdapter(this)
recyclerView.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL))
recyclerView.adapter = adapter
fab_add.setOnClickListener(this)
}
override fun onClick(v: View) {
when (v.id) {
R.id.fab_add -> createCategory()
}
}
override fun onItemClick(item: FavouriteCategory, position: Int, view: View) {
view.showPopupMenu(R.menu.popup_category) {
when (it.itemId) {
R.id.action_remove -> deleteCategory(item)
R.id.action_rename -> renameCategory(item)
}
true
}
}
override fun onCategoriesChanged(categories: List<FavouriteCategory>) {
adapter.replaceData(categories)
textView_holder.isVisible = categories.isEmpty()
}
override fun onCheckedCategoriesChanged(checkedIds: Set<Int>) = Unit
override fun onError(e: Throwable) {
Snackbar.make(recyclerView, e.getDisplayMessage(resources), Snackbar.LENGTH_LONG)
.show()
}
private fun deleteCategory(category: FavouriteCategory) {
AlertDialog.Builder(this)
.setMessage(getString(R.string.category_delete_confirm, category.title))
.setTitle(R.string.remove_category)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.remove) { _, _ ->
presenter.deleteCategory(category.id)
}.create()
.show()
}
private fun renameCategory(category: FavouriteCategory) {
TextInputDialog.Builder(this)
.setTitle(R.string.rename)
.setText(category.title)
.setHint(R.string.enter_category_name)
.setInputType(InputType.TYPE_TEXT_VARIATION_PERSON_NAME or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES)
.setNegativeButton(android.R.string.cancel)
.setMaxLength(12, false)
.setPositiveButton(R.string.rename) { _, name ->
presenter.renameCategory(category.id, name)
}.create()
.show()
}
private fun createCategory() {
TextInputDialog.Builder(this)
.setTitle(R.string.add_new_category)
.setHint(R.string.enter_category_name)
.setInputType(InputType.TYPE_TEXT_VARIATION_PERSON_NAME or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES)
.setNegativeButton(android.R.string.cancel)
.setMaxLength(12, false)
.setPositiveButton(R.string.add) { _, name ->
presenter.createCategory(name)
}.create()
.show()
}
companion object {
fun newIntent(context: Context) = Intent(context, CategoriesActivity::class.java)
}
}

View File

@@ -1,46 +1,17 @@
package org.koitharu.kotatsu.ui.main.list.favourites.categories
import android.util.SparseBooleanArray
import android.view.ViewGroup
import android.widget.Checkable
import androidx.core.util.set
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.ui.common.list.BaseRecyclerAdapter
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
import org.koitharu.kotatsu.ui.common.list.OnRecyclerItemClickListener
import org.koitharu.kotatsu.ui.main.list.favourites.categories.select.CategoryCheckableHolder
class CategoriesAdapter(private val listener: OnCategoryCheckListener) :
BaseRecyclerAdapter<FavouriteCategory, Boolean>() {
class CategoriesAdapter(onItemClickListener: OnRecyclerItemClickListener<FavouriteCategory>? = null) :
BaseRecyclerAdapter<FavouriteCategory, Unit>(onItemClickListener) {
private val checkedIds = SparseBooleanArray()
fun setCheckedIds(ids: Iterable<Int>) {
checkedIds.clear()
ids.forEach {
checkedIds[it] = true
}
notifyDataSetChanged()
}
override fun getExtra(item: FavouriteCategory, position: Int) =
checkedIds.get(item.id.toInt(), false)
override fun onCreateViewHolder(parent: ViewGroup) =
CategoryHolder(
parent
)
override fun onCreateViewHolder(parent: ViewGroup) = CategoryHolder(parent)
override fun onGetItemId(item: FavouriteCategory) = item.id
override fun onViewHolderCreated(holder: BaseViewHolder<FavouriteCategory, Boolean>) {
super.onViewHolderCreated(holder)
holder.itemView.setOnClickListener {
if (it !is Checkable) return@setOnClickListener
it.toggle()
if (it.isChecked) {
listener.onCategoryChecked(holder.requireData())
} else {
listener.onCategoryUnchecked(holder.requireData())
}
}
}
override fun getExtra(item: FavouriteCategory, position: Int) = Unit
}

View File

@@ -1,16 +1,15 @@
package org.koitharu.kotatsu.ui.main.list.favourites.categories
import android.view.ViewGroup
import kotlinx.android.synthetic.main.item_category_checkable.*
import kotlinx.android.synthetic.main.item_category.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
class CategoryHolder(parent: ViewGroup) :
BaseViewHolder<FavouriteCategory, Boolean>(parent, R.layout.item_category_checkable) {
BaseViewHolder<FavouriteCategory, Unit>(parent, R.layout.item_category) {
override fun onBind(data: FavouriteCategory, extra: Boolean) {
checkedTextView.text = data.title
checkedTextView.isChecked = extra
override fun onBind(data: FavouriteCategory, extra: Unit) {
textView.text = data.title
}
}

View File

@@ -70,6 +70,40 @@ class FavouriteCategoriesPresenter : BasePresenter<FavouriteCategoriesView>() {
}
}
fun renameCategory(id: Long, name: String) {
presenterScope.launch {
try {
val categories = withContext(Dispatchers.IO) {
repository.renameCategory(id, name)
repository.getAllCategories()
}
viewState.onCategoriesChanged(categories)
} catch (e: Exception) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
viewState.onError(e)
}
}
}
fun deleteCategory(id: Long) {
presenterScope.launch {
try {
val categories = withContext(Dispatchers.IO) {
repository.removeCategory(id)
repository.getAllCategories()
}
viewState.onCategoriesChanged(categories)
} catch (e: Exception) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
viewState.onError(e)
}
}
}
fun addToCategory(manga: Manga, categoryId: Long) {
presenterScope.launch {
try {

View File

@@ -0,0 +1,46 @@
package org.koitharu.kotatsu.ui.main.list.favourites.categories.select
import android.util.SparseBooleanArray
import android.view.ViewGroup
import android.widget.Checkable
import androidx.core.util.set
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.ui.common.list.BaseRecyclerAdapter
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
class CategoriesSelectAdapter(private val listener: OnCategoryCheckListener) :
BaseRecyclerAdapter<FavouriteCategory, Boolean>() {
private val checkedIds = SparseBooleanArray()
fun setCheckedIds(ids: Iterable<Int>) {
checkedIds.clear()
ids.forEach {
checkedIds[it] = true
}
notifyDataSetChanged()
}
override fun getExtra(item: FavouriteCategory, position: Int) =
checkedIds.get(item.id.toInt(), false)
override fun onCreateViewHolder(parent: ViewGroup) =
CategoryCheckableHolder(
parent
)
override fun onGetItemId(item: FavouriteCategory) = item.id
override fun onViewHolderCreated(holder: BaseViewHolder<FavouriteCategory, Boolean>) {
super.onViewHolderCreated(holder)
holder.itemView.setOnClickListener {
if (it !is Checkable) return@setOnClickListener
it.toggle()
if (it.isChecked) {
listener.onCategoryChecked(holder.requireData())
} else {
listener.onCategoryUnchecked(holder.requireData())
}
}
}
}

View File

@@ -0,0 +1,16 @@
package org.koitharu.kotatsu.ui.main.list.favourites.categories.select
import android.view.ViewGroup
import kotlinx.android.synthetic.main.item_category_checkable.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
class CategoryCheckableHolder(parent: ViewGroup) :
BaseViewHolder<FavouriteCategory, Boolean>(parent, R.layout.item_category_checkable) {
override fun onBind(data: FavouriteCategory, extra: Boolean) {
checkedTextView.text = data.title
checkedTextView.isChecked = extra
}
}

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.main.list.favourites.categories
package org.koitharu.kotatsu.ui.main.list.favourites.categories.select
import android.os.Bundle
import android.text.InputType
@@ -12,6 +12,8 @@ import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.ui.common.BaseBottomSheet
import org.koitharu.kotatsu.ui.common.dialog.TextInputDialog
import org.koitharu.kotatsu.ui.main.list.favourites.categories.FavouriteCategoriesPresenter
import org.koitharu.kotatsu.ui.main.list.favourites.categories.FavouriteCategoriesView
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.withArgs
@@ -23,11 +25,13 @@ class FavouriteCategoriesDialog : BaseBottomSheet(R.layout.dialog_favorite_categ
private val manga get() = arguments?.getParcelable<Manga>(ARG_MANGA)
private var adapter: CategoriesAdapter? = null
private var adapter: CategoriesSelectAdapter? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
adapter = CategoriesAdapter(this)
adapter =
CategoriesSelectAdapter(
this)
recyclerView_categories.adapter = adapter
textView_add.setOnClickListener {
createCategory()
@@ -66,6 +70,7 @@ class FavouriteCategoriesDialog : BaseBottomSheet(R.layout.dialog_favorite_categ
TextInputDialog.Builder(context ?: return)
.setTitle(R.string.add_new_category)
.setHint(R.string.enter_category_name)
.setMaxLength(12, false)
.setInputType(InputType.TYPE_TEXT_VARIATION_PERSON_NAME or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES)
.setNegativeButton(android.R.string.cancel)
.setPositiveButton(R.string.add) { _, name ->
@@ -79,8 +84,10 @@ class FavouriteCategoriesDialog : BaseBottomSheet(R.layout.dialog_favorite_categ
private const val ARG_MANGA = "manga"
private const val TAG = "FavouriteCategoriesDialog"
fun show(fm: FragmentManager, manga: Manga) = FavouriteCategoriesDialog().withArgs(1) {
fun show(fm: FragmentManager, manga: Manga) = FavouriteCategoriesDialog()
.withArgs(1) {
putParcelable(ARG_MANGA, manga)
}.show(fm, TAG)
}.show(fm,
TAG)
}
}

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.main.list.favourites.categories
package org.koitharu.kotatsu.ui.main.list.favourites.categories.select
import org.koitharu.kotatsu.core.model.FavouriteCategory

View File

@@ -48,8 +48,8 @@ class HistoryListFragment : MangaListFragment<MangaHistory>(), MangaListView<Man
}
override fun setUpEmptyListHolder() {
textView_holder.setText(R.string.history_is_empty)
textView_holder.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
textView_holder.setText(R.string.text_history_holder)
textView_holder.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
}
override fun onCreatePopupMenu(inflater: MenuInflater, menu: Menu, data: Manga) {

View File

@@ -59,8 +59,8 @@ class LocalListFragment : MangaListFragment<File>() {
}
override fun setUpEmptyListHolder() {
textView_holder.setText(R.string.no_saved_manga)
textView_holder.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
textView_holder.setText(R.string.text_local_holder)
textView_holder.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {

View File

@@ -16,7 +16,7 @@ import kotlin.coroutines.CoroutineContext
class PageLoader : KoinComponent, CoroutineScope, DisposableHandle {
private val job = SupervisorJob()
private val tasks = HashMap<String, Job>()
private val tasks = HashMap<String, Deferred<File>>()
private val okHttp by inject<OkHttpClient>()
private val cache by inject<PagesCache>()
@@ -30,8 +30,13 @@ class PageLoader : KoinComponent, CoroutineScope, DisposableHandle {
return it
}
}
val task = tasks[url]?.takeUnless { it.isCancelled }
return (task ?: loadAsync(url).also { tasks[url] = it }).await()
}
private fun loadAsync(url: String) = async(Dispatchers.IO) {
val uri = Uri.parse(url)
return if (uri.scheme == "cbz") {
if (uri.scheme == "cbz") {
val zip = ZipFile(uri.schemeSpecificPart)
val entry = zip.getEntry(uri.fragment)
zip.getInputStream(entry).use {
@@ -46,13 +51,13 @@ class PageLoader : KoinComponent, CoroutineScope, DisposableHandle {
.cacheControl(CacheUtils.CONTROL_DISABLED)
.build()
okHttp.newCall(request).await().use { response ->
val body = response.body!!
val body = response.body()!!
val type = body.contentType()
check(type?.type == "image") {
"Unexpected content type ${type?.type}/${type?.subtype}"
check(type?.type() == "image") {
"Unexpected content type ${type?.type()}/${type?.subtype()}"
}
cache.put(url) { out ->
response.body!!.byteStream().copyTo(out)
response.body()!!.byteStream().copyTo(out)
}
}
}
@@ -60,5 +65,6 @@ class PageLoader : KoinComponent, CoroutineScope, DisposableHandle {
override fun dispose() {
coroutineContext.cancel()
tasks.clear()
}
}

View File

@@ -12,7 +12,6 @@ import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import androidx.core.view.postDelayed
import androidx.core.view.updatePadding
import androidx.fragment.app.commit
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_reader.*
@@ -79,10 +78,10 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
getString(R.string.chapter_d_of_d, state.chapter?.number ?: 0, size)
}
appbar_bottom.setOnApplyWindowInsetsListener { view, insets ->
/*appbar_bottom.setOnApplyWindowInsetsListener { view, insets ->
view.updatePadding(bottom = insets.systemWindowInsetBottom)
insets
}
}*/
settings.subscribe(this)
loadSettings()
@@ -286,6 +285,10 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
setUiIsVisible(!appbar_top.isVisible)
true
}
KeyEvent.KEYCODE_MENU -> {
setUiIsVisible(!appbar_top.isVisible)
true
}
else -> super.onKeyDown(keyCode, event)
}

View File

@@ -83,7 +83,7 @@ class ReaderPresenter : BasePresenter<ReaderView>() {
val fileName =
URLUtil.guessFileName(url, response.contentDisposition, response.mimeType)
MediaStoreCompat.insertImage(resolver, fileName) {
response.body!!.byteStream().copyTo(it)
response.body()!!.byteStream().copyTo(it)
}
}
withContext(Dispatchers.Main) {

View File

@@ -4,9 +4,11 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.commit
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.ui.common.BaseActivity
import org.koitharu.kotatsu.ui.settings.MainSettingsFragment
import org.koitharu.kotatsu.ui.settings.NetworkSettingsFragment
import org.koitharu.kotatsu.ui.settings.ReaderSettingsFragment
class SimpleSettingsActivity : BaseActivity() {
@@ -15,10 +17,10 @@ class SimpleSettingsActivity : BaseActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings_simple)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val section = intent?.getIntExtra(EXTRA_SECTION, 0)
supportFragmentManager.commit {
replace(R.id.container, when(section) {
SECTION_READER -> ReaderSettingsFragment()
replace(R.id.container, when(intent?.action) {
Intent.ACTION_MANAGE_NETWORK_USAGE -> NetworkSettingsFragment()
ACTION_READER -> ReaderSettingsFragment()
else -> MainSettingsFragment()
})
}
@@ -26,10 +28,9 @@ class SimpleSettingsActivity : BaseActivity() {
companion object {
private const val EXTRA_SECTION = "section"
private const val SECTION_READER = 1
private const val ACTION_READER = "${BuildConfig.APPLICATION_ID}.action.MANAGE_READER_SETTINGS"
fun newReaderSettingsIntent(context: Context) = Intent(context, SimpleSettingsActivity::class.java)
.putExtra(EXTRA_SECTION, SECTION_READER)
.setAction(ACTION_READER)
}
}

View File

@@ -0,0 +1,36 @@
package org.koitharu.kotatsu.ui.reader.standard
import android.view.View
import androidx.viewpager2.widget.ViewPager2
class PageAnimTransformer : ViewPager2.PageTransformer {
override fun transformPage(page: View, position: Float) {
page.apply {
val pageWidth = width
when {
position < -1 -> alpha = 0f
position <= 0 -> { // [-1,0]
alpha = 1f
translationX = 0f
// translationZ = 0f
scaleX = 1 + FACTOR * position
scaleY = 1f
}
position <= 1 -> { // (0,1]
alpha = 1f
translationX = pageWidth * -position
// translationZ = -1f
scaleX = 1f
scaleY = 1f
}
else -> alpha = 0f
}
}
}
private companion object {
const val FACTOR = 0.1f
}
}

View File

@@ -1,10 +1,14 @@
package org.koitharu.kotatsu.ui.reader.standard
import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
import android.view.View
import kotlinx.android.synthetic.main.fragment_reader_standard.*
import org.koin.android.ext.android.inject
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.ui.reader.ReaderState
import org.koitharu.kotatsu.ui.reader.base.AbstractReader
import org.koitharu.kotatsu.ui.reader.base.BaseReaderAdapter
@@ -12,19 +16,34 @@ import org.koitharu.kotatsu.ui.reader.base.GroupedList
import org.koitharu.kotatsu.utils.ext.doOnPageChanged
import org.koitharu.kotatsu.utils.ext.withArgs
class PagerReaderFragment : AbstractReader(R.layout.fragment_reader_standard) {
class PagerReaderFragment : AbstractReader(R.layout.fragment_reader_standard),
SharedPreferences.OnSharedPreferenceChangeListener {
private var paginationListener: PagerPaginationListener? = null
private val settings by inject<AppSettings>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
paginationListener = PagerPaginationListener(adapter!!, 2, this)
pager.adapter = adapter
if (settings.readerAnimation) {
pager.setPageTransformer(PageAnimTransformer())
}
pager.offscreenPageLimit = 2
pager.registerOnPageChangeCallback(paginationListener!!)
pager.doOnPageChanged(::notifyPageChanged)
}
override fun onAttach(context: Context) {
super.onAttach(context)
settings.subscribe(this)
}
override fun onDetach() {
settings.unsubscribe(this)
super.onDetach()
}
override fun onDestroyView() {
paginationListener = null
super.onDestroyView()
@@ -47,6 +66,18 @@ class PagerReaderFragment : AbstractReader(R.layout.fragment_reader_standard) {
override fun restorePageScroll(position: Int, scroll: Float) = Unit
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
when (key) {
getString(R.string.key_reader_animation) -> {
if (settings.readerAnimation) {
pager.setPageTransformer(PageAnimTransformer())
} else {
pager.setPageTransformer(null)
}
}
}
}
companion object {
fun newInstance(state: ReaderState) = PagerReaderFragment().withArgs(1) {

View File

@@ -40,7 +40,7 @@ class PagesThumbnailsSheet : BaseBottomSheet(R.layout.sheet_pages),
if (dialog !is BottomSheetDialog) {
toolbar.isVisible = true
textView_title.isVisible = false
appbar.elevation = resources.getDimension(R.dimen.elevation_large)
// appbar.elevation = resources.getDimension(R.dimen.elevation_large)
}
recyclerView.addOnLayoutChangeListener(UiUtils.SpanCountResolver)
}
@@ -57,11 +57,11 @@ class PagesThumbnailsSheet : BaseBottomSheet(R.layout.sheet_pages),
if (newState == BottomSheetBehavior.STATE_EXPANDED) {
toolbar.isVisible = true
textView_title.isVisible = false
appbar.elevation = elevation
// appbar.elevation = elevation
} else {
toolbar.isVisible = false
textView_title.isVisible = true
appbar.elevation = 0f
// appbar.elevation = 0f
}
}
})

View File

@@ -1,10 +1,12 @@
package org.koitharu.kotatsu.ui.settings
import android.annotation.SuppressLint
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Build
@@ -22,8 +24,18 @@ import org.koitharu.kotatsu.core.github.VersionId
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.ui.common.BaseService
import org.koitharu.kotatsu.utils.FileSizeUtils
import org.koitharu.kotatsu.utils.ext.byte2HexFormatted
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.security.cert.CertificateEncodingException
import java.security.cert.CertificateException
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit
class AppUpdateService : BaseService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@@ -92,25 +104,25 @@ class AppUpdateService : BaseService() {
)
builder.setSmallIcon(R.drawable.ic_stat_update)
builder.setAutoCancel(true)
builder.setColor(ContextCompat.getColor(this, R.color.blue_primary_dark))
builder.color = ContextCompat.getColor(this, R.color.blue_primary_dark)
builder.setLargeIcon(BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher))
manager.notify(NOTIFICATION_ID, builder.build())
}
companion object {
private const val CERT_SHA1 = "2C:19:C7:E8:07:61:2B:8E:94:51:1B:FD:72:67:07:64:5D:C2:58:AE"
private const val NOTIFICATION_ID = 202
private const val CHANNEL_ID = "update"
private val PERIOD = TimeUnit.HOURS.toMillis(6)
fun start(context: Context) {
try {
context.startService(Intent(context, AppUpdateService::class.java))
} catch (_: IllegalStateException) {
}
}
@Suppress("UNUSED_PARAMETER")
fun isUpdateSupported(context: Context) = false
fun startIfRequired(context: Context) {
if (!isUpdateSupported(context)) {
return
}
val settings = AppSettings(context)
if (settings.appUpdateAuto) {
val lastUpdate = settings.appUpdate
@@ -119,5 +131,47 @@ class AppUpdateService : BaseService() {
}
}
}
private fun start(context: Context) {
try {
context.startService(Intent(context, AppUpdateService::class.java))
} catch (_: IllegalStateException) {
}
}
@Suppress("DEPRECATION")
@SuppressLint("PackageManagerGetSignatures")
private fun getCertificateSHA1Fingerprint(context: Context): String? {
val packageInfo = try {
context.packageManager.getPackageInfo(
context.packageName,
PackageManager.GET_SIGNATURES
)
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
return null
}
val signatures = packageInfo?.signatures
val cert: ByteArray = signatures?.firstOrNull()?.toByteArray() ?: return null
val input: InputStream = ByteArrayInputStream(cert)
val c = try {
val cf = CertificateFactory.getInstance("X509")
cf.generateCertificate(input) as X509Certificate
} catch (e: CertificateException) {
e.printStackTrace()
return null
}
return try {
val md: MessageDigest = MessageDigest.getInstance("SHA1")
val publicKey: ByteArray = md.digest(c.getEncoded())
publicKey.byte2HexFormatted()
} catch (e: NoSuchAlgorithmException) {
e.printStackTrace()
null
} catch (e: CertificateEncodingException) {
e.printStackTrace()
null
}
}
}
}

View File

@@ -37,6 +37,9 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
}
findPreference<MultiSelectListPreference>(R.string.key_reader_switchers)?.summaryProvider =
MultiSummaryProvider(R.string.gestures_only)
findPreference<Preference>(R.string.key_app_update_auto)?.run {
isVisible = AppUpdateService.isUpdateSupported(context)
}
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {

View File

@@ -0,0 +1,12 @@
package org.koitharu.kotatsu.ui.settings
import android.os.Bundle
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.ui.common.BasePreferenceFragment
class NetworkSettingsFragment : BasePreferenceFragment(R.string.settings) {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
//TODO https://developer.android.com/training/basics/network-ops/managing
}
}

View File

@@ -3,8 +3,6 @@ package org.koitharu.kotatsu.ui.settings
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.transition.Slide
import android.view.Gravity
import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
import androidx.preference.Preference
@@ -24,7 +22,7 @@ class SettingsActivity : BaseActivity(),
if (supportFragmentManager.findFragmentById(R.id.container) == null) {
supportFragmentManager.commit {
replace(R.id.container, MainSettingsFragment().also {
it.exitTransition = Slide(Gravity.START)
// it.exitTransition = Slide(Gravity.START)
})
}
}
@@ -48,8 +46,8 @@ class SettingsActivity : BaseActivity(),
}
private fun openFragment(fragment: Fragment) {
fragment.enterTransition = Slide(Gravity.END)
fragment.exitTransition = Slide(Gravity.START)
// fragment.enterTransition = Slide(Gravity.END)
// fragment.exitTransition = Slide(Gravity.START)
supportFragmentManager.commit {
replace(R.id.container, fragment)
addToBackStack(null)

View File

@@ -19,9 +19,10 @@ class AppCrashHandler(private val applicationContext: Context) : Thread.Uncaught
intent.flags = (Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
try {
applicationContext.startActivity(intent)
} catch (e: Throwable) {
e.printStackTrace()
} catch (t: Throwable) {
t.printStackTrace()
}
e.printStackTrace()
exitProcess(1)
}
}

View File

@@ -0,0 +1,31 @@
package org.koitharu.kotatsu.ui.widget
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import org.koitharu.kotatsu.domain.favourites.OnFavouritesChangeListener
import org.koitharu.kotatsu.domain.history.OnHistoryChangeListener
import org.koitharu.kotatsu.ui.widget.recent.RecentWidgetProvider
import org.koitharu.kotatsu.ui.widget.shelf.ShelfWidgetProvider
class WidgetUpdater(private val context: Context) : OnFavouritesChangeListener,
OnHistoryChangeListener {
override fun onFavouritesChanged(mangaId: Long) {
updateWidget(ShelfWidgetProvider::class.java)
}
override fun onHistoryChanged() {
updateWidget(RecentWidgetProvider::class.java)
}
private fun updateWidget(cls: Class<*>) {
val intent = Intent(context, cls)
intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
val ids = AppWidgetManager.getInstance(context)
.getAppWidgetIds(ComponentName(context, cls))
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
context.sendBroadcast(intent)
}
}

View File

@@ -0,0 +1,61 @@
package org.koitharu.kotatsu.ui.widget.recent
import android.content.Context
import android.content.Intent
import android.widget.RemoteViews
import android.widget.RemoteViewsService
import androidx.core.graphics.drawable.toBitmap
import coil.Coil
import coil.api.get
import kotlinx.coroutines.runBlocking
import okio.IOException
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.domain.history.HistoryRepository
import org.koitharu.kotatsu.ui.details.MangaDetailsActivity
class RecentListFactory(context: Context, private val intent: Intent) : RemoteViewsService.RemoteViewsFactory {
private val packageName = context.packageName
private val dataSet = ArrayList<Manga>()
override fun onCreate() {
}
override fun getLoadingView() = null
override fun getItemId(position: Int) = dataSet[position].id
override fun onDataSetChanged() {
dataSet.clear()
val data = runBlocking { HistoryRepository().getList(0, 10) }
dataSet.addAll(data)
}
override fun hasStableIds() = true
override fun getViewAt(position: Int): RemoteViews {
val views = RemoteViews(packageName, R.layout.item_recent)
val item = dataSet[position]
try {
val cover = runBlocking {
Coil.loader().get(item.coverUrl).toBitmap()
}
views.setImageViewBitmap(R.id.imageView_cover, cover)
} catch (e: IOException) {
views.setImageViewResource(R.id.imageView_cover, R.drawable.ic_placeholder)
}
val intent = Intent()
intent.putExtra(MangaDetailsActivity.EXTRA_MANGA_ID, item.id)
views.setOnClickFillInIntent(R.id.imageView_cover, intent)
return views
}
override fun getCount() = dataSet.size
override fun getViewTypeCount() = 1
override fun onDestroy() {
}
}

View File

@@ -0,0 +1,39 @@
package org.koitharu.kotatsu.ui.widget.recent
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.widget.RemoteViews
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.ui.details.MangaDetailsActivity
class RecentWidgetProvider : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
appWidgetIds.forEach { id ->
val views = RemoteViews(context.packageName, R.layout.widget_recent)
val adapter = Intent(context, RecentWidgetService::class.java)
adapter.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, id)
adapter.data = Uri.parse(adapter.toUri(Intent.URI_INTENT_SCHEME))
views.setRemoteAdapter(R.id.stackView, adapter)
val intent = Intent(context, MangaDetailsActivity::class.java)
intent.action = MangaDetailsActivity.ACTION_MANGA_VIEW
views.setPendingIntentTemplate(R.id.stackView, PendingIntent.getActivity(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT
))
views.setEmptyView(R.id.stackView, R.id.textView_holder)
appWidgetManager.updateAppWidget(id, views)
}
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.stackView)
}
}

View File

@@ -0,0 +1,11 @@
package org.koitharu.kotatsu.ui.widget.recent
import android.content.Intent
import android.widget.RemoteViewsService
class RecentWidgetService : RemoteViewsService() {
override fun onGetViewFactory(intent: Intent): RemoteViewsFactory {
return RecentListFactory(this, intent)
}
}

View File

@@ -0,0 +1,62 @@
package org.koitharu.kotatsu.ui.widget.shelf
import android.content.Context
import android.content.Intent
import android.widget.RemoteViews
import android.widget.RemoteViewsService
import androidx.core.graphics.drawable.toBitmap
import coil.Coil
import coil.api.get
import kotlinx.coroutines.runBlocking
import okio.IOException
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.domain.favourites.FavouritesRepository
import org.koitharu.kotatsu.ui.details.MangaDetailsActivity
class ShelfListFactory(context: Context, private val intent: Intent) : RemoteViewsService.RemoteViewsFactory {
private val packageName = context.packageName
private val dataSet = ArrayList<Manga>()
override fun onCreate() {
}
override fun getLoadingView() = null
override fun getItemId(position: Int) = dataSet[position].id
override fun onDataSetChanged() {
dataSet.clear()
val data = runBlocking { FavouritesRepository().getAllManga(0) }
dataSet.addAll(data)
}
override fun hasStableIds() = true
override fun getViewAt(position: Int): RemoteViews {
val views = RemoteViews(packageName, R.layout.item_shelf)
val item = dataSet[position]
views.setTextViewText(R.id.textView_title, item.title)
try {
val cover = runBlocking {
Coil.loader().get(item.coverUrl).toBitmap()
}
views.setImageViewBitmap(R.id.imageView_cover, cover)
} catch (e: IOException) {
views.setImageViewResource(R.id.imageView_cover, R.drawable.ic_placeholder)
}
val intent = Intent()
intent.putExtra(MangaDetailsActivity.EXTRA_MANGA_ID, item.id)
views.setOnClickFillInIntent(R.id.rootLayout, intent)
return views
}
override fun getCount() = dataSet.size
override fun getViewTypeCount() = 1
override fun onDestroy() {
}
}

View File

@@ -0,0 +1,39 @@
package org.koitharu.kotatsu.ui.widget.shelf
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.widget.RemoteViews
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.ui.details.MangaDetailsActivity
class ShelfWidgetProvider : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
appWidgetIds.forEach { id ->
val views = RemoteViews(context.packageName, R.layout.widget_shelf)
val adapter = Intent(context, ShelfWidgetService::class.java)
adapter.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, id)
adapter.data = Uri.parse(adapter.toUri(Intent.URI_INTENT_SCHEME))
views.setRemoteAdapter(R.id.gridView, adapter)
val intent = Intent(context, MangaDetailsActivity::class.java)
intent.action = MangaDetailsActivity.ACTION_MANGA_VIEW
views.setPendingIntentTemplate(R.id.gridView, PendingIntent.getActivity(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT
))
views.setEmptyView(R.id.gridView, R.id.textView_holder)
appWidgetManager.updateAppWidget(id, views)
}
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.gridView)
}
}

View File

@@ -0,0 +1,11 @@
package org.koitharu.kotatsu.ui.widget.shelf
import android.content.Intent
import android.widget.RemoteViewsService
class ShelfWidgetService : RemoteViewsService() {
override fun onGetViewFactory(intent: Intent): RemoteViewsFactory {
return ShelfListFactory(this, intent)
}
}

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.utils
import android.content.Context
import android.os.Build
import androidx.annotation.WorkerThread
import okhttp3.Cache
import okhttp3.CacheControl
@@ -17,8 +18,11 @@ object CacheUtils {
.build()
@JvmStatic
fun getCacheDirs(context: Context) = (context.externalCacheDirs + context.cacheDir)
.filterNotNull()
fun getCacheDirs(context: Context) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
(context.externalCacheDirs + context.cacheDir)
} else {
arrayOf(context.externalCacheDir, context.cacheDir)
}.filterNotNull()
.distinctBy { it.absolutePath }
@JvmStatic
@@ -35,7 +39,7 @@ object CacheUtils {
@JvmStatic
fun createHttpCache(context: Context) = Cache(
directory = (context.externalCacheDir ?: context.cacheDir).sub("http"),
maxSize = FileSizeUtils.mbToBytes(60)
(context.externalCacheDir ?: context.cacheDir).sub("http"),
FileSizeUtils.mbToBytes(60)
)
}

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.utils.ext
import android.content.res.Resources
import android.util.Log
import kotlinx.coroutines.delay
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
@@ -43,4 +44,12 @@ fun Throwable.getDisplayMessage(resources: Resources) = when (this) {
} else {
resources.getString(R.string.error_occurred)
}
}
inline fun <T> measured(tag: String, block: () -> T): T {
val time = System.currentTimeMillis()
val res = block()
val spent = System.currentTimeMillis() - time
Log.d("measured", "$tag ${spent.format(1)} ms")
return res
}

View File

@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.utils.ext
import okhttp3.Response
val Response.mimeType: String?
get() = body?.contentType()?.run { "$type/$subtype" }
get() = body()?.contentType()?.run { "${type()}/${subtype()}" }
val Response.contentDisposition: String?
get() = header("Content-Disposition")

View File

@@ -1,30 +1,30 @@
package org.koitharu.kotatsu.utils.ext
import okhttp3.Response
import okhttp3.internal.closeQuietly
import okhttp3.internal.Util.closeQuietly
import org.json.JSONObject
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
fun Response.parseHtml(): Document {
try {
val stream = body?.byteStream() ?: throw NullPointerException("Response body is null")
val charset = body!!.contentType()?.charset()?.name()
val stream = body()?.byteStream() ?: throw NullPointerException("Response body is null")
val charset = body()!!.contentType()?.charset()?.name()
return Jsoup.parse(
stream,
charset,
request.url.toString()
request().url().toString()
)
} finally {
closeQuietly()
closeQuietly(this)
}
}
fun Response.parseJson(): JSONObject {
try {
val string = body?.string() ?: throw NullPointerException("Response body is null")
val string = body()?.string() ?: throw NullPointerException("Response body is null")
return JSONObject(string)
} finally {
closeQuietly()
closeQuietly(this)
}
}

View File

@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.utils.ext
import android.net.Uri
import java.net.URLEncoder
import java.util.*
fun String.longHashCode(): Long {
var h = 1125899906842597L
@@ -72,4 +73,23 @@ fun String.toUriOrNull(): Uri? = if (isEmpty()) {
null
} else {
Uri.parse(this)
}
fun ByteArray.byte2HexFormatted(): String? {
val str = StringBuilder(size * 2)
for (i in indices) {
var h = Integer.toHexString(this[i].toInt())
val l = h.length
if (l == 1) {
h = "0$h"
}
if (l > 2) {
h = h.substring(l - 2, l)
}
str.append(h.toUpperCase(Locale.ROOT))
if (i < size - 1) {
str.append(':')
}
}
return str.toString()
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 534 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

View File

@@ -2,7 +2,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="5dp" />
<solid android:color="?colorAccent" />
<solid android:color="@color/red_accent" />
<padding
android:bottom="2dp"
android:left="2dp"

View File

@@ -2,7 +2,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="5dp" />
<solid android:color="?android:textColorTertiary" />
<solid android:color="@android:color/darker_gray" />
<padding
android:bottom="2dp"
android:left="2dp"

View File

@@ -4,7 +4,7 @@
<corners android:radius="5dp" />
<stroke
android:width="2dp"
android:color="?android:textColorTertiary" />
android:color="@android:color/darker_gray" />
<padding
android:bottom="2dp"
android:left="2dp"

View File

@@ -4,7 +4,7 @@
<corners android:radius="5dp" />
<stroke
android:width="2dp"
android:color="?android:colorAccent" />
android:color="@color/red_accent" />
<padding
android:bottom="2dp"
android:left="2dp"

View File

@@ -1,12 +0,0 @@
<!-- drawable/book_outline.xml -->
<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">
<path
android:fillColor="#000"
android:pathData="M18,2A2,2 0 0,1 20,4V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2H18M18,4H13V12L10.5,9.75L8,12V4H6V20H18V4Z" />
</vector>

View File

@@ -3,7 +3,7 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?android:textColorPrimary"
android:tint="@android:color/darker_gray"
android:viewportWidth="24"
android:viewportHeight="24">
<path

View File

@@ -2,7 +2,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12dp"
android:height="12dp"
android:tint="?android:textColorPrimary"
android:tint="?attr/android:textColorPrimary"
android:viewportWidth="24"
android:viewportHeight="24">
<path

View File

@@ -3,7 +3,7 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12dp"
android:height="12dp"
android:tint="?android:textColorPrimary"
android:tint="?attr/android:textColorPrimary"
android:viewportWidth="24"
android:viewportHeight="24">
<path

View File

@@ -2,7 +2,7 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?android:textColorPrimary"
android:tint="?attr/android:textColorPrimary"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path

View File

@@ -1,11 +0,0 @@
<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">
<path
android:fillColor="#000"
android:pathData="M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z" />
</vector>

View File

@@ -1,23 +0,0 @@
<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="M12,22C6.49,22 2,17.51 2,12S6.49,2 12,2s10,4.04 10,9c0,3.31 -2.69,6 -6,6h-1.77c-0.28,0 -0.5,0.22 -0.5,0.5 0,0.12 0.05,0.23 0.13,0.33 0.41,0.47 0.64,1.06 0.64,1.67 0,1.38 -1.12,2.5 -2.5,2.5zM12,4c-4.41,0 -8,3.59 -8,8s3.59,8 8,8c0.28,0 0.5,-0.22 0.5,-0.5 0,-0.16 -0.08,-0.28 -0.14,-0.35 -0.41,-0.46 -0.63,-1.05 -0.63,-1.65 0,-1.38 1.12,-2.5 2.5,-2.5L16,15c2.21,0 4,-1.79 4,-4 0,-3.86 -3.59,-7 -8,-7z" />
<path
android:fillColor="@android:color/white"
android:pathData="M6.5,11.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0" />
<path
android:fillColor="@android:color/white"
android:pathData="M9.5,7.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0" />
<path
android:fillColor="@android:color/white"
android:pathData="M14.5,7.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0" />
<path
android:fillColor="@android:color/white"
android:pathData="M17.5,11.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0" />
</vector>

View File

@@ -1,7 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="60dp"
android:height="60dp"
android:tint="?android:textColorTertiary"
android:tint="?attr/android:textColorTertiary"
android:viewportWidth="60"
android:viewportHeight="60">
<path

View File

@@ -2,7 +2,7 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?android:colorControlNormal"
android:tint="?attr/android:textColorPrimary"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path

View File

@@ -8,6 +8,6 @@
android:fillColor="?attr/colorAccent"
android:pathData="M12,15.4l-3.76,2.27l1,-4.28l-3.32,-2.88l4.38,-0.38l1.7,-4.03l1.71,4.04l4.38,0.38l-3.32,2.88l1,4.28z" />
<path
android:fillColor="?android:textColorPrimary"
android:fillColor="?attr/android:textColorPrimary"
android:pathData="M22,9.24l-7.19,-0.62L12,2L9.19,8.63L2,9.24l5.46,4.73L5.82,21L12,17.27L18.18,21l-1.63,-7.03L22,9.24zM12,15.4l-3.76,2.27l1,-4.28l-3.32,-2.88l4.38,-0.38L12,6.1l1.71,4.04l4.38,0.38l-3.32,2.88l1,4.28L12,15.4z" />
</vector>

View File

@@ -2,7 +2,7 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?android:textColorPrimary"
android:tint="?attr/android:textColorPrimary"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<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">
<path
android:fillColor="#000"
android:pathData="M16.36,14C16.44,13.34 16.5,12.68 16.5,12C16.5,11.32 16.44,10.66 16.36,10H19.74C19.9,10.64 20,11.31 20,12C20,12.69 19.9,13.36 19.74,14M14.59,19.56C15.19,18.45 15.65,17.25 15.97,16H18.92C17.96,17.65 16.43,18.93 14.59,19.56M14.34,14H9.66C9.56,13.34 9.5,12.68 9.5,12C9.5,11.32 9.56,10.65 9.66,10H14.34C14.43,10.65 14.5,11.32 14.5,12C14.5,12.68 14.43,13.34 14.34,14M12,19.96C11.17,18.76 10.5,17.43 10.09,16H13.91C13.5,17.43 12.83,18.76 12,19.96M8,8H5.08C6.03,6.34 7.57,5.06 9.4,4.44C8.8,5.55 8.35,6.75 8,8M5.08,16H8C8.35,17.25 8.8,18.45 9.4,19.56C7.57,18.93 6.03,17.65 5.08,16M4.26,14C4.1,13.36 4,12.69 4,12C4,11.31 4.1,10.64 4.26,10H7.64C7.56,10.66 7.5,11.32 7.5,12C7.5,12.68 7.56,13.34 7.64,14M12,4.03C12.83,5.23 13.5,6.57 13.91,8H10.09C10.5,6.57 11.17,5.23 12,4.03M18.92,8H15.97C15.65,6.75 15.19,5.55 14.59,4.44C16.43,5.07 17.96,6.34 18.92,8M12,2C6.47,2 2,6.5 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" />
</vector>

View File

@@ -16,11 +16,12 @@
android:layout_height="0dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:scaleType="centerInside"
android:scaleType="fitCenter"
app:layout_constraintDimensionRatio="13:18"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.3" />
app:layout_constraintWidth_percent="0.3"
android:layout_marginLeft="8dp" />
<TextView
android:id="@+id/textView_title"
@@ -73,7 +74,8 @@
app:icon="@drawable/ic_read"
app:iconPadding="12dp"
app:layout_constraintEnd_toEndOf="@id/textView_title"
app:layout_constraintTop_toBottomOf="@id/ratingBar" />
app:layout_constraintTop_toBottomOf="@id/ratingBar"
android:layout_marginRight="4dp" />
<ImageView
android:id="@+id/imageView_favourite"
@@ -83,12 +85,13 @@
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/add_to_favourites"
android:scaleType="center"
android:src="@drawable/ic_tag_heart_outline"
app:srcCompat="@drawable/ic_tag_heart_outline"
app:layout_constraintBottom_toBottomOf="@id/button_read"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toStartOf="@id/button_read"
app:layout_constraintTop_toTopOf="@id/button_read"
app:tint="?colorAccent" />
app:tint="?colorAccent"
android:layout_marginRight="4dp" />
<View
android:id="@+id/divider_top"

View File

@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorPrimary"
android:fitsSystemWindows="true"
android:theme="@style/AppToolbarTheme">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:popupTheme="@style/AppPopupTheme" />
</com.google.android.material.appbar.AppBarLayout>
<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.LinearLayoutManager"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
<TextView
android:id="@+id/textView_holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:visibility="gone"
tools:visibility="visible"
android:layout_margin="20dp"
android:textAppearance="?android:textAppearanceMedium"
android:textColor="?android:textColorSecondary"
android:text="@string/text_categories_holder"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="@string/add_new_category"
app:srcCompat="@drawable/ic_add"
app:backgroundTint="?colorAccent"
app:fabSize="normal"
app:layout_anchor="@id/recyclerView"
app:layout_anchorGravity="bottom|end"
app:layout_dodgeInsetEdges="bottom" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -13,7 +13,9 @@
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="2dp"
android:layout_marginBottom="2dp">
android:layout_marginBottom="2dp"
android:layout_alignParentLeft="true"
android:layout_alignParentRight="true">
<TextView
android:id="@+id/textView"
@@ -31,7 +33,8 @@
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_toStartOf="@id/button_restart"
android:text="@string/close" />
android:text="@string/close"
android:layout_toLeftOf="@id/button_restart" />
<Button
android:id="@+id/button_restart"
@@ -40,6 +43,7 @@
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:text="@string/restart" />
android:text="@string/restart"
android:layout_alignParentRight="true" />
</RelativeLayout>

View File

@@ -39,7 +39,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:src="@drawable/ic_read_fill"
app:srcCompat="@drawable/ic_read_fill"
android:visibility="gone"
app:fabSize="normal"
app:backgroundTint="?colorAccent"

View File

@@ -5,7 +5,9 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="?android:listPreferredItemPaddingStart"
android:paddingEnd="?android:listPreferredItemPaddingEnd">
android:paddingEnd="?android:listPreferredItemPaddingEnd"
android:paddingRight="?android:listPreferredItemPaddingRight"
android:paddingLeft="?android:listPreferredItemPaddingLeft">
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@android:id/checkbox"

View File

@@ -26,6 +26,7 @@
android:layout_height="wrap_content"
android:orientation="vertical"
android:scrollbars="vertical"
android:overScrollMode="never"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_category_checkable" />
@@ -39,12 +40,14 @@
android:layout_width="match_parent"
android:layout_height="?android:listPreferredItemHeightSmall"
android:background="?android:selectableItemBackground"
android:drawableEnd="@drawable/ic_add"
android:gravity="start|center_vertical"
android:paddingStart="?android:listPreferredItemPaddingStart"
android:paddingEnd="?android:listPreferredItemPaddingEnd"
android:text="@string/add_new_category"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="?android:textColorPrimary" />
android:textColor="?android:textColorPrimary"
app:drawableEndCompat="@drawable/ic_add"
android:paddingRight="?android:listPreferredItemPaddingRight"
android:paddingLeft="?android:listPreferredItemPaddingLeft" />
</LinearLayout>

View File

@@ -13,6 +13,7 @@
android:id="@+id/inputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:boxBackgroundColor="@android:color/transparent"
app:boxBackgroundMode="filled">
<com.google.android.material.textfield.TextInputEditText
@@ -21,6 +22,7 @@
android:layout_height="wrap_content"
android:imeOptions="actionDone"
android:maxLines="1"
android:singleLine="true"
tools:text="@tools:sample/lorem[2]" />
</com.google.android.material.textfield.TextInputLayout>

View File

@@ -20,7 +20,8 @@
app:layout_constraintDimensionRatio="13:18"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.3" />
app:layout_constraintWidth_percent="0.3"
android:layout_marginLeft="8dp" />
<TextView
android:id="@+id/textView_title"
@@ -76,7 +77,8 @@
app:layout_constraintBottom_toBottomOf="@id/imageView_cover"
app:layout_constraintEnd_toEndOf="@id/textView_title"
app:layout_constraintTop_toBottomOf="@id/ratingBar"
app:layout_constraintVertical_bias="1" />
app:layout_constraintVertical_bias="1"
android:layout_marginRight="4dp" />
<ImageView
android:id="@+id/imageView_favourite"
@@ -86,14 +88,15 @@
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/add_to_favourites"
android:scaleType="center"
android:src="@drawable/ic_tag_heart_outline"
app:srcCompat="@drawable/ic_tag_heart_outline"
app:layout_constraintBottom_toBottomOf="@id/button_read"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintEnd_toStartOf="@id/button_read"
app:layout_constraintTop_toTopOf="@id/button_read"
app:layout_constraintStart_toStartOf="@id/textView_title"
app:tint="?colorAccent" />
app:tint="?colorAccent"
android:layout_marginRight="4dp" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_title"

Some files were not shown because too many files have changed in this diff Show More