Compare commits
16 Commits
v0.2-b1
...
v0.3-legac
| Author | SHA1 | Date | |
|---|---|---|---|
| 5fe40ac17e | |||
| 0dc74f9188 | |||
| f95cf9b231 | |||
| 0d0982b244 | |||
| ef4dd82e92 | |||
| bc825681a8 | |||
| da6204f44f | |||
| 10c68bdd72 | |||
| b1e90dde8f | |||
| e0d45961f8 | |||
| b732a220f6 | |||
| 582adae11f | |||
| c13c43c616 | |||
| ab1eacea3f | |||
| ac4b97928a | |||
| aa8281678b |
1
.idea/codeStyles/Project.xml
generated
@@ -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">
|
||||
|
||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal 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>
|
||||
11
README.md
@@ -2,13 +2,11 @@
|
||||
|
||||
Kotatsu is a free and open source manga reader for Android.
|
||||
|
||||
  [](https://travis-ci.org/nv95/Kotatsu)  [](http://4pda.ru/forum/index.php?showtopic=697669)
|
||||
  [](https://travis-ci.org/nv95/Kotatsu)  [](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
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) } }
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 // прячем строку состояния
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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?) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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?) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
BIN
app/src/main/res/drawable-hdpi/ic_cross_compat.png
Normal file
|
After Width: | Height: | Size: 319 B |
BIN
app/src/main/res/drawable-mdpi/ic_cross_compat.png
Normal file
|
After Width: | Height: | Size: 162 B |
BIN
app/src/main/res/drawable-nodpi/ic_appwidget_recent.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
app/src/main/res/drawable-nodpi/ic_appwidget_shelf.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_cross_compat.png
Normal file
|
After Width: | Height: | Size: 254 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_cross_compat.png
Normal file
|
After Width: | Height: | Size: 534 B |
BIN
app/src/main/res/drawable-xxxhdpi/ic_cross_compat.png
Normal file
|
After Width: | Height: | Size: 462 B |
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
60
app/src/main/res/layout/activity_categories.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||