Compare commits

..

23 Commits

Author SHA1 Message Date
a0aa33a499 Option to clear updates feed 2020-06-29 13:43:01 +03:00
b27bc86141 Fix search history preference 2020-06-29 13:28:25 +03:00
84ef2af82f Fix MangaDetailsPresenter sharing 2020-06-29 13:26:28 +03:00
a2f09d8763 Restore download after network error 2020-06-29 09:48:17 +03:00
79058440a1 Fix grid span count #11 2020-06-25 19:40:50 +03:00
7f9cfdbf7a Show app update in dialog 2020-06-20 12:10:42 +03:00
85f7477450 Cleaning up traks 2020-06-20 11:35:42 +03:00
1b4a65f476 Update pages thumbnails list 2020-06-11 19:59:54 +03:00
2e69395ade Update ui 2020-06-08 19:22:56 +03:00
3f61f13b7b Show related manga 2020-06-07 20:14:44 +03:00
10a0f0ad53 Fix empty tracker log records 2020-06-05 19:25:10 +03:00
680fc66f21 Tracker fixes 2020-06-03 18:52:00 +03:00
e01b74ee3d Pagination loading indicator 2020-05-31 12:10:43 +03:00
3539e6a892 Global search 2020-05-30 11:18:14 +03:00
ff56f5a343 Fix updates feed 2020-05-30 10:25:48 +03:00
9ce43a39c8 Global search 2020-05-30 09:48:04 +03:00
0e3aa3f380 Update readme 2020-05-24 13:25:38 +03:00
7927bf0c9a Refactor 2020-05-24 12:58:17 +03:00
aec2d71688 Manga updates feed 2020-05-22 20:28:14 +03:00
140a0f4d66 Log manga tracking 2020-05-22 19:20:08 +03:00
7cf57535ab Update dependencies 2020-05-21 21:27:21 +03:00
31fe924157 Merge branch 'master' into devel 2020-05-21 21:20:39 +03:00
6444122c0a Update database: add tracklogs table 2020-05-21 20:30:10 +03:00
140 changed files with 1759 additions and 621 deletions

4
.idea/compiler.xml generated
View File

@@ -1,8 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="CompilerConfiguration"> <component name="CompilerConfiguration">
<bytecodeTargetLevel> <bytecodeTargetLevel target="1.8" />
<module name="Kotatsu.app" target="1.8" />
</bytecodeTargetLevel>
</component> </component>
</project> </project>

View File

@@ -3,6 +3,7 @@
<words> <words>
<w>chucker</w> <w>chucker</w>
<w>desu</w> <w>desu</w>
<w>failsafe</w>
<w>koin</w> <w>koin</w>
<w>kotatsu</w> <w>kotatsu</w>
<w>manga</w> <w>manga</w>

2
.idea/misc.xml generated
View File

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

View File

@@ -8,7 +8,7 @@ Kotatsu is a free and open source manga reader for Android.
Latest release: [get here](https://github.com/nv95/Kotatsu/releases/latest) Latest release: [get here](https://github.com/nv95/Kotatsu/releases/latest)
Legacy build (Android 4.1+): [available here](https://github.com/nv95/Kotatsu/releases/tag/v0.3-legacy) Legacy build (Android 4.1+): [available here](https://github.com/nv95/Kotatsu/releases/tag/v0.4-legacy)
### Main Features ### Main Features
@@ -24,6 +24,7 @@ Legacy build (Android 4.1+): [available here](https://github.com/nv95/Kotatsu/re
### Coming Features ### Coming Features
* Updates feed
* Global search * Global search
### Screenshots ### Screenshots

View File

@@ -6,7 +6,6 @@ plugins {
} }
def gitCommits = 'git rev-list --count HEAD'.execute([], rootDir).text.trim().toInteger() def gitCommits = 'git rev-list --count HEAD'.execute([], rootDir).text.trim().toInteger()
def gitBranch = 'git branch --show-current'.execute([], rootDir).text.trim()
android { android {
compileSdkVersion 29 compileSdkVersion 29
@@ -17,9 +16,7 @@ android {
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 29 targetSdkVersion 29
versionCode gitCommits versionCode gitCommits
versionName '0.4' versionName '0.5-b2'
buildConfigField 'String', 'GIT_BRANCH', "\"${gitBranch}\""
kapt { kapt {
arguments { arguments {
@@ -33,6 +30,7 @@ android {
} }
kotlinOptions { kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString() jvmTarget = JavaVersion.VERSION_1_8.toString()
freeCompilerArgs += "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
} }
buildTypes { buildTypes {
debug { debug {
@@ -63,18 +61,17 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6'
implementation 'androidx.core:core-ktx:1.3.0-rc01' implementation 'androidx.core:core-ktx:1.5.0-alpha01'
implementation 'androidx.appcompat:appcompat:1.2.0-rc01' implementation 'androidx.activity:activity-ktx:1.2.0-alpha06'
implementation 'androidx.activity:activity-ktx:1.2.0-alpha04' implementation 'androidx.fragment:fragment-ktx:1.3.0-alpha06'
implementation 'androidx.fragment:fragment-ktx:1.3.0-alpha04' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha05'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha02' implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta7'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta6' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-rc01' implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha04'
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha03'
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01' implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
implementation 'androidx.preference:preference-ktx:1.1.1' implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'androidx.work:work-runtime-ktx:2.3.4' implementation 'androidx.work:work-runtime-ktx:2.4.0-rc01'
implementation 'com.google.android.material:material:1.2.0-alpha06' implementation 'com.google.android.material:material:1.3.0-alpha01'
implementation 'androidx.room:room-runtime:2.2.5' implementation 'androidx.room:room-runtime:2.2.5'
implementation 'androidx.room:room-ktx:2.2.5' implementation 'androidx.room:room-ktx:2.2.5'
@@ -100,5 +97,5 @@ dependencies {
releaseImplementation 'com.github.ChuckerTeam.Chucker:library-no-op:3.2.0' releaseImplementation 'com.github.ChuckerTeam.Chucker:library-no-op:3.2.0'
testImplementation 'junit:junit:4.13' testImplementation 'junit:junit:4.13'
testImplementation 'org.json:json:20190722' testImplementation 'org.json:json:20200518'
} }

View File

@@ -23,7 +23,7 @@
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
tools:ignore="UnusedAttribute"> tools:ignore="UnusedAttribute">
<activity android:name=".ui.main.MainActivity"> <activity android:name=".ui.list.MainActivity">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
@@ -59,7 +59,7 @@
android:theme="@android:style/Theme.DeviceDefault.Dialog" android:theme="@android:style/Theme.DeviceDefault.Dialog"
android:windowSoftInputMode="stateAlwaysHidden" /> android:windowSoftInputMode="stateAlwaysHidden" />
<activity <activity
android:name=".ui.main.list.favourites.categories.CategoriesActivity" android:name="org.koitharu.kotatsu.ui.list.favourites.categories.CategoriesActivity"
android:label="@string/favourites_categories" android:label="@string/favourites_categories"
android:windowSoftInputMode="stateAlwaysHidden" /> android:windowSoftInputMode="stateAlwaysHidden" />
<activity <activity
@@ -69,6 +69,8 @@
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" /> <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".ui.search.global.GlobalSearchActivity"
android:label="@string/search" />
<service <service
android:name=".ui.download.DownloadService" android:name=".ui.download.DownloadService"

View File

@@ -3,6 +3,8 @@ package org.koitharu.kotatsu
import android.app.Application import android.app.Application
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.room.Room import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
import coil.Coil import coil.Coil
import coil.ComponentRegistry import coil.ComponentRegistry
import coil.ImageLoaderBuilder import coil.ImageLoaderBuilder
@@ -16,10 +18,7 @@ import org.koin.core.context.startKoin
import org.koin.dsl.module import org.koin.dsl.module
import org.koitharu.kotatsu.core.db.DatabasePrePopulateCallback import org.koitharu.kotatsu.core.db.DatabasePrePopulateCallback
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.migrations.Migration1To2 import org.koitharu.kotatsu.core.db.migrations.*
import org.koitharu.kotatsu.core.db.migrations.Migration2To3
import org.koitharu.kotatsu.core.db.migrations.Migration3To4
import org.koitharu.kotatsu.core.db.migrations.Migration4To5
import org.koitharu.kotatsu.core.local.CbzFetcher import org.koitharu.kotatsu.core.local.CbzFetcher
import org.koitharu.kotatsu.core.local.PagesCache import org.koitharu.kotatsu.core.local.PagesCache
import org.koitharu.kotatsu.core.local.cookies.PersistentCookieJar import org.koitharu.kotatsu.core.local.cookies.PersistentCookieJar
@@ -126,6 +125,6 @@ class KotatsuApp : Application() {
applicationContext, applicationContext,
MangaDatabase::class.java, MangaDatabase::class.java,
"kotatsu-db" "kotatsu-db"
).addMigrations(Migration1To2, Migration2To3, Migration3To4, Migration4To5) ).addMigrations(Migration1To2, Migration2To3, Migration3To4, Migration4To5, Migration5To6)
.addCallback(DatabasePrePopulateCallback(resources)) .addCallback(DatabasePrePopulateCallback(resources))
} }

View File

@@ -7,8 +7,9 @@ import org.koitharu.kotatsu.core.db.entity.*
@Database( @Database(
entities = [ entities = [
MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class, MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class,
FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class, TrackEntity::class FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class,
], version = 5 TrackEntity::class, TrackLogEntity::class
], version = 6
) )
abstract class MangaDatabase : RoomDatabase() { abstract class MangaDatabase : RoomDatabase() {
@@ -25,4 +26,6 @@ abstract class MangaDatabase : RoomDatabase() {
abstract val favouriteCategoriesDao: FavouriteCategoriesDao abstract val favouriteCategoriesDao: FavouriteCategoriesDao
abstract val tracksDao: TracksDao abstract val tracksDao: TracksDao
abstract val trackLogsDao: TrackLogsDao
} }

View File

@@ -0,0 +1,28 @@
package org.koitharu.kotatsu.core.db
import androidx.room.*
import org.koitharu.kotatsu.core.db.entity.TrackLogEntity
import org.koitharu.kotatsu.core.db.entity.TrackLogWithManga
@Dao
interface TrackLogsDao {
@Transaction
@Query("SELECT * FROM track_logs ORDER BY created_at DESC LIMIT :limit OFFSET :offset")
suspend fun findAll(offset: Int, limit: Int): List<TrackLogWithManga>
@Query("DELETE FROM track_logs")
suspend fun clear()
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(entity: TrackLogEntity): Long
@Query("DELETE FROM track_logs WHERE manga_id = :mangaId")
suspend fun removeAll(mangaId: Long)
@Query("DELETE FROM track_logs WHERE manga_id NOT IN (SELECT manga_id FROM tracks)")
suspend fun cleanup()
@Query("SELECT COUNT(*) FROM track_logs")
suspend fun count(): Int
}

View File

@@ -25,11 +25,13 @@ abstract class TracksDao {
@Query("DELETE FROM tracks WHERE manga_id = :mangaId") @Query("DELETE FROM tracks WHERE manga_id = :mangaId")
abstract suspend fun delete(mangaId: Long) abstract suspend fun delete(mangaId: Long)
@Query("DELETE FROM tracks WHERE manga_id NOT IN (SELECT manga_id FROM history UNION SELECT manga_id FROM favourites)")
abstract suspend fun cleanup()
@Transaction @Transaction
open suspend fun upsert(entity: TrackEntity) { open suspend fun upsert(entity: TrackEntity) {
if (update(entity) == 0) { if (update(entity) == 0) {
insert(entity) insert(entity)
} }
} }
} }

View File

@@ -0,0 +1,24 @@
package org.koitharu.kotatsu.core.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@Entity(
tableName = "track_logs", foreignKeys = [
ForeignKey(
entity = MangaEntity::class,
parentColumns = ["manga_id"],
childColumns = ["manga_id"],
onDelete = ForeignKey.CASCADE
)
]
)
data class TrackLogEntity(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id") val id: Long = 0L,
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
@ColumnInfo(name = "chapters") val chapters: String,
@ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis()
)

View File

@@ -0,0 +1,30 @@
package org.koitharu.kotatsu.core.db.entity
import androidx.room.Embedded
import androidx.room.Junction
import androidx.room.Relation
import org.koitharu.kotatsu.core.model.TrackingLogItem
import java.util.*
data class TrackLogWithManga(
@Embedded val trackLog: TrackLogEntity,
@Relation(
parentColumn = "manga_id",
entityColumn = "manga_id"
)
val manga: MangaEntity,
@Relation(
parentColumn = "manga_id",
entityColumn = "tag_id",
associateBy = Junction(MangaTagsEntity::class)
)
val tags: List<TagEntity>
) {
fun toTrackingLogItem() = TrackingLogItem(
id = trackLog.id,
chapters = trackLog.chapters.split('\n').filterNot { x -> x.isEmpty() },
manga = manga.toManga(tags.map { x -> x.toMangaTag() }.toSet()),
createdAt = Date(trackLog.createdAt)
)
}

View File

@@ -0,0 +1,12 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
object Migration5To6 : Migration(5, 6) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS track_logs (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, manga_id INTEGER NOT NULL, chapters TEXT NOT NULL, created_at INTEGER NOT NULL, FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE)")
database.execSQL("CREATE INDEX IF NOT EXISTS index_track_logs_manga_id ON track_logs (manga_id)")
}
}

View File

@@ -9,5 +9,6 @@ data class AppVersion(
val name: String, val name: String,
val url: String, val url: String,
val apkSize: Long, val apkSize: Long,
val apkUrl: String val apkUrl: String,
val description: String
) : Parcelable ) : Parcelable

View File

@@ -22,7 +22,8 @@ class GithubRepository : KoinComponent {
url = json.getString("html_url"), url = json.getString("html_url"),
name = json.getString("name").removePrefix("v"), name = json.getString("name").removePrefix("v"),
apkSize = asset.getLong("size"), apkSize = asset.getLong("size"),
apkUrl = asset.getString("browser_download_url") apkUrl = asset.getString("browser_download_url"),
description = json.getString("body")
) )
} }
} }

View File

@@ -0,0 +1,13 @@
package org.koitharu.kotatsu.core.model
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
import java.util.*
@Parcelize
data class TrackingLogItem (
val id: Long,
val manga: Manga,
val chapters: List<String>,
val createdAt: Date
): Parcelable

View File

@@ -72,7 +72,6 @@ class LocalMangaRepository : MangaRepository, KoinComponent {
} }
} }
fun delete(manga: Manga): Boolean { fun delete(manga: Manga): Boolean {
val file = Uri.parse(manga.url).toFile() val file = Uri.parse(manga.url).toFile()
return file.delete() return file.delete()

View File

@@ -113,7 +113,7 @@ abstract class GroupleRepository(loaderContext: MangaLoaderContext) :
a.attr("href")?.withDomain(domain) ?: return@mapIndexedNotNull null a.attr("href")?.withDomain(domain) ?: return@mapIndexedNotNull null
MangaChapter( MangaChapter(
id = href.longHashCode(), id = href.longHashCode(),
name = a.ownText(), name = a.ownText().removePrefix(manga.title).trim(),
number = i + 1, number = i + 1,
url = href, url = href,
source = source source = source

View File

@@ -2,5 +2,5 @@ package org.koitharu.kotatsu.core.prefs
enum class AppSection { enum class AppSection {
LOCAL, FAVOURITES, HISTORY; LOCAL, FAVOURITES, HISTORY, FEED
} }

View File

@@ -13,7 +13,8 @@ import java.util.*
object MangaProviderFactory : KoinComponent { object MangaProviderFactory : KoinComponent {
private val loaderContext by inject<MangaLoaderContext>() private val loaderContext by inject<MangaLoaderContext>()
private val cache = EnumMap<MangaSource, WeakReference<MangaRepository>>(MangaSource::class.java) private val cache =
EnumMap<MangaSource, WeakReference<MangaRepository>>(MangaSource::class.java)
fun getSources(includeHidden: Boolean): List<MangaSource> { fun getSources(includeHidden: Boolean): List<MangaSource> {
val settings = get<AppSettings>() val settings = get<AppSettings>()
@@ -33,24 +34,37 @@ object MangaProviderFactory : KoinComponent {
} }
} }
fun createLocal(): LocalMangaRepository = fun createLocal(): LocalMangaRepository {
(cache[MangaSource.LOCAL]?.get() as? LocalMangaRepository) var instance = cache[MangaSource.LOCAL]?.get()
?: LocalMangaRepository().also { if (instance == null) {
cache[MangaSource.LOCAL] = WeakReference<MangaRepository>(it) synchronized(cache) {
instance = cache[MangaSource.LOCAL]?.get()
if (instance == null) {
instance = LocalMangaRepository()
cache[MangaSource.LOCAL] = WeakReference<MangaRepository>(instance)
}
} }
}
return instance as LocalMangaRepository
}
@Throws(Throwable::class) @Throws(Throwable::class)
fun create(source: MangaSource): MangaRepository { fun create(source: MangaSource): MangaRepository {
cache[source]?.get()?.let { var instance = cache[source]?.get()
return it if (instance == null) {
synchronized(cache) {
instance = cache[source]?.get()
if (instance == null) {
instance = try {
source.cls.getDeclaredConstructor(MangaLoaderContext::class.java)
.newInstance(loaderContext)
} catch (e: NoSuchMethodException) {
source.cls.newInstance()
}
cache[source] = WeakReference(instance!!)
}
}
} }
val instance = try { return instance!!
source.cls.getDeclaredConstructor(MangaLoaderContext::class.java)
.newInstance(loaderContext)
} catch (e: NoSuchMethodException) {
source.cls.newInstance()
}
cache[source] = WeakReference<MangaRepository>(instance)
return instance
} }
} }

View File

@@ -0,0 +1,34 @@
package org.koitharu.kotatsu.domain
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import org.koin.core.KoinComponent
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.SortOrder
import java.util.*
class MangaSearchRepository : KoinComponent {
fun globalSearch(query: String, batchSize: Int = 4): Flow<List<Manga>> = flow {
val sources = MangaProviderFactory.getSources(false)
val lists = EnumMap<MangaSource, List<Manga>>(MangaSource::class.java)
var i = 0
while (true) {
var isEmitted = false
for (source in sources) {
val list = lists.getOrPut(source) {
MangaProviderFactory.create(source).getList(0, query, SortOrder.POPULARITY)
}
if (i < list.size) {
emit(list.subList(i, (i + batchSize).coerceAtMost(list.lastIndex)))
isEmitted = true
}
}
i += batchSize
if (!isEmitted) {
return@flow
}
}
}
}

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.domain
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.util.Size import android.util.Size
import androidx.annotation.WorkerThread
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import org.koin.core.KoinComponent import org.koin.core.KoinComponent
@@ -21,6 +22,8 @@ object MangaUtils : KoinComponent {
* Automatic determine type of manga by page size * Automatic determine type of manga by page size
* @return ReaderMode.WEBTOON if page is wide * @return ReaderMode.WEBTOON if page is wide
*/ */
@WorkerThread
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun determineReaderMode(pages: List<MangaPage>): ReaderMode? { suspend fun determineReaderMode(pages: List<MangaPage>): ReaderMode? {
try { try {
val page = pages.medianOrNull() ?: return null val page = pages.medianOrNull() ?: return null

View File

@@ -1,11 +1,15 @@
package org.koitharu.kotatsu.domain.tracking package org.koitharu.kotatsu.domain.tracking
import androidx.room.withTransaction
import org.koin.core.KoinComponent import org.koin.core.KoinComponent
import org.koin.core.inject import org.koin.core.inject
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.TrackEntity import org.koitharu.kotatsu.core.db.entity.TrackEntity
import org.koitharu.kotatsu.core.db.entity.TrackLogEntity
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.core.model.MangaTracking import org.koitharu.kotatsu.core.model.MangaTracking
import org.koitharu.kotatsu.core.model.TrackingLogItem
import java.util.* import java.util.*
class TrackingRepository : KoinComponent { class TrackingRepository : KoinComponent {
@@ -34,22 +38,50 @@ class TrackingRepository : KoinComponent {
} }
} }
suspend fun getTrackingLog(offset: Int, limit: Int): List<TrackingLogItem> {
return db.trackLogsDao.findAll(offset, limit).map { x ->
x.toTrackingLogItem()
}
}
suspend fun count() = db.trackLogsDao.count()
suspend fun clearLogs() = db.trackLogsDao.clear()
suspend fun cleanup() {
db.withTransaction {
db.tracksDao.cleanup()
db.trackLogsDao.cleanup()
}
}
suspend fun storeTrackResult( suspend fun storeTrackResult(
mangaId: Long, mangaId: Long,
knownChaptersCount: Int, knownChaptersCount: Int,
lastChapterId: Long, lastChapterId: Long,
newChapters: Int, newChapters: List<MangaChapter>,
lastNotifiedChapterId: Long previousTrackChapterId: Long
) { ) {
val entity = TrackEntity( db.withTransaction {
mangaId = mangaId, val entity = TrackEntity(
newChapters = newChapters, mangaId = mangaId,
lastCheck = System.currentTimeMillis(), newChapters = newChapters.size,
lastChapterId = lastChapterId, lastCheck = System.currentTimeMillis(),
totalChapters = knownChaptersCount, lastChapterId = lastChapterId,
lastNotifiedChapterId = lastNotifiedChapterId totalChapters = knownChaptersCount,
) lastNotifiedChapterId = newChapters.lastOrNull()?.id ?: previousTrackChapterId
db.tracksDao.upsert(entity) )
db.tracksDao.upsert(entity)
val foundChapters = newChapters.takeLastWhile { x -> x.id != previousTrackChapterId }
if (foundChapters.isNotEmpty()) {
val logEntity = TrackLogEntity(
mangaId = mangaId,
chapters = foundChapters.joinToString("\n") { x -> x.name },
createdAt = System.currentTimeMillis()
)
db.trackLogsDao.insert(logEntity)
}
}
} }
suspend fun upsert(manga: Manga) { suspend fun upsert(manga: Manga) {

View File

@@ -29,7 +29,7 @@ class BrowserActivity : BaseActivity(), BrowserCallback {
webView.webViewClient = BrowserClient(this) webView.webViewClient = BrowserClient(this)
val url = intent?.dataString val url = intent?.dataString
if (url.isNullOrEmpty()) { if (url.isNullOrEmpty()) {
finish() finishAfterTransition()
} else { } else {
webView.loadUrl(url) webView.loadUrl(url)
} }
@@ -43,7 +43,7 @@ class BrowserActivity : BaseActivity(), BrowserCallback {
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
android.R.id.home -> { android.R.id.home -> {
webView.stopLoading() webView.stopLoading()
finish() finishAfterTransition()
true true
} }
R.id.action_browser -> { R.id.action_browser -> {

View File

@@ -5,6 +5,7 @@ import android.os.Bundle
import android.view.View import android.view.View
import androidx.annotation.LayoutRes import androidx.annotation.LayoutRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import moxy.MvpAppCompatDialogFragment import moxy.MvpAppCompatDialogFragment
abstract class AlertDialogFragment(@LayoutRes private val layoutResId: Int) : MvpAppCompatDialogFragment() { abstract class AlertDialogFragment(@LayoutRes private val layoutResId: Int) : MvpAppCompatDialogFragment() {
@@ -17,7 +18,7 @@ abstract class AlertDialogFragment(@LayoutRes private val layoutResId: Int) : Mv
if (view != null) { if (view != null) {
onViewCreated(view, savedInstanceState) onViewCreated(view, savedInstanceState)
} }
return AlertDialog.Builder(requireContext(), theme) return MaterialAlertDialogBuilder(requireContext(), theme)
.setView(view) .setView(view)
.also(::onBuildDialog) .also(::onBuildDialog)
.create() .create()

View File

@@ -1,12 +1,10 @@
package org.koitharu.kotatsu.ui.common package org.koitharu.kotatsu.ui.common
import android.view.KeyEvent
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import moxy.MvpAppCompatActivity import moxy.MvpAppCompatActivity
import org.koin.core.KoinComponent import org.koin.core.KoinComponent
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
abstract class BaseActivity : MvpAppCompatActivity(), KoinComponent { abstract class BaseActivity : MvpAppCompatActivity(), KoinComponent {
@@ -29,13 +27,4 @@ abstract class BaseActivity : MvpAppCompatActivity(), KoinComponent {
onBackPressed() onBackPressed()
true true
} else super.onOptionsItemSelected(item) } else super.onOptionsItemSelected(item)
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
//TODO remove. Just for testing
if (BuildConfig.DEBUG && keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
recreate()
return true
}
return super.onKeyDown(keyCode, event)
}
} }

View File

@@ -0,0 +1,25 @@
package org.koitharu.kotatsu.ui.common
import android.util.ArrayMap
import moxy.MvpPresenter
import java.lang.ref.WeakReference
abstract class SharedPresenterHolder<T : MvpPresenter<*>> {
private val cache = ArrayMap<Int, WeakReference<T>>(3)
fun getInstance(key: Int): T {
var instance = cache[key]?.get()
if (instance == null) {
instance = onCreatePresenter(key)
cache[key] = WeakReference(instance)
}
return instance
}
fun clear(key: Int) {
cache.remove(key)
}
protected abstract fun onCreatePresenter(key: Int): T
}

View File

@@ -8,6 +8,7 @@ import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.google.android.material.checkbox.MaterialCheckBox import com.google.android.material.checkbox.MaterialCheckBox
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
class CheckBoxAlertDialog private constructor(private val delegate: AlertDialog) : class CheckBoxAlertDialog private constructor(private val delegate: AlertDialog) :
@@ -22,7 +23,7 @@ class CheckBoxAlertDialog private constructor(private val delegate: AlertDialog)
.inflate(R.layout.dialog_checkbox, null, false) .inflate(R.layout.dialog_checkbox, null, false)
private val checkBox = view.findViewById<MaterialCheckBox>(android.R.id.checkbox) private val checkBox = view.findViewById<MaterialCheckBox>(android.R.id.checkbox)
private val delegate = AlertDialog.Builder(context) private val delegate = MaterialAlertDialogBuilder(context)
.setView(view) .setView(view)
fun setTitle(@StringRes titleResId: Int): Builder { fun setTitle(@StringRes titleResId: Int): Builder {

View File

@@ -7,6 +7,7 @@ import android.view.ViewGroup
import android.widget.BaseAdapter import android.widget.BaseAdapter
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.android.synthetic.main.item_storage.view.* import kotlinx.android.synthetic.main.item_storage.view.*
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.LocalMangaRepository import org.koitharu.kotatsu.core.parser.LocalMangaRepository
@@ -23,7 +24,7 @@ class StorageSelectDialog private constructor(private val delegate: AlertDialog)
class Builder(context: Context, defaultValue: File?, listener: OnStorageSelectListener) { class Builder(context: Context, defaultValue: File?, listener: OnStorageSelectListener) {
private val adapter = VolumesAdapter(context) private val adapter = VolumesAdapter(context)
private val delegate = AlertDialog.Builder(context) private val delegate = MaterialAlertDialogBuilder(context)
init { init {
if (adapter.isEmpty) { if (adapter.isEmpty) {

View File

@@ -7,6 +7,7 @@ import android.text.InputFilter
import android.view.LayoutInflater import android.view.LayoutInflater
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.android.synthetic.main.dialog_input.view.* import kotlinx.android.synthetic.main.dialog_input.view.*
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
@@ -20,7 +21,7 @@ class TextInputDialog private constructor(private val delegate: AlertDialog) :
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
private val view = LayoutInflater.from(context).inflate(R.layout.dialog_input, null, false) private val view = LayoutInflater.from(context).inflate(R.layout.dialog_input, null, false)
private val delegate = AlertDialog.Builder(context) private val delegate = MaterialAlertDialogBuilder(context)
.setView(view) .setView(view)
fun setTitle(@StringRes titleResId: Int): Builder { fun setTitle(@StringRes titleResId: Int): Builder {

View File

@@ -87,16 +87,23 @@ abstract class BaseRecyclerAdapter<T, E>(private val onItemClickListener: OnRecy
final override fun getItemCount() = dataSet.size final override fun getItemCount() = dataSet.size
final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<T, E> { final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<T, E> {
return onCreateViewHolder(parent).setOnItemClickListener(onItemClickListener) return onCreateViewHolder(parent)
.also(this::onViewHolderCreated) }
override fun onViewDetachedFromWindow(holder: BaseViewHolder<T, E>) {
holder.setOnItemClickListener(null)
super.onViewDetachedFromWindow(holder)
}
override fun onViewAttachedToWindow(holder: BaseViewHolder<T, E>) {
super.onViewAttachedToWindow(holder)
holder.setOnItemClickListener(onItemClickListener)
} }
protected open fun onDataSetChanged() = Unit protected open fun onDataSetChanged() = Unit
protected abstract fun getExtra(item: T, position: Int): E protected abstract fun getExtra(item: T, position: Int): E
protected open fun onViewHolderCreated(holder: BaseViewHolder<T, E>) = Unit
protected abstract fun onCreateViewHolder(parent: ViewGroup): BaseViewHolder<T, E> protected abstract fun onCreateViewHolder(parent: ViewGroup): BaseViewHolder<T, E>
protected abstract fun onGetItemId(item: T): Long protected abstract fun onGetItemId(item: T): Long

View File

@@ -1,6 +1,5 @@
package org.koitharu.kotatsu.ui.common.list package org.koitharu.kotatsu.ui.common.list
import android.os.Build
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.LayoutRes import androidx.annotation.LayoutRes
@@ -27,26 +26,29 @@ abstract class BaseViewHolder<T, E> protected constructor(view: View) :
onBind(data, extra) onBind(data, extra)
} }
fun requireData() = boundData ?: throw IllegalStateException("Calling requireData() before bind()") fun requireData(): T {
return boundData ?: throw IllegalStateException("Calling requireData() before bind()")
}
fun setOnItemClickListener(listener: OnRecyclerItemClickListener<T>?): BaseViewHolder<T, E> { fun setOnItemClickListener(listener: OnRecyclerItemClickListener<T>?) {
if (listener != null) { val listenersAdapter = listener?.let { HolderListenersAdapter(it) }
itemView.setOnClickListener { itemView.setOnClickListener(listenersAdapter)
listener.onItemClick(boundData ?: return@setOnClickListener, bindingAdapterPosition, it) itemView.setOnLongClickListener(listenersAdapter)
}
itemView.setOnLongClickListener {
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
} }
open fun onRecycled() = Unit open fun onRecycled() = Unit
abstract fun onBind(data: T, extra: E) abstract fun onBind(data: T, extra: E)
private inner class HolderListenersAdapter(private val listener: OnRecyclerItemClickListener<T>) :
View.OnClickListener, View.OnLongClickListener {
override fun onClick(v: View) {
listener.onItemClick(boundData ?: return, bindingAdapterPosition, v)
}
override fun onLongClick(v: View): Boolean {
return listener.onItemLongClick(boundData ?: return false, bindingAdapterPosition, v)
}
}
} }

View File

@@ -12,13 +12,15 @@ abstract class BoundsScrollListener(private val offsetTop: Int, private val offs
super.onScrolled(recyclerView, dx, dy) super.onScrolled(recyclerView, dx, dy)
val layoutManager = (recyclerView.layoutManager as? LinearLayoutManager) ?: return val layoutManager = (recyclerView.layoutManager as? LinearLayoutManager) ?: return
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
if (firstVisibleItemPosition == RecyclerView.NO_POSITION) {
return
}
if (firstVisibleItemPosition <= offsetTop) { if (firstVisibleItemPosition <= offsetTop) {
onScrolledToStart(recyclerView) onScrolledToStart(recyclerView)
return
} }
val visibleItemCount = layoutManager.childCount val visibleItemCount = layoutManager.childCount
val totalItemCount = layoutManager.itemCount val totalItemCount = layoutManager.itemCount
if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - offsetBottom && firstVisibleItemPosition >= 0) { if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - offsetBottom) {
onScrolledToEnd(recyclerView) onScrolledToEnd(recyclerView)
} }
} }

View File

@@ -2,24 +2,31 @@ package org.koitharu.kotatsu.ui.common.list
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
class PaginationScrollListener(offset: Int, private val callback: Callback) : BoundsScrollListener(0, offset) { class PaginationScrollListener(offset: Int, private val callback: Callback) :
BoundsScrollListener(0, offset) {
private var lastTotalCount = 0 private var lastTotalCount = 0
override fun onScrolledToStart(recyclerView: RecyclerView) = Unit override fun onScrolledToStart(recyclerView: RecyclerView) = Unit
override fun onScrolledToEnd(recyclerView: RecyclerView) { override fun onScrolledToEnd(recyclerView: RecyclerView) {
val total = recyclerView.adapter?.itemCount ?: 0 val total = callback.getItemsCount()
if (total > lastTotalCount) { if (total > lastTotalCount) {
callback.onRequestMoreItems(total)
lastTotalCount = total lastTotalCount = total
callback.onRequestMoreItems(total)
} else if (total < lastTotalCount) { } else if (total < lastTotalCount) {
lastTotalCount = total lastTotalCount = total
} }
} }
fun reset() {
lastTotalCount = 0
}
interface Callback { interface Callback {
fun onRequestMoreItems(offset: Int) fun onRequestMoreItems(offset: Int)
fun getItemsCount(): Int
} }
} }

View File

@@ -0,0 +1,24 @@
package org.koitharu.kotatsu.ui.common.list
import android.view.ViewGroup
class ProgressBarAdapter : BaseRecyclerAdapter<Boolean, Unit>() {
var isProgressVisible: Boolean
get() = dataSet.isNotEmpty()
set(value) {
if (value == dataSet.isEmpty()) {
if (value) {
appendItem(true)
} else {
removeItemAt(0)
}
}
}
override fun getExtra(item: Boolean, position: Int) = Unit
override fun onCreateViewHolder(parent: ViewGroup) = ProgressBarHolder(parent)
override fun onGetItemId(item: Boolean) = -1L
}

View File

@@ -0,0 +1,34 @@
package org.koitharu.kotatsu.ui.common.list
import android.view.View
import android.view.ViewGroup
import kotlinx.android.synthetic.main.item_progress.*
import org.koitharu.kotatsu.R
class ProgressBarHolder(parent: ViewGroup) :
BaseViewHolder<Boolean, Unit>(parent, R.layout.item_progress) {
private var pendingVisibility: Int = View.GONE
private val action = Runnable {
progressBar?.visibility = pendingVisibility
pendingVisibility = View.GONE
}
override fun onBind(data: Boolean, extra: Unit) {
val visibility = if (data) {
View.VISIBLE
} else {
View.INVISIBLE
}
if (visibility != progressBar.visibility && visibility != pendingVisibility) {
progressBar.removeCallbacks(action)
pendingVisibility = visibility
progressBar.postDelayed(action, 400)
}
}
override fun onRecycled() {
progressBar.removeCallbacks(action)
super.onRecycled()
}
}

View File

@@ -35,7 +35,7 @@ class ChapterHolder(parent: ViewGroup) :
textView_number.setTextColor(context.getThemeColor(android.R.attr.textColorPrimaryInverse)) textView_number.setTextColor(context.getThemeColor(android.R.attr.textColorPrimaryInverse))
} }
ChapterExtra.CHECKED -> { ChapterExtra.CHECKED -> {
textView_number.setBackgroundResource(R.drawable.bg_badge_accent) textView_number.background = null
textView_number.setTextColor(Color.TRANSPARENT) textView_number.setTextColor(Color.TRANSPARENT)
} }
} }

View File

@@ -25,7 +25,9 @@ class ChaptersFragment : BaseFragment(R.layout.fragment_chapters), MangaDetailsV
OnRecyclerItemClickListener<MangaChapter>, ActionMode.Callback { OnRecyclerItemClickListener<MangaChapter>, ActionMode.Callback {
@Suppress("unused") @Suppress("unused")
private val presenter by moxyPresenter(factory = MangaDetailsPresenter.Companion::getInstance) private val presenter by moxyPresenter {
MangaDetailsPresenter.getInstance(activity.hashCode())
}
private var manga: Manga? = null private var manga: Manga? = null

View File

@@ -7,11 +7,12 @@ import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.core.content.ContextCompat
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.net.toFile import androidx.core.net.toFile
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
@@ -31,11 +32,14 @@ import org.koitharu.kotatsu.ui.download.DownloadService
import org.koitharu.kotatsu.utils.MangaShortcut import org.koitharu.kotatsu.utils.MangaShortcut
import org.koitharu.kotatsu.utils.ShareHelper import org.koitharu.kotatsu.utils.ShareHelper
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.getThemeColor
class MangaDetailsActivity : BaseActivity(), MangaDetailsView, class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
TabLayoutMediator.TabConfigurationStrategy { TabLayoutMediator.TabConfigurationStrategy {
private val presenter by moxyPresenter(factory = MangaDetailsPresenter.Companion::getInstance) private val presenter by moxyPresenter {
MangaDetailsPresenter.getInstance(hashCode())
}
private var manga: Manga? = null private var manga: Manga? = null
@@ -50,7 +54,7 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
presenter.loadDetails(it, true) presenter.loadDetails(it, true)
} ?: intent?.getLongExtra(EXTRA_MANGA_ID, 0)?.takeUnless { it == 0L }?.let { } ?: intent?.getLongExtra(EXTRA_MANGA_ID, 0)?.takeUnless { it == 0L }?.let {
presenter.findMangaById(it) presenter.findMangaById(it)
} ?: finish() } ?: finishAfterTransition()
} }
} }
@@ -71,13 +75,13 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
this, getString(R.string._s_deleted_from_local_storage, manga.title), this, getString(R.string._s_deleted_from_local_storage, manga.title),
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
finish() finishAfterTransition()
} }
override fun onError(e: Throwable) { override fun onError(e: Throwable) {
if (manga == null) { if (manga == null) {
Toast.makeText(this, e.getDisplayMessage(resources), Toast.LENGTH_LONG).show() Toast.makeText(this, e.getDisplayMessage(resources), Toast.LENGTH_LONG).show()
finish() finishAfterTransition()
} else { } else {
Snackbar.make(pager, e.getDisplayMessage(resources), Snackbar.LENGTH_LONG).show() Snackbar.make(pager, e.getDisplayMessage(resources), Snackbar.LENGTH_LONG).show()
} }
@@ -124,7 +128,7 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
} }
R.id.action_delete -> { R.id.action_delete -> {
manga?.let { m -> manga?.let { m ->
AlertDialog.Builder(this) MaterialAlertDialogBuilder(this)
.setTitle(R.string.delete_manga) .setTitle(R.string.delete_manga)
.setMessage(getString(R.string.text_delete_local_manga, m.title)) .setMessage(getString(R.string.text_delete_local_manga, m.title))
.setPositiveButton(R.string.delete) { _, _ -> .setPositiveButton(R.string.delete) { _, _ ->
@@ -139,7 +143,7 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
manga?.let { manga?.let {
val chaptersCount = it.chapters?.size ?: 0 val chaptersCount = it.chapters?.size ?: 0
if (chaptersCount > 5) { if (chaptersCount > 5) {
AlertDialog.Builder(this) MaterialAlertDialogBuilder(this)
.setTitle(R.string.save_manga) .setTitle(R.string.save_manga)
.setMessage( .setMessage(
getString( getString(
@@ -188,6 +192,7 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
tab.text = when (position) { tab.text = when (position) {
0 -> getString(R.string.details) 0 -> getString(R.string.details)
1 -> getString(R.string.chapters) 1 -> getString(R.string.chapters)
2 -> getString(R.string.related)
else -> null else -> null
} }
} }
@@ -195,11 +200,13 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
override fun onSupportActionModeStarted(mode: ActionMode) { override fun onSupportActionModeStarted(mode: ActionMode) {
super.onSupportActionModeStarted(mode) super.onSupportActionModeStarted(mode)
pager.isUserInputEnabled = false pager.isUserInputEnabled = false
window?.statusBarColor = ContextCompat.getColor(this, R.color.grey_dark)
} }
override fun onSupportActionModeFinished(mode: ActionMode) { override fun onSupportActionModeFinished(mode: ActionMode) {
super.onSupportActionModeFinished(mode) super.onSupportActionModeFinished(mode)
pager.isUserInputEnabled = true pager.isUserInputEnabled = true
window?.statusBarColor = getThemeColor(R.attr.colorPrimaryDark)
} }
companion object { companion object {

View File

@@ -6,11 +6,12 @@ import androidx.viewpager2.adapter.FragmentStateAdapter
class MangaDetailsAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) { class MangaDetailsAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
override fun getItemCount() = 2 override fun getItemCount() = 3
override fun createFragment(position: Int): Fragment = when(position) { override fun createFragment(position: Int): Fragment = when(position) {
0 -> MangaDetailsFragment() 0 -> MangaDetailsFragment()
1 -> ChaptersFragment() 1 -> ChaptersFragment()
2 -> RelatedMangaFragment()
else -> throw IndexOutOfBoundsException("No fragment for position $position") else -> throw IndexOutOfBoundsException("No fragment for position $position")
} }
} }

View File

@@ -14,7 +14,7 @@ import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaHistory import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.ui.common.BaseFragment import org.koitharu.kotatsu.ui.common.BaseFragment
import org.koitharu.kotatsu.ui.main.list.favourites.categories.select.FavouriteCategoriesDialog import org.koitharu.kotatsu.ui.list.favourites.categories.select.FavouriteCategoriesDialog
import org.koitharu.kotatsu.ui.reader.ReaderActivity import org.koitharu.kotatsu.ui.reader.ReaderActivity
import org.koitharu.kotatsu.ui.search.MangaSearchSheet import org.koitharu.kotatsu.ui.search.MangaSearchSheet
import org.koitharu.kotatsu.utils.FileSizeUtils import org.koitharu.kotatsu.utils.FileSizeUtils
@@ -29,7 +29,9 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
View.OnLongClickListener { View.OnLongClickListener {
@Suppress("unused") @Suppress("unused")
private val presenter by moxyPresenter(factory = MangaDetailsPresenter.Companion::getInstance) private val presenter by moxyPresenter {
MangaDetailsPresenter.getInstance(activity.hashCode())
}
private var manga: Manga? = null private var manga: Manga? = null
private var history: MangaHistory? = null private var history: MangaHistory? = null

View File

@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.ui.details
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import moxy.InjectViewState import moxy.InjectViewState
@@ -13,23 +14,25 @@ import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.parser.LocalMangaRepository import org.koitharu.kotatsu.core.parser.LocalMangaRepository
import org.koitharu.kotatsu.domain.MangaDataRepository import org.koitharu.kotatsu.domain.MangaDataRepository
import org.koitharu.kotatsu.domain.MangaProviderFactory import org.koitharu.kotatsu.domain.MangaProviderFactory
import org.koitharu.kotatsu.domain.MangaSearchRepository
import org.koitharu.kotatsu.domain.favourites.FavouritesRepository import org.koitharu.kotatsu.domain.favourites.FavouritesRepository
import org.koitharu.kotatsu.domain.favourites.OnFavouritesChangeListener import org.koitharu.kotatsu.domain.favourites.OnFavouritesChangeListener
import org.koitharu.kotatsu.domain.history.HistoryRepository import org.koitharu.kotatsu.domain.history.HistoryRepository
import org.koitharu.kotatsu.domain.history.OnHistoryChangeListener import org.koitharu.kotatsu.domain.history.OnHistoryChangeListener
import org.koitharu.kotatsu.domain.tracking.TrackingRepository import org.koitharu.kotatsu.domain.tracking.TrackingRepository
import org.koitharu.kotatsu.ui.common.BasePresenter import org.koitharu.kotatsu.ui.common.BasePresenter
import org.koitharu.kotatsu.ui.common.SharedPresenterHolder
import org.koitharu.kotatsu.utils.ext.safe import org.koitharu.kotatsu.utils.ext.safe
import java.io.IOException import java.io.IOException
@InjectViewState @InjectViewState
class MangaDetailsPresenter private constructor() : BasePresenter<MangaDetailsView>(), class MangaDetailsPresenter private constructor(private val key: Int) :
OnHistoryChangeListener, BasePresenter<MangaDetailsView>(), OnHistoryChangeListener, OnFavouritesChangeListener {
OnFavouritesChangeListener {
private lateinit var historyRepository: HistoryRepository private lateinit var historyRepository: HistoryRepository
private lateinit var favouritesRepository: FavouritesRepository private lateinit var favouritesRepository: FavouritesRepository
private lateinit var trackingRepository: TrackingRepository private lateinit var trackingRepository: TrackingRepository
private lateinit var searchRepository: MangaSearchRepository
private var manga: Manga? = null private var manga: Manga? = null
@@ -37,6 +40,7 @@ class MangaDetailsPresenter private constructor() : BasePresenter<MangaDetailsVi
historyRepository = HistoryRepository() historyRepository = HistoryRepository()
favouritesRepository = FavouritesRepository() favouritesRepository = FavouritesRepository()
trackingRepository = TrackingRepository() trackingRepository = TrackingRepository()
searchRepository = MangaSearchRepository()
super.onFirstViewAttach() super.onFirstViewAttach()
HistoryRepository.subscribe(this) HistoryRepository.subscribe(this)
FavouritesRepository.subscribe(this) FavouritesRepository.subscribe(this)
@@ -51,7 +55,7 @@ class MangaDetailsPresenter private constructor() : BasePresenter<MangaDetailsVi
} ?: throw MangaNotFoundException("Cannot find manga by id") } ?: throw MangaNotFoundException("Cannot find manga by id")
viewState.onMangaUpdated(manga) viewState.onMangaUpdated(manga)
loadDetails(manga, true) loadDetails(manga, true)
} catch (_: CancellationException){ } catch (_: CancellationException) {
} catch (e: Throwable) { } catch (e: Throwable) {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
e.printStackTrace() e.printStackTrace()
@@ -79,7 +83,7 @@ class MangaDetailsPresenter private constructor() : BasePresenter<MangaDetailsVi
viewState.onMangaUpdated(data) viewState.onMangaUpdated(data)
this@MangaDetailsPresenter.manga = data this@MangaDetailsPresenter.manga = data
viewState.onNewChaptersChanged(trackingRepository.getNewChaptersCount(manga.id)) viewState.onNewChaptersChanged(trackingRepository.getNewChaptersCount(manga.id))
} catch (_: CancellationException){ } catch (_: CancellationException) {
} catch (e: Throwable) { } catch (e: Throwable) {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
e.printStackTrace() e.printStackTrace()
@@ -147,6 +151,38 @@ class MangaDetailsPresenter private constructor() : BasePresenter<MangaDetailsVi
} }
} }
fun loadRelated() {
val manga = this.manga ?: return
presenterScope.launch {
viewState.onLoadingStateChanged(isLoading = true)
var isFirstCall = true
searchRepository.globalSearch(manga.title)
.map { list ->
list.filter { x -> x.id != manga.id }
}.filterNot { x -> x.isEmpty() }
.flowOn(Dispatchers.IO)
.catch { e ->
if (e is IOException) {
viewState.onError(e)
}
}
.onEmpty {
viewState.onListChanged(emptyList())
viewState.onLoadingStateChanged(isLoading = false)
}.onCompletion {
viewState.onListAppended(emptyList())
}.collect {
if (isFirstCall) {
isFirstCall = false
viewState.onListChanged(it)
viewState.onLoadingStateChanged(isLoading = false)
} else {
viewState.onListAppended(it)
}
}
}
}
override fun onHistoryChanged() { override fun onHistoryChanged() {
loadHistory(manga ?: return) loadHistory(manga ?: return)
} }
@@ -162,18 +198,12 @@ class MangaDetailsPresenter private constructor() : BasePresenter<MangaDetailsVi
override fun onDestroy() { override fun onDestroy() {
HistoryRepository.unsubscribe(this) HistoryRepository.unsubscribe(this)
FavouritesRepository.unsubscribe(this) FavouritesRepository.unsubscribe(this)
instance = null clear(key)
super.onDestroy() super.onDestroy()
} }
companion object { companion object Holder : SharedPresenterHolder<MangaDetailsPresenter>() {
private var instance: MangaDetailsPresenter? = null override fun onCreatePresenter(key: Int) = MangaDetailsPresenter(key)
fun getInstance(): MangaDetailsPresenter = instance ?: synchronized(this) {
MangaDetailsPresenter().also {
instance = it
}
}
} }
} }

View File

@@ -1,5 +1,8 @@
package org.koitharu.kotatsu.ui.details package org.koitharu.kotatsu.ui.details
import moxy.viewstate.strategy.AddToEndSingleTagStrategy
import moxy.viewstate.strategy.AddToEndStrategy
import moxy.viewstate.strategy.StateStrategyType
import moxy.viewstate.strategy.alias.AddToEndSingle import moxy.viewstate.strategy.alias.AddToEndSingle
import moxy.viewstate.strategy.alias.SingleState import moxy.viewstate.strategy.alias.SingleState
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
@@ -23,4 +26,13 @@ interface MangaDetailsView : BaseMvpView {
@AddToEndSingle @AddToEndSingle
fun onNewChaptersChanged(newChapters: Int) fun onNewChaptersChanged(newChapters: Int)
@StateStrategyType(AddToEndSingleTagStrategy::class, tag = "content")
fun onListChanged(list: List<Manga>) = Unit
@StateStrategyType(AddToEndStrategy::class, tag = "content")
fun onListAppended(list: List<Manga>) = Unit
@StateStrategyType(AddToEndSingleTagStrategy::class, tag = "content")
fun onListError(e: Throwable) = Unit
} }

View File

@@ -0,0 +1,43 @@
package org.koitharu.kotatsu.ui.details
import android.os.Bundle
import android.view.View
import moxy.ktx.moxyPresenter
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.list.MangaListFragment
class RelatedMangaFragment : MangaListFragment<Unit>(), MangaDetailsView {
private val presenter by moxyPresenter {
MangaDetailsPresenter.getInstance(activity.hashCode())
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
isSwipeRefreshEnabled = false
}
override fun onRequestMoreItems(offset: Int) {
if (offset == 0) {
presenter.loadRelated()
}
}
override fun onMangaUpdated(manga: Manga) = Unit
override fun onHistoryChanged(history: MangaHistory?) = Unit
override fun onFavouriteChanged(categories: List<FavouriteCategory>) = Unit
override fun onMangaRemoved(manga: Manga) = Unit
override fun onNewChaptersChanged(newChapters: Int) = Unit
override fun onListChanged(list: List<Manga>) = super<MangaListFragment>.onListChanged(list)
override fun onListAppended(list: List<Manga>) = super<MangaListFragment>.onListAppended(list)
override fun onListError(e: Throwable) = super<MangaListFragment>.onListError(e)
}

View File

@@ -92,6 +92,11 @@ class DownloadNotification(private val context: Context) {
builder.setCategory(NotificationCompat.CATEGORY_PROGRESS) builder.setCategory(NotificationCompat.CATEGORY_PROGRESS)
} }
fun setWaitingForNetwork() {
builder.setProgress(0, 0, false)
builder.setContentText(context.getString(R.string.waiting_for_network))
}
fun setPostProcessing() { fun setPostProcessing() {
builder.setProgress(1, 0, true) builder.setProgress(1, 0, true)
builder.setContentText(context.getString(R.string.processing_)) builder.setContentText(context.getString(R.string.processing_))

View File

@@ -13,6 +13,7 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okio.IOException
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
@@ -24,10 +25,7 @@ import org.koitharu.kotatsu.domain.local.MangaZip
import org.koitharu.kotatsu.ui.common.BaseService import org.koitharu.kotatsu.ui.common.BaseService
import org.koitharu.kotatsu.ui.common.dialog.CheckBoxAlertDialog import org.koitharu.kotatsu.ui.common.dialog.CheckBoxAlertDialog
import org.koitharu.kotatsu.utils.CacheUtils import org.koitharu.kotatsu.utils.CacheUtils
import org.koitharu.kotatsu.utils.ext.await import org.koitharu.kotatsu.utils.ext.*
import org.koitharu.kotatsu.utils.ext.retryUntilSuccess
import org.koitharu.kotatsu.utils.ext.safe
import org.koitharu.kotatsu.utils.ext.sub
import java.io.File import java.io.File
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.collections.set import kotlin.collections.set
@@ -37,6 +35,7 @@ class DownloadService : BaseService() {
private lateinit var notification: DownloadNotification private lateinit var notification: DownloadNotification
private lateinit var wakeLock: PowerManager.WakeLock private lateinit var wakeLock: PowerManager.WakeLock
private lateinit var connectivityManager: ConnectivityManager
private val okHttp by inject<OkHttpClient>() private val okHttp by inject<OkHttpClient>()
private val cache by inject<PagesCache>() private val cache by inject<PagesCache>()
@@ -47,6 +46,7 @@ class DownloadService : BaseService() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
notification = DownloadNotification(this) notification = DownloadNotification(this)
connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager) wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager)
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "kotatsu:downloading") .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "kotatsu:downloading")
} }
@@ -88,9 +88,11 @@ class DownloadService : BaseService() {
try { try {
val repo = MangaProviderFactory.create(manga.source) val repo = MangaProviderFactory.create(manga.source)
val cover = safe { val cover = safe {
Coil.execute(GetRequestBuilder(this@DownloadService) Coil.execute(
.data(manga.coverUrl) GetRequestBuilder(this@DownloadService)
.build()).drawable .data(manga.coverUrl)
.build()
).drawable
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
notification.setLargeIcon(cover) notification.setLargeIcon(cover)
@@ -112,23 +114,30 @@ class DownloadService : BaseService() {
if (chaptersIds == null || chapter.id in chaptersIds) { if (chaptersIds == null || chapter.id in chaptersIds) {
val pages = repo.getPages(chapter) val pages = repo.getPages(chapter)
for ((pageIndex, page) in pages.withIndex()) { for ((pageIndex, page) in pages.withIndex()) {
val url = repo.getPageFullUrl(page) failsafe@ do {
val file = cache[url] ?: downloadPage(url, destination) try {
output.addPage( val url = repo.getPageFullUrl(page)
chapter, val file = cache[url] ?: downloadPage(url, destination)
file, output.addPage(
pageIndex, chapter,
MimeTypeMap.getFileExtensionFromUrl(url) file,
pageIndex,
MimeTypeMap.getFileExtensionFromUrl(url)
)
} catch (e: IOException) {
notification.setWaitingForNetwork()
notification.update()
connectivityManager.waitForNetwork()
continue@failsafe
}
} while (false)
notification.setProgress(
chapters.size,
pages.size,
chapterIndex,
pageIndex
) )
withContext(Dispatchers.Main) { notification.update()
notification.setProgress(
chapters.size,
pages.size,
chapterIndex,
pageIndex
)
notification.update()
}
} }
} }
} }

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.main.list package org.koitharu.kotatsu.ui.list
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
@@ -55,6 +55,9 @@ class ListModeSelectDialog : AlertDialogFragment(R.layout.dialog_list_mode), Vie
private const val TAG = "ListModeSelectDialog" private const val TAG = "ListModeSelectDialog"
fun show(fm: FragmentManager) = ListModeSelectDialog().show(fm, TAG) fun show(fm: FragmentManager) = ListModeSelectDialog()
.show(fm,
TAG
)
} }
} }

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.main package org.koitharu.kotatsu.ui.list
import android.app.ActivityOptions import android.app.ActivityOptions
import android.content.SharedPreferences import android.content.SharedPreferences
@@ -11,7 +11,6 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.postDelayed
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.swiperefreshlayout.widget.CircularProgressDrawable import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import com.google.android.material.navigation.NavigationView import com.google.android.material.navigation.NavigationView
@@ -25,13 +24,15 @@ import org.koitharu.kotatsu.core.prefs.AppSection
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.domain.MangaProviderFactory import org.koitharu.kotatsu.domain.MangaProviderFactory
import org.koitharu.kotatsu.ui.common.BaseActivity import org.koitharu.kotatsu.ui.common.BaseActivity
import org.koitharu.kotatsu.ui.main.list.favourites.FavouritesContainerFragment import org.koitharu.kotatsu.ui.list.favourites.FavouritesContainerFragment
import org.koitharu.kotatsu.ui.main.list.history.HistoryListFragment import org.koitharu.kotatsu.ui.list.feed.FeedFragment
import org.koitharu.kotatsu.ui.main.list.local.LocalListFragment import org.koitharu.kotatsu.ui.list.history.HistoryListFragment
import org.koitharu.kotatsu.ui.main.list.remote.RemoteListFragment import org.koitharu.kotatsu.ui.list.local.LocalListFragment
import org.koitharu.kotatsu.ui.list.remote.RemoteListFragment
import org.koitharu.kotatsu.ui.reader.ReaderActivity import org.koitharu.kotatsu.ui.reader.ReaderActivity
import org.koitharu.kotatsu.ui.reader.ReaderState import org.koitharu.kotatsu.ui.reader.ReaderState
import org.koitharu.kotatsu.ui.settings.AppUpdateService import org.koitharu.kotatsu.ui.search.SearchHelper
import org.koitharu.kotatsu.ui.settings.AppUpdateChecker
import org.koitharu.kotatsu.ui.settings.SettingsActivity import org.koitharu.kotatsu.ui.settings.SettingsActivity
import org.koitharu.kotatsu.ui.tracker.TrackWorker import org.koitharu.kotatsu.ui.tracker.TrackWorker
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
@@ -68,10 +69,8 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
} ?: run { } ?: run {
openDefaultSection() openDefaultSection()
} }
drawer.postDelayed(2000) {
AppUpdateService.startIfRequired(applicationContext)
}
TrackWorker.setup(applicationContext) TrackWorker.setup(applicationContext)
AppUpdateChecker(this).invoke()
} }
override fun onDestroy() { override fun onDestroy() {
@@ -90,8 +89,11 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
drawerToggle.onConfigurationChanged(newConfig) drawerToggle.onConfigurationChanged(newConfig)
} }
override fun onCreateOptionsMenu(menu: Menu?): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.opt_main, menu) menuInflater.inflate(R.menu.opt_main, menu)
menu.findItem(R.id.action_search)?.let { menuItem ->
SearchHelper.setupSearchView(menuItem)
}
return super.onCreateOptionsMenu(menu) return super.onCreateOptionsMenu(menu)
} }
@@ -118,6 +120,10 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
settings.defaultSection = AppSection.LOCAL settings.defaultSection = AppSection.LOCAL
setPrimaryFragment(LocalListFragment.newInstance()) setPrimaryFragment(LocalListFragment.newInstance())
} }
R.id.nav_feed -> {
settings.defaultSection = AppSection.FEED
setPrimaryFragment(FeedFragment.newInstance())
}
R.id.nav_action_settings -> { R.id.nav_action_settings -> {
startActivity(SettingsActivity.newIntent(this)) startActivity(SettingsActivity.newIntent(this))
return true return true
@@ -190,6 +196,10 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
navigationView.setCheckedItem(R.id.nav_history) navigationView.setCheckedItem(R.id.nav_history)
setPrimaryFragment(HistoryListFragment.newInstance()) setPrimaryFragment(HistoryListFragment.newInstance())
} }
AppSection.FEED -> {
navigationView.setCheckedItem(R.id.nav_feed)
setPrimaryFragment(FeedFragment.newInstance())
}
} }
} }

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.main package org.koitharu.kotatsu.ui.list
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.main package org.koitharu.kotatsu.ui.list
import moxy.viewstate.strategy.alias.OneExecution import moxy.viewstate.strategy.alias.OneExecution
import org.koitharu.kotatsu.ui.common.BaseMvpView import org.koitharu.kotatsu.ui.common.BaseMvpView

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.main.list package org.koitharu.kotatsu.ui.list
import android.view.ViewGroup import android.view.ViewGroup
import coil.api.clear import coil.api.clear

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.main.list package org.koitharu.kotatsu.ui.list
import android.view.ViewGroup import android.view.ViewGroup
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
@@ -14,7 +14,9 @@ class MangaListAdapter(onItemClickListener: OnRecyclerItemClickListener<Manga>)
override fun onCreateViewHolder(parent: ViewGroup) = when(listMode) { override fun onCreateViewHolder(parent: ViewGroup) = when(listMode) {
ListMode.LIST -> MangaListHolder(parent) ListMode.LIST -> MangaListHolder(parent)
ListMode.DETAILED_LIST -> MangaListDetailsHolder(parent) ListMode.DETAILED_LIST -> MangaListDetailsHolder(
parent
)
ListMode.GRID -> MangaGridHolder(parent) ListMode.GRID -> MangaGridHolder(parent)
} }

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.main.list package org.koitharu.kotatsu.ui.list
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.view.ViewGroup import android.view.ViewGroup

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.main.list package org.koitharu.kotatsu.ui.list
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
@@ -9,10 +9,7 @@ import androidx.core.view.GravityCompat
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.drawerlayout.widget.DrawerLayout import androidx.drawerlayout.widget.DrawerLayout
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.*
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_list.* import kotlinx.android.synthetic.main.fragment_list.*
@@ -28,23 +25,31 @@ import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.ui.common.BaseFragment import org.koitharu.kotatsu.ui.common.BaseFragment
import org.koitharu.kotatsu.ui.common.list.OnRecyclerItemClickListener import org.koitharu.kotatsu.ui.common.list.OnRecyclerItemClickListener
import org.koitharu.kotatsu.ui.common.list.PaginationScrollListener import org.koitharu.kotatsu.ui.common.list.PaginationScrollListener
import org.koitharu.kotatsu.ui.common.list.ProgressBarAdapter
import org.koitharu.kotatsu.ui.common.list.decor.ItemTypeDividerDecoration import org.koitharu.kotatsu.ui.common.list.decor.ItemTypeDividerDecoration
import org.koitharu.kotatsu.ui.common.list.decor.SectionItemDecoration import org.koitharu.kotatsu.ui.common.list.decor.SectionItemDecoration
import org.koitharu.kotatsu.ui.common.list.decor.SpacingItemDecoration import org.koitharu.kotatsu.ui.common.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.ui.details.MangaDetailsActivity import org.koitharu.kotatsu.ui.details.MangaDetailsActivity
import org.koitharu.kotatsu.ui.main.list.filter.FilterAdapter import org.koitharu.kotatsu.ui.list.filter.FilterAdapter
import org.koitharu.kotatsu.ui.main.list.filter.OnFilterChangedListener import org.koitharu.kotatsu.ui.list.filter.OnFilterChangedListener
import org.koitharu.kotatsu.utils.UiUtils import org.koitharu.kotatsu.utils.UiUtils
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
abstract class MangaListFragment<E> : BaseFragment(R.layout.fragment_list), MangaListView<E>, abstract class MangaListFragment<E> : BaseFragment(R.layout.fragment_list),
PaginationScrollListener.Callback, OnRecyclerItemClickListener<Manga>, MangaListView<E>, PaginationScrollListener.Callback, OnRecyclerItemClickListener<Manga>,
SharedPreferences.OnSharedPreferenceChangeListener, OnFilterChangedListener, SharedPreferences.OnSharedPreferenceChangeListener, OnFilterChangedListener,
SectionItemDecoration.Callback, SwipeRefreshLayout.OnRefreshListener { SectionItemDecoration.Callback, SwipeRefreshLayout.OnRefreshListener {
private val settings by inject<AppSettings>() private val settings by inject<AppSettings>()
private val adapterConfig = ConcatAdapter.Config.Builder()
.setIsolateViewTypes(true)
.setStableIdMode(ConcatAdapter.Config.StableIdMode.SHARED_STABLE_IDS)
.build()
private var adapter: MangaListAdapter? = null private var adapter: MangaListAdapter? = null
private var progressAdapter: ProgressBarAdapter? = null
private var paginationListener : PaginationScrollListener? = null
protected var isSwipeRefreshEnabled = true
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -55,10 +60,11 @@ abstract class MangaListFragment<E> : BaseFragment(R.layout.fragment_list), Mang
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
drawer?.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) drawer?.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
adapter = MangaListAdapter(this) adapter = MangaListAdapter(this)
progressAdapter = ProgressBarAdapter()
paginationListener = PaginationScrollListener(4, this)
recyclerView.setHasFixedSize(true) recyclerView.setHasFixedSize(true)
initListMode(settings.listMode) initListMode(settings.listMode)
recyclerView.adapter = adapter recyclerView.addOnScrollListener(paginationListener!!)
recyclerView.addOnScrollListener(PaginationScrollListener(4, this))
swipeRefreshLayout.setOnRefreshListener(this) swipeRefreshLayout.setOnRefreshListener(this)
recyclerView_filter.setHasFixedSize(true) recyclerView_filter.setHasFixedSize(true)
recyclerView_filter.addItemDecoration(ItemTypeDividerDecoration(view.context)) recyclerView_filter.addItemDecoration(ItemTypeDividerDecoration(view.context))
@@ -71,6 +77,8 @@ abstract class MangaListFragment<E> : BaseFragment(R.layout.fragment_list), Mang
override fun onDestroyView() { override fun onDestroyView() {
adapter = null adapter = null
progressAdapter = null
paginationListener = null
settings.unsubscribe(this) settings.unsubscribe(this)
super.onDestroyView() super.onDestroyView()
} }
@@ -122,6 +130,7 @@ abstract class MangaListFragment<E> : BaseFragment(R.layout.fragment_list), Mang
} }
override fun onListChanged(list: List<Manga>) { override fun onListChanged(list: List<Manga>) {
paginationListener?.reset()
adapter?.replaceData(list) adapter?.replaceData(list)
if (list.isEmpty()) { if (list.isEmpty()) {
setUpEmptyListHolder() setUpEmptyListHolder()
@@ -129,11 +138,16 @@ abstract class MangaListFragment<E> : BaseFragment(R.layout.fragment_list), Mang
} else { } else {
layout_holder.isVisible = false layout_holder.isVisible = false
} }
progressAdapter?.isProgressVisible = list.isNotEmpty()
recyclerView.callOnScrollListeners() recyclerView.callOnScrollListeners()
} }
override fun onListAppended(list: List<Manga>) { override fun onListAppended(list: List<Manga>) {
adapter?.appendData(list) adapter?.appendData(list)
progressAdapter?.isProgressVisible = list.isNotEmpty()
if (list.isNotEmpty()) {
layout_holder.isVisible = false
}
recyclerView.callOnScrollListeners() recyclerView.callOnScrollListeners()
} }
@@ -175,7 +189,7 @@ abstract class MangaListFragment<E> : BaseFragment(R.layout.fragment_list), Mang
val hasItems = recyclerView.hasItems val hasItems = recyclerView.hasItems
progressBar.isVisible = isLoading && !hasItems progressBar.isVisible = isLoading && !hasItems
swipeRefreshLayout.isRefreshing = isLoading && hasItems swipeRefreshLayout.isRefreshing = isLoading && hasItems
swipeRefreshLayout.isEnabled = !progressBar.isVisible swipeRefreshLayout.isEnabled = isSwipeRefreshEnabled && !progressBar.isVisible
if (isLoading) { if (isLoading) {
layout_holder.isVisible = false layout_holder.isVisible = false
} }
@@ -230,11 +244,18 @@ abstract class MangaListFragment<E> : BaseFragment(R.layout.fragment_list), Mang
recyclerView.removeOnLayoutChangeListener(UiUtils.SpanCountResolver) recyclerView.removeOnLayoutChangeListener(UiUtils.SpanCountResolver)
adapter?.listMode = mode adapter?.listMode = mode
recyclerView.layoutManager = when (mode) { recyclerView.layoutManager = when (mode) {
ListMode.GRID -> GridLayoutManager(ctx, UiUtils.resolveGridSpanCount(ctx)) ListMode.GRID -> {
GridLayoutManager(ctx, UiUtils.resolveGridSpanCount(ctx)).apply {
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int) = if (position < getItemsCount())
1 else this@apply.spanCount
}
}
}
else -> LinearLayoutManager(ctx) else -> LinearLayoutManager(ctx)
} }
recyclerView.recycledViewPool.clear() recyclerView.recycledViewPool.clear()
recyclerView.adapter = adapter recyclerView.adapter = ConcatAdapter(adapterConfig, adapter, progressAdapter)
recyclerView.addItemDecoration( recyclerView.addItemDecoration(
when (mode) { when (mode) {
ListMode.LIST -> DividerItemDecoration(ctx, RecyclerView.VERTICAL) ListMode.LIST -> DividerItemDecoration(ctx, RecyclerView.VERTICAL)
@@ -251,6 +272,8 @@ abstract class MangaListFragment<E> : BaseFragment(R.layout.fragment_list), Mang
recyclerView.firstItem = position recyclerView.firstItem = position
} }
override fun getItemsCount() = adapter?.itemCount ?: 0
final override fun isSection(position: Int): Boolean { final override fun isSection(position: Int): Boolean {
return position == 0 || recyclerView_filter.adapter?.run { return position == 0 || recyclerView_filter.adapter?.run {
getItemViewType(position) != getItemViewType(position - 1) getItemViewType(position) != getItemViewType(position - 1)

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.main.list package org.koitharu.kotatsu.ui.list
import android.view.ViewGroup import android.view.ViewGroup
import coil.api.clear import coil.api.clear

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.main.list package org.koitharu.kotatsu.ui.list
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
@@ -7,10 +7,7 @@ import android.view.View
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.*
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
@@ -27,22 +24,30 @@ import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.ui.common.BaseBottomSheet import org.koitharu.kotatsu.ui.common.BaseBottomSheet
import org.koitharu.kotatsu.ui.common.list.OnRecyclerItemClickListener import org.koitharu.kotatsu.ui.common.list.OnRecyclerItemClickListener
import org.koitharu.kotatsu.ui.common.list.PaginationScrollListener import org.koitharu.kotatsu.ui.common.list.PaginationScrollListener
import org.koitharu.kotatsu.ui.common.list.ProgressBarAdapter
import org.koitharu.kotatsu.ui.common.list.decor.SpacingItemDecoration import org.koitharu.kotatsu.ui.common.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.ui.details.MangaDetailsActivity import org.koitharu.kotatsu.ui.details.MangaDetailsActivity
import org.koitharu.kotatsu.utils.UiUtils import org.koitharu.kotatsu.utils.UiUtils
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
abstract class MangaListSheet<E> : BaseBottomSheet(R.layout.sheet_list), MangaListView<E>, abstract class MangaListSheet<E> : BaseBottomSheet(R.layout.sheet_list),
MangaListView<E>,
PaginationScrollListener.Callback, OnRecyclerItemClickListener<Manga>, PaginationScrollListener.Callback, OnRecyclerItemClickListener<Manga>,
SharedPreferences.OnSharedPreferenceChangeListener, Toolbar.OnMenuItemClickListener { SharedPreferences.OnSharedPreferenceChangeListener, Toolbar.OnMenuItemClickListener {
private val settings by inject<AppSettings>() private val settings by inject<AppSettings>()
private val adapterConfig = ConcatAdapter.Config.Builder()
.setIsolateViewTypes(true)
.setStableIdMode(ConcatAdapter.Config.StableIdMode.SHARED_STABLE_IDS)
.build()
private var adapter: MangaListAdapter? = null private var adapter: MangaListAdapter? = null
private var progressAdapter: ProgressBarAdapter? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
adapter = MangaListAdapter(this) adapter = MangaListAdapter(this)
progressAdapter = ProgressBarAdapter()
initListMode(settings.listMode) initListMode(settings.listMode)
recyclerView.adapter = adapter recyclerView.adapter = adapter
recyclerView.addOnScrollListener(PaginationScrollListener(4, this)) recyclerView.addOnScrollListener(PaginationScrollListener(4, this))
@@ -65,6 +70,7 @@ abstract class MangaListSheet<E> : BaseBottomSheet(R.layout.sheet_list), MangaLi
override fun onDestroyView() { override fun onDestroyView() {
settings.unsubscribe(this) settings.unsubscribe(this)
adapter = null adapter = null
progressAdapter = null
super.onDestroyView() super.onDestroyView()
} }
@@ -122,6 +128,7 @@ abstract class MangaListSheet<E> : BaseBottomSheet(R.layout.sheet_list), MangaLi
override fun onListChanged(list: List<Manga>) { override fun onListChanged(list: List<Manga>) {
adapter?.replaceData(list) adapter?.replaceData(list)
textView_holder.isVisible = list.isEmpty() textView_holder.isVisible = list.isEmpty()
progressAdapter?.isProgressVisible = list.isNotEmpty()
recyclerView.callOnScrollListeners() recyclerView.callOnScrollListeners()
} }
@@ -130,6 +137,7 @@ abstract class MangaListSheet<E> : BaseBottomSheet(R.layout.sheet_list), MangaLi
if (list.isNotEmpty()) { if (list.isNotEmpty()) {
textView_holder.isVisible = false textView_holder.isVisible = false
} }
progressAdapter?.isProgressVisible = list.isNotEmpty()
recyclerView.callOnScrollListeners() recyclerView.callOnScrollListeners()
} }
@@ -137,6 +145,8 @@ abstract class MangaListSheet<E> : BaseBottomSheet(R.layout.sheet_list), MangaLi
Snackbar.make(recyclerView, e.getDisplayMessage(resources), Snackbar.LENGTH_SHORT).show() Snackbar.make(recyclerView, e.getDisplayMessage(resources), Snackbar.LENGTH_SHORT).show()
} }
override fun getItemsCount() = adapter?.itemCount ?: 0
override fun onInitFilter( override fun onInitFilter(
sortOrders: List<SortOrder>, sortOrders: List<SortOrder>,
tags: List<MangaTag>, tags: List<MangaTag>,
@@ -170,10 +180,17 @@ abstract class MangaListSheet<E> : BaseBottomSheet(R.layout.sheet_list), MangaLi
recyclerView.removeOnLayoutChangeListener(UiUtils.SpanCountResolver) recyclerView.removeOnLayoutChangeListener(UiUtils.SpanCountResolver)
adapter?.listMode = mode adapter?.listMode = mode
recyclerView.layoutManager = when (mode) { recyclerView.layoutManager = when (mode) {
ListMode.GRID -> GridLayoutManager(ctx, UiUtils.resolveGridSpanCount(ctx)) ListMode.GRID -> {
GridLayoutManager(ctx, UiUtils.resolveGridSpanCount(ctx)).apply {
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int) = if (position < getItemsCount())
1 else this@apply.spanCount
}
}
}
else -> LinearLayoutManager(ctx) else -> LinearLayoutManager(ctx)
} }
recyclerView.adapter = adapter recyclerView.adapter = ConcatAdapter(adapterConfig, adapter, progressAdapter)
recyclerView.addItemDecoration( recyclerView.addItemDecoration(
when (mode) { when (mode) {
ListMode.LIST -> DividerItemDecoration(ctx, RecyclerView.VERTICAL) ListMode.LIST -> DividerItemDecoration(ctx, RecyclerView.VERTICAL)

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.main.list package org.koitharu.kotatsu.ui.list
import moxy.viewstate.strategy.AddToEndSingleTagStrategy import moxy.viewstate.strategy.AddToEndSingleTagStrategy
import moxy.viewstate.strategy.AddToEndStrategy import moxy.viewstate.strategy.AddToEndStrategy

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.main.list.favourites package org.koitharu.kotatsu.ui.list.favourites
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
@@ -14,9 +14,9 @@ import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.domain.favourites.FavouritesRepository import org.koitharu.kotatsu.domain.favourites.FavouritesRepository
import org.koitharu.kotatsu.domain.favourites.OnFavouritesChangeListener import org.koitharu.kotatsu.domain.favourites.OnFavouritesChangeListener
import org.koitharu.kotatsu.ui.common.BaseFragment import org.koitharu.kotatsu.ui.common.BaseFragment
import org.koitharu.kotatsu.ui.main.list.favourites.categories.CategoriesActivity import org.koitharu.kotatsu.ui.list.favourites.categories.CategoriesActivity
import org.koitharu.kotatsu.ui.main.list.favourites.categories.FavouriteCategoriesPresenter import org.koitharu.kotatsu.ui.list.favourites.categories.FavouriteCategoriesPresenter
import org.koitharu.kotatsu.ui.main.list.favourites.categories.FavouriteCategoriesView import org.koitharu.kotatsu.ui.list.favourites.categories.FavouriteCategoriesView
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList

View File

@@ -1,13 +1,14 @@
package org.koitharu.kotatsu.ui.main.list.favourites package org.koitharu.kotatsu.ui.list.favourites
import kotlinx.android.synthetic.main.fragment_list.* import kotlinx.android.synthetic.main.fragment_list.*
import moxy.ktx.moxyPresenter import moxy.ktx.moxyPresenter
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.ui.main.list.MangaListFragment import org.koitharu.kotatsu.ui.list.MangaListFragment
import org.koitharu.kotatsu.ui.main.list.MangaListView import org.koitharu.kotatsu.ui.list.MangaListView
import org.koitharu.kotatsu.utils.ext.withArgs import org.koitharu.kotatsu.utils.ext.withArgs
class FavouritesListFragment : MangaListFragment<Unit>(), MangaListView<Unit> { class FavouritesListFragment : MangaListFragment<Unit>(),
MangaListView<Unit> {
private val presenter by moxyPresenter(factory = ::FavouritesListPresenter) private val presenter by moxyPresenter(factory = ::FavouritesListPresenter)

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.main.list.favourites package org.koitharu.kotatsu.ui.list.favourites
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -9,7 +9,7 @@ import moxy.presenterScope
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.domain.favourites.FavouritesRepository import org.koitharu.kotatsu.domain.favourites.FavouritesRepository
import org.koitharu.kotatsu.ui.common.BasePresenter import org.koitharu.kotatsu.ui.common.BasePresenter
import org.koitharu.kotatsu.ui.main.list.MangaListView import org.koitharu.kotatsu.ui.list.MangaListView
@InjectViewState @InjectViewState
class FavouritesListPresenter : BasePresenter<MangaListView<Unit>>() { class FavouritesListPresenter : BasePresenter<MangaListView<Unit>>() {

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.main.list.favourites package org.koitharu.kotatsu.ui.list.favourites
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.main.list.favourites.categories package org.koitharu.kotatsu.ui.list.favourites.categories
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@@ -7,11 +7,11 @@ import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.text.InputType import android.text.InputType
import android.view.View import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_categories.* import kotlinx.android.synthetic.main.activity_categories.*
import moxy.ktx.moxyPresenter import moxy.ktx.moxyPresenter
@@ -78,7 +78,7 @@ class CategoriesActivity : BaseActivity(), OnRecyclerItemClickListener<Favourite
} }
private fun deleteCategory(category: FavouriteCategory) { private fun deleteCategory(category: FavouriteCategory) {
AlertDialog.Builder(this) MaterialAlertDialogBuilder(this)
.setMessage(getString(R.string.category_delete_confirm, category.title)) .setMessage(getString(R.string.category_delete_confirm, category.title))
.setTitle(R.string.remove_category) .setTitle(R.string.remove_category)
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.main.list.favourites.categories package org.koitharu.kotatsu.ui.list.favourites.categories
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.view.MotionEvent import android.view.MotionEvent
@@ -19,7 +19,7 @@ class CategoriesAdapter(private val onItemClickListener: OnRecyclerItemClickList
override fun getExtra(item: FavouriteCategory, position: Int) = Unit override fun getExtra(item: FavouriteCategory, position: Int) = Unit
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
override fun onViewHolderCreated(holder: BaseViewHolder<FavouriteCategory, Unit>) { override fun onViewAttachedToWindow(holder: BaseViewHolder<FavouriteCategory, Unit>) {
holder.imageView_more.setOnClickListener { v -> holder.imageView_more.setOnClickListener { v ->
onItemClickListener.onItemClick(holder.requireData(), holder.bindingAdapterPosition, v) onItemClickListener.onItemClick(holder.requireData(), holder.bindingAdapterPosition, v)
} }
@@ -32,6 +32,11 @@ class CategoriesAdapter(private val onItemClickListener: OnRecyclerItemClickList
} }
} }
override fun onViewDetachedFromWindow(holder: BaseViewHolder<FavouriteCategory, Unit>) {
holder.imageView_more.setOnClickListener(null)
holder.imageView_handle.setOnTouchListener(null)
}
fun moveItem(oldPos: Int, newPos: Int) { fun moveItem(oldPos: Int, newPos: Int) {
val item = dataSet.removeAt(oldPos) val item = dataSet.removeAt(oldPos)
dataSet.add(newPos, item) dataSet.add(newPos, item)

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.main.list.favourites.categories package org.koitharu.kotatsu.ui.list.favourites.categories
import android.view.ViewGroup import android.view.ViewGroup
import kotlinx.android.synthetic.main.item_category.* import kotlinx.android.synthetic.main.item_category.*

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.main.list.favourites.categories package org.koitharu.kotatsu.ui.list.favourites.categories
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.main.list.favourites.categories package org.koitharu.kotatsu.ui.list.favourites.categories
import moxy.MvpView import moxy.MvpView
import moxy.viewstate.strategy.AddToEndSingleStrategy import moxy.viewstate.strategy.AddToEndSingleStrategy

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.main.list.favourites.categories.select package org.koitharu.kotatsu.ui.list.favourites.categories.select
import android.util.SparseBooleanArray import android.util.SparseBooleanArray
import android.view.ViewGroup import android.view.ViewGroup
@@ -31,8 +31,11 @@ class CategoriesSelectAdapter(private val listener: OnCategoryCheckListener) :
override fun onGetItemId(item: FavouriteCategory) = item.id override fun onGetItemId(item: FavouriteCategory) = item.id
override fun onViewHolderCreated(holder: BaseViewHolder<FavouriteCategory, Boolean>) { override fun onViewDetachedFromWindow(holder: BaseViewHolder<FavouriteCategory, Boolean>) {
super.onViewHolderCreated(holder) holder.itemView.setOnClickListener(null)
}
override fun onViewAttachedToWindow(holder: BaseViewHolder<FavouriteCategory, Boolean>) {
holder.itemView.setOnClickListener { holder.itemView.setOnClickListener {
if (it !is Checkable) return@setOnClickListener if (it !is Checkable) return@setOnClickListener
it.toggle() it.toggle()

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.main.list.favourites.categories.select package org.koitharu.kotatsu.ui.list.favourites.categories.select
import android.view.ViewGroup import android.view.ViewGroup
import kotlinx.android.synthetic.main.item_category_checkable.* import kotlinx.android.synthetic.main.item_category_checkable.*

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.main.list.favourites.categories.select package org.koitharu.kotatsu.ui.list.favourites.categories.select
import android.os.Bundle import android.os.Bundle
import android.text.InputType import android.text.InputType
@@ -12,8 +12,8 @@ import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.ui.common.BaseBottomSheet import org.koitharu.kotatsu.ui.common.BaseBottomSheet
import org.koitharu.kotatsu.ui.common.dialog.TextInputDialog import org.koitharu.kotatsu.ui.common.dialog.TextInputDialog
import org.koitharu.kotatsu.ui.main.list.favourites.categories.FavouriteCategoriesPresenter import org.koitharu.kotatsu.ui.list.favourites.categories.FavouriteCategoriesPresenter
import org.koitharu.kotatsu.ui.main.list.favourites.categories.FavouriteCategoriesView import org.koitharu.kotatsu.ui.list.favourites.categories.FavouriteCategoriesView
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.withArgs import org.koitharu.kotatsu.utils.ext.withArgs

View File

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

View File

@@ -0,0 +1,19 @@
package org.koitharu.kotatsu.ui.list.feed
import android.view.ViewGroup
import org.koitharu.kotatsu.core.model.TrackingLogItem
import org.koitharu.kotatsu.ui.common.list.BaseRecyclerAdapter
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
import org.koitharu.kotatsu.ui.common.list.OnRecyclerItemClickListener
class FeedAdapter(onItemClickListener: OnRecyclerItemClickListener<TrackingLogItem>? = null) :
BaseRecyclerAdapter<TrackingLogItem, Unit>(onItemClickListener) {
override fun onCreateViewHolder(parent: ViewGroup): BaseViewHolder<TrackingLogItem, Unit> {
return FeedHolder(parent)
}
override fun onGetItemId(item: TrackingLogItem) = item.id
override fun getExtra(item: TrackingLogItem, position: Int) = Unit
}

View File

@@ -0,0 +1,126 @@
package org.koitharu.kotatsu.ui.list.feed
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.View
import androidx.core.view.isVisible
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_tracklogs.*
import moxy.MvpDelegate
import moxy.ktx.moxyPresenter
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.TrackingLogItem
import org.koitharu.kotatsu.ui.common.BaseFragment
import org.koitharu.kotatsu.ui.common.list.OnRecyclerItemClickListener
import org.koitharu.kotatsu.ui.common.list.PaginationScrollListener
import org.koitharu.kotatsu.ui.common.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.ui.details.MangaDetailsActivity
import org.koitharu.kotatsu.utils.ext.callOnScrollListeners
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.hasItems
class FeedFragment : BaseFragment(R.layout.fragment_tracklogs), FeedView,
PaginationScrollListener.Callback, OnRecyclerItemClickListener<TrackingLogItem> {
private val presenter by moxyPresenter(factory = ::FeedPresenter)
private var adapter: FeedAdapter? = null
override fun getTitle() = context?.getString(R.string.updates)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
adapter = FeedAdapter(this)
recyclerView.adapter = adapter
recyclerView.addItemDecoration(
SpacingItemDecoration(resources.getDimensionPixelOffset(R.dimen.grid_spacing))
)
recyclerView.setHasFixedSize(true)
recyclerView.addOnScrollListener(PaginationScrollListener(4, this))
if (savedInstanceState?.containsKey(MvpDelegate.MOXY_DELEGATE_TAGS_KEY) != true) {
onRequestMoreItems(0)
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.opt_feed, menu)
}
override fun onDestroyView() {
adapter = null
super.onDestroyView()
}
override fun onListChanged(list: List<TrackingLogItem>) {
adapter?.replaceData(list)
if (list.isEmpty()) {
setUpEmptyListHolder()
layout_holder.isVisible = true
} else {
layout_holder.isVisible = false
}
recyclerView.callOnScrollListeners()
}
override fun onListAppended(list: List<TrackingLogItem>) {
adapter?.appendData(list)
recyclerView.callOnScrollListeners()
}
override fun onListError(e: Throwable) {
if (recyclerView.hasItems) {
Snackbar.make(recyclerView, e.getDisplayMessage(resources), Snackbar.LENGTH_SHORT)
.show()
} else {
textView_holder.text = e.getDisplayMessage(resources)
textView_holder.setCompoundDrawablesRelativeWithIntrinsicBounds(
0,
R.drawable.ic_error_large,
0,
0
)
layout_holder.isVisible = true
}
}
override fun onError(e: Throwable) {
Snackbar.make(recyclerView, e.getDisplayMessage(resources), Snackbar.LENGTH_SHORT).show()
}
override fun onLoadingStateChanged(isLoading: Boolean) {
val hasItems = recyclerView.hasItems
progressBar.isVisible = isLoading && !hasItems
if (isLoading) {
layout_holder.isVisible = false
}
}
override fun getItemsCount(): Int {
return adapter?.itemCount ?: 0
}
override fun onRequestMoreItems(offset: Int) {
presenter.loadList(offset)
}
override fun onItemClick(item: TrackingLogItem, position: Int, view: View) {
startActivity(MangaDetailsActivity.newIntent(context ?: return, item.manga))
}
private fun setUpEmptyListHolder() {
textView_holder.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, null, null)
textView_holder.setText(R.string.text_feed_holder)
}
companion object {
fun newInstance() = FeedFragment()
}
}

View File

@@ -0,0 +1,41 @@
package org.koitharu.kotatsu.ui.list.feed
import android.text.format.DateUtils
import android.view.ViewGroup
import coil.api.clear
import coil.api.load
import kotlinx.android.synthetic.main.item_tracklog.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.TrackingLogItem
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
import org.koitharu.kotatsu.utils.ext.formatRelative
class FeedHolder(parent: ViewGroup) :
BaseViewHolder<TrackingLogItem, Unit>(parent, R.layout.item_tracklog) {
override fun onBind(data: TrackingLogItem, extra: Unit) {
imageView_cover.load(data.manga.coverUrl) {
placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_placeholder)
}
textView_title.text = data.manga.title
textView_subtitle.text = buildString {
append(data.createdAt.formatRelative(DateUtils.DAY_IN_MILLIS))
append(" ")
append(
context.resources.getQuantityString(
R.plurals.new_chapters,
data.chapters.size,
data.chapters.size
)
)
}
textView_chapters.text = data.chapters.joinToString("\n")
}
override fun onRecycled() {
super.onRecycled()
imageView_cover.clear()
}
}

View File

@@ -0,0 +1,48 @@
package org.koitharu.kotatsu.ui.list.feed
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import moxy.presenterScope
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.domain.tracking.TrackingRepository
import org.koitharu.kotatsu.ui.common.BasePresenter
class FeedPresenter : BasePresenter<FeedView>() {
private lateinit var repository: TrackingRepository
override fun onFirstViewAttach() {
repository = TrackingRepository()
super.onFirstViewAttach()
}
fun loadList(offset: Int) {
presenterScope.launch {
viewState.onLoadingStateChanged(true)
try {
val list = withContext(Dispatchers.IO) {
repository.getTrackingLog(offset, 20)
}
if (offset == 0) {
viewState.onListChanged(list)
} else {
viewState.onListAppended(list)
}
} catch (e: CancellationException) {
} catch (e: Throwable) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
if (offset == 0) {
viewState.onListError(e)
} else {
viewState.onError(e)
}
} finally {
viewState.onLoadingStateChanged(false)
}
}
}
}

View File

@@ -0,0 +1,19 @@
package org.koitharu.kotatsu.ui.list.feed
import moxy.viewstate.strategy.AddToEndSingleTagStrategy
import moxy.viewstate.strategy.AddToEndStrategy
import moxy.viewstate.strategy.StateStrategyType
import org.koitharu.kotatsu.core.model.TrackingLogItem
import org.koitharu.kotatsu.ui.common.BaseMvpView
interface FeedView : BaseMvpView {
@StateStrategyType(AddToEndSingleTagStrategy::class, tag = "content")
fun onListChanged(list: List<TrackingLogItem>)
@StateStrategyType(AddToEndStrategy::class, tag = "content")
fun onListAppended(list: List<TrackingLogItem>)
@StateStrategyType(AddToEndSingleTagStrategy::class, tag = "content")
fun onListError(e: Throwable)
}

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.main.list.filter package org.koitharu.kotatsu.ui.list.filter
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.main.list.filter package org.koitharu.kotatsu.ui.list.filter
import android.view.ViewGroup import android.view.ViewGroup
import kotlinx.android.synthetic.main.item_checkable_single.* import kotlinx.android.synthetic.main.item_checkable_single.*

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.main.list.filter package org.koitharu.kotatsu.ui.list.filter
import android.view.ViewGroup import android.view.ViewGroup
import kotlinx.android.synthetic.main.item_checkable_single.* import kotlinx.android.synthetic.main.item_checkable_single.*

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.main.list.filter package org.koitharu.kotatsu.ui.list.filter
import org.koitharu.kotatsu.core.model.MangaFilter import org.koitharu.kotatsu.core.model.MangaFilter

View File

@@ -1,20 +1,21 @@
package org.koitharu.kotatsu.ui.main.list.history package org.koitharu.kotatsu.ui.list.history
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.app.AlertDialog import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_list.* import kotlinx.android.synthetic.main.fragment_list.*
import moxy.ktx.moxyPresenter import moxy.ktx.moxyPresenter
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaHistory import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.ui.main.list.MangaListFragment import org.koitharu.kotatsu.ui.list.MangaListFragment
import org.koitharu.kotatsu.ui.main.list.MangaListView import org.koitharu.kotatsu.ui.list.MangaListView
import org.koitharu.kotatsu.utils.ext.ellipsize import org.koitharu.kotatsu.utils.ext.ellipsize
class HistoryListFragment : MangaListFragment<MangaHistory>(), MangaListView<MangaHistory> { class HistoryListFragment : MangaListFragment<MangaHistory>(),
MangaListView<MangaHistory> {
private val presenter by moxyPresenter(factory = ::HistoryListPresenter) private val presenter by moxyPresenter(factory = ::HistoryListPresenter)
@@ -30,7 +31,7 @@ class HistoryListFragment : MangaListFragment<MangaHistory>(), MangaListView<Man
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) { return when (item.itemId) {
R.id.action_clear_history -> { R.id.action_clear_history -> {
AlertDialog.Builder(context ?: return false) MaterialAlertDialogBuilder(context ?: return false)
.setTitle(R.string.clear_history) .setTitle(R.string.clear_history)
.setMessage(R.string.text_clear_history_prompt) .setMessage(R.string.text_clear_history_prompt)
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.main.list.history package org.koitharu.kotatsu.ui.list.history
import android.os.Build import android.os.Build
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
@@ -13,7 +13,7 @@ import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaHistory import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.domain.history.HistoryRepository import org.koitharu.kotatsu.domain.history.HistoryRepository
import org.koitharu.kotatsu.ui.common.BasePresenter import org.koitharu.kotatsu.ui.common.BasePresenter
import org.koitharu.kotatsu.ui.main.list.MangaListView import org.koitharu.kotatsu.ui.list.MangaListView
import org.koitharu.kotatsu.utils.MangaShortcut import org.koitharu.kotatsu.utils.MangaShortcut
@InjectViewState @InjectViewState

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.main.list.local package org.koitharu.kotatsu.ui.list.local
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.net.Uri import android.net.Uri
@@ -7,14 +7,14 @@ import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import androidx.activity.result.ActivityResultCallback import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_list.* import kotlinx.android.synthetic.main.fragment_list.*
import moxy.ktx.moxyPresenter import moxy.ktx.moxyPresenter
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.ui.main.list.MangaListFragment import org.koitharu.kotatsu.ui.list.MangaListFragment
import org.koitharu.kotatsu.utils.ext.ellipsize import org.koitharu.kotatsu.utils.ext.ellipsize
import java.io.File import java.io.File
@@ -23,9 +23,7 @@ class LocalListFragment : MangaListFragment<File>(), ActivityResultCallback<Uri>
private val presenter by moxyPresenter(factory = ::LocalListPresenter) private val presenter by moxyPresenter(factory = ::LocalListPresenter)
override fun onRequestMoreItems(offset: Int) { override fun onRequestMoreItems(offset: Int) {
if (offset == 0) { presenter.loadList(offset)
presenter.loadList()
}
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@@ -76,7 +74,7 @@ class LocalListFragment : MangaListFragment<File>(), ActivityResultCallback<Uri>
override fun onPopupMenuItemSelected(item: MenuItem, data: Manga): Boolean { override fun onPopupMenuItemSelected(item: MenuItem, data: Manga): Boolean {
return when (item.itemId) { return when (item.itemId) {
R.id.action_delete -> { R.id.action_delete -> {
AlertDialog.Builder(context ?: return false) MaterialAlertDialogBuilder(context ?: return false)
.setTitle(R.string.delete_manga) .setTitle(R.string.delete_manga)
.setMessage(getString(R.string.text_delete_local_manga, data.title)) .setMessage(getString(R.string.text_delete_local_manga, data.title))
.setPositiveButton(R.string.delete) { _, _ -> .setPositiveButton(R.string.delete) { _, _ ->
@@ -102,8 +100,6 @@ class LocalListFragment : MangaListFragment<File>(), ActivityResultCallback<Uri>
companion object { companion object {
private const val REQUEST_IMPORT = 50
fun newInstance() = LocalListFragment() fun newInstance() = LocalListFragment()
} }
} }

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.main.list.local package org.koitharu.kotatsu.ui.list.local
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
@@ -13,13 +13,12 @@ import org.koin.core.get
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.parser.LocalMangaRepository import org.koitharu.kotatsu.core.parser.LocalMangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.domain.MangaProviderFactory import org.koitharu.kotatsu.domain.MangaProviderFactory
import org.koitharu.kotatsu.domain.history.HistoryRepository import org.koitharu.kotatsu.domain.history.HistoryRepository
import org.koitharu.kotatsu.ui.common.BasePresenter import org.koitharu.kotatsu.ui.common.BasePresenter
import org.koitharu.kotatsu.ui.main.list.MangaListView import org.koitharu.kotatsu.ui.list.MangaListView
import org.koitharu.kotatsu.utils.MangaShortcut import org.koitharu.kotatsu.utils.MangaShortcut
import org.koitharu.kotatsu.utils.MediaStoreCompat import org.koitharu.kotatsu.utils.MediaStoreCompat
import org.koitharu.kotatsu.utils.ext.safe import org.koitharu.kotatsu.utils.ext.safe
@@ -33,12 +32,17 @@ class LocalListPresenter : BasePresenter<MangaListView<File>>() {
private lateinit var repository: LocalMangaRepository private lateinit var repository: LocalMangaRepository
override fun onFirstViewAttach() { override fun onFirstViewAttach() {
repository = MangaProviderFactory.create(MangaSource.LOCAL) as LocalMangaRepository repository = MangaProviderFactory.createLocal()
super.onFirstViewAttach() super.onFirstViewAttach()
} }
fun loadList() { fun loadList(offset: Int) {
presenterScope.launch { presenterScope.launch {
if (offset != 0) {
viewState.onListAppended(emptyList())
return@launch
}
viewState.onLoadingStateChanged(true) viewState.onLoadingStateChanged(true)
try { try {
val list = withContext(Dispatchers.IO) { val list = withContext(Dispatchers.IO) {

View File

@@ -1,13 +1,14 @@
package org.koitharu.kotatsu.ui.main.list.remote package org.koitharu.kotatsu.ui.list.remote
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem
import moxy.ktx.moxyPresenter import moxy.ktx.moxyPresenter
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaFilter import org.koitharu.kotatsu.core.model.MangaFilter
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.ui.main.list.MangaListFragment import org.koitharu.kotatsu.ui.list.MangaListFragment
import org.koitharu.kotatsu.ui.search.SearchHelper import org.koitharu.kotatsu.ui.search.SearchActivity
import org.koitharu.kotatsu.utils.ext.withArgs import org.koitharu.kotatsu.utils.ext.withArgs
class RemoteListFragment : MangaListFragment<Unit>() { class RemoteListFragment : MangaListFragment<Unit>() {
@@ -31,12 +32,17 @@ class RemoteListFragment : MangaListFragment<Unit>() {
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.opt_remote, menu) inflater.inflate(R.menu.opt_remote, menu)
menu.findItem(R.id.action_search)?.let { menuItem ->
SearchHelper.setupSearchView(menuItem, source)
}
super.onCreateOptionsMenu(menu, inflater) super.onCreateOptionsMenu(menu, inflater)
} }
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.action_search_internal -> {
context?.startActivity(SearchActivity.newIntent(requireContext(), source, null))
true
}
else -> super.onOptionsItemSelected(item)
}
companion object { companion object {
private const val ARG_SOURCE = "provider" private const val ARG_SOURCE = "provider"

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.main.list.remote package org.koitharu.kotatsu.ui.list.remote
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -11,7 +11,7 @@ import org.koitharu.kotatsu.core.model.MangaFilter
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.domain.MangaProviderFactory import org.koitharu.kotatsu.domain.MangaProviderFactory
import org.koitharu.kotatsu.ui.common.BasePresenter import org.koitharu.kotatsu.ui.common.BasePresenter
import org.koitharu.kotatsu.ui.main.list.MangaListView import org.koitharu.kotatsu.ui.list.MangaListView
@InjectViewState @InjectViewState
class RemoteListPresenter : BasePresenter<MangaListView<Unit>>() { class RemoteListPresenter : BasePresenter<MangaListView<Unit>>() {

View File

@@ -12,12 +12,12 @@ import android.view.*
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.ActivityResultCallback import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.postDelayed import androidx.core.view.postDelayed
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.commit import androidx.fragment.app.commit
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_reader.* import kotlinx.android.synthetic.main.activity_reader.*
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@@ -75,7 +75,7 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
?: intent.getParcelableExtra<ReaderState>(EXTRA_STATE) ?: intent.getParcelableExtra<ReaderState>(EXTRA_STATE)
?: let { ?: let {
Toast.makeText(this, R.string.error_occurred, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.error_occurred, Toast.LENGTH_SHORT).show()
finish() finishAfterTransition()
return return
} }
@@ -224,7 +224,7 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
} }
override fun onError(e: Throwable) { override fun onError(e: Throwable) {
val dialog = AlertDialog.Builder(this) val dialog = MaterialAlertDialogBuilder(this)
.setTitle(R.string.error_occurred) .setTitle(R.string.error_occurred)
.setMessage(e.message) .setMessage(e.message)
.setPositiveButton(R.string.close, null) .setPositiveButton(R.string.close, null)

View File

@@ -6,7 +6,7 @@ import androidx.fragment.app.FragmentManager
import moxy.ktx.moxyPresenter import moxy.ktx.moxyPresenter
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.ui.main.list.MangaListSheet import org.koitharu.kotatsu.ui.list.MangaListSheet
import org.koitharu.kotatsu.utils.ext.withArgs import org.koitharu.kotatsu.utils.ext.withArgs
class MangaSearchSheet : MangaListSheet<Unit>() { class MangaSearchSheet : MangaListSheet<Unit>() {

View File

@@ -4,38 +4,60 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import androidx.appcompat.widget.SearchView
import kotlinx.android.synthetic.main.activity_search.*
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.ui.common.BaseActivity import org.koitharu.kotatsu.ui.common.BaseActivity
import org.koitharu.kotatsu.utils.ext.showKeyboard
class SearchActivity : BaseActivity() { class SearchActivity : BaseActivity(), SearchView.OnQueryTextListener {
private lateinit var source: MangaSource
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_search) setContentView(R.layout.activity_search)
val source = intent.getParcelableExtra<MangaSource>(EXTRA_SOURCE) source = intent.getParcelableExtra(EXTRA_SOURCE) ?: run {
val query = intent.getStringExtra(EXTRA_QUERY) finishAfterTransition()
if (source == null || query == null) {
finish()
return return
} }
val query = intent.getStringExtra(EXTRA_QUERY)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
title = query searchView.queryHint = getString(R.string.search_on_s, source.title)
supportActionBar?.subtitle = getString(R.string.search_results_on_s, source.title) searchView.suggestionsAdapter = MangaSuggestionsProvider.getSuggestionAdapter(this)
supportFragmentManager searchView.setOnSuggestionListener(SearchHelper.SuggestionListener(searchView))
.beginTransaction() searchView.setOnQueryTextListener(this)
.replace(R.id.container, SearchFragment.newInstance(source, query))
.commit() if (query.isNullOrBlank()) {
searchView.requestFocus()
searchView.showKeyboard()
} else {
searchView.setQuery(query, true)
}
} }
override fun onQueryTextSubmit(query: String?): Boolean {
return if (!query.isNullOrBlank()) {
title = query
supportFragmentManager
.beginTransaction()
.replace(R.id.container, SearchFragment.newInstance(source, query))
.commit()
searchView.clearFocus()
MangaSuggestionsProvider.saveQuery(this, query)
true
} else false
}
override fun onQueryTextChange(newText: String?) = false
companion object { companion object {
private const val EXTRA_SOURCE = "source" private const val EXTRA_SOURCE = "source"
private const val EXTRA_QUERY = "query" private const val EXTRA_QUERY = "query"
fun newIntent(context: Context, source: MangaSource, query: String) = fun newIntent(context: Context, source: MangaSource, query: String?) =
Intent(context, SearchActivity::class.java) Intent(context, SearchActivity::class.java)
.putExtra(EXTRA_SOURCE, source as Parcelable) .putExtra(EXTRA_SOURCE, source as Parcelable)
.putExtra(EXTRA_QUERY, query) .putExtra(EXTRA_QUERY, query)

View File

@@ -2,7 +2,7 @@ package org.koitharu.kotatsu.ui.search
import moxy.ktx.moxyPresenter import moxy.ktx.moxyPresenter
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.ui.main.list.MangaListFragment import org.koitharu.kotatsu.ui.list.MangaListFragment
import org.koitharu.kotatsu.utils.ext.withArgs import org.koitharu.kotatsu.utils.ext.withArgs
class SearchFragment : MangaListFragment<Unit>() { class SearchFragment : MangaListFragment<Unit>() {

View File

@@ -6,27 +6,27 @@ import android.database.Cursor
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.ui.search.global.GlobalSearchActivity
import org.koitharu.kotatsu.utils.ext.safe import org.koitharu.kotatsu.utils.ext.safe
object SearchHelper { object SearchHelper {
@JvmStatic @JvmStatic
fun setupSearchView(menuItem: MenuItem, source: MangaSource) { fun setupSearchView(menuItem: MenuItem) {
val view = menuItem.actionView as? SearchView ?: return val view = menuItem.actionView as? SearchView ?: return
val context = view.context val context = view.context
view.queryHint = context.getString(R.string.search_manga) view.queryHint = context.getString(R.string.search_manga)
view.suggestionsAdapter = MangaSuggestionsProvider.getSuggestionAdapter(context) view.suggestionsAdapter = MangaSuggestionsProvider.getSuggestionAdapter(context)
view.setOnQueryTextListener(QueryListener(context, source)) view.setOnQueryTextListener(QueryListener(context))
view.setOnSuggestionListener(SuggestionListener(view)) view.setOnSuggestionListener(SuggestionListener(view))
} }
private class QueryListener(private val context: Context, private val source: MangaSource) : private class QueryListener(private val context: Context) :
SearchView.OnQueryTextListener { SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean { override fun onQueryTextSubmit(query: String?): Boolean {
return if (!query.isNullOrBlank()) { return if (!query.isNullOrBlank()) {
context.startActivity(SearchActivity.newIntent(context, source, query.trim())) context.startActivity(GlobalSearchActivity.newIntent(context, query.trim()))
MangaSuggestionsProvider.saveQuery(context, query) MangaSuggestionsProvider.saveQuery(context, query)
true true
} else false } else false
@@ -35,7 +35,7 @@ object SearchHelper {
override fun onQueryTextChange(newText: String?) = false override fun onQueryTextChange(newText: String?) = false
} }
private class SuggestionListener(private val view: SearchView) : class SuggestionListener(private val view: SearchView) :
SearchView.OnSuggestionListener { SearchView.OnSuggestionListener {
override fun onSuggestionSelect(position: Int) = false override fun onSuggestionSelect(position: Int) = false

View File

@@ -10,18 +10,11 @@ import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.domain.MangaProviderFactory import org.koitharu.kotatsu.domain.MangaProviderFactory
import org.koitharu.kotatsu.ui.common.BasePresenter import org.koitharu.kotatsu.ui.common.BasePresenter
import org.koitharu.kotatsu.ui.main.list.MangaListView import org.koitharu.kotatsu.ui.list.MangaListView
@InjectViewState @InjectViewState
class SearchPresenter : BasePresenter<MangaListView<Unit>>() { class SearchPresenter : BasePresenter<MangaListView<Unit>>() {
private lateinit var sources: Array<MangaSource>
override fun onFirstViewAttach() {
sources = MangaSource.values()
super.onFirstViewAttach()
}
fun loadList(source: MangaSource, query: String, offset: Int) { fun loadList(source: MangaSource, query: String, offset: Int) {
presenterScope.launch { presenterScope.launch {
viewState.onLoadingStateChanged(true) viewState.onLoadingStateChanged(true)

View File

@@ -0,0 +1,38 @@
package org.koitharu.kotatsu.ui.search.global
import android.content.Context
import android.content.Intent
import android.os.Bundle
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.ui.common.BaseActivity
class GlobalSearchActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_search_global)
val query = intent.getStringExtra(EXTRA_QUERY)
if (query == null) {
finishAfterTransition()
return
}
supportActionBar?.setDisplayHomeAsUpEnabled(true)
title = query
supportActionBar?.subtitle = getString(R.string.search_results)
supportFragmentManager
.beginTransaction()
.replace(R.id.container, GlobalSearchFragment.newInstance(query))
.commit()
}
companion object {
private const val EXTRA_QUERY = "query"
fun newIntent(context: Context, query: String) =
Intent(context, GlobalSearchActivity::class.java)
.putExtra(EXTRA_QUERY, query)
}
}

View File

@@ -0,0 +1,32 @@
package org.koitharu.kotatsu.ui.search.global
import moxy.ktx.moxyPresenter
import org.koitharu.kotatsu.ui.list.MangaListFragment
import org.koitharu.kotatsu.utils.ext.withArgs
class GlobalSearchFragment: MangaListFragment<Unit>() {
private val presenter by moxyPresenter(factory = ::GlobalSearchPresenter)
private val query by stringArg(ARG_QUERY)
override fun onRequestMoreItems(offset: Int) {
if (offset == 0) {
presenter.startSearch(query.orEmpty())
}
}
override fun getTitle(): CharSequence? {
return query
}
companion object {
private const val ARG_QUERY = "query"
fun newInstance(query: String) = GlobalSearchFragment().withArgs(1) {
putString(ARG_QUERY, query)
}
}
}

View File

@@ -0,0 +1,49 @@
package org.koitharu.kotatsu.ui.search.global
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import moxy.presenterScope
import org.koitharu.kotatsu.domain.MangaSearchRepository
import org.koitharu.kotatsu.ui.common.BasePresenter
import org.koitharu.kotatsu.ui.list.MangaListView
import java.io.IOException
class GlobalSearchPresenter : BasePresenter<MangaListView<Unit>>() {
private lateinit var repository: MangaSearchRepository
override fun onFirstViewAttach() {
repository = MangaSearchRepository()
super.onFirstViewAttach()
}
fun startSearch(query: String) {
presenterScope.launch {
viewState.onLoadingStateChanged(isLoading = true)
var isFirstCall = true
repository.globalSearch(query)
.flowOn(Dispatchers.IO)
.catch { e ->
if (e is IOException) {
viewState.onError(e)
}
}.filterNot { x -> x.isEmpty() }
.onEmpty {
viewState.onListChanged(emptyList())
viewState.onLoadingStateChanged(isLoading = false)
}.onCompletion {
viewState.onListAppended(emptyList())
}.collect {
if (isFirstCall) {
isFirstCall = false
viewState.onListChanged(it)
viewState.onLoadingStateChanged(isLoading = false)
} else {
viewState.onListAppended(it)
}
}
}
}
}

View File

@@ -0,0 +1,128 @@
package org.koitharu.kotatsu.ui.settings
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.KoinComponent
import org.koin.core.inject
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.github.AppVersion
import org.koitharu.kotatsu.core.github.GithubRepository
import org.koitharu.kotatsu.core.github.VersionId
import org.koitharu.kotatsu.core.prefs.AppSettings
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 AppUpdateChecker(private val activity: ComponentActivity) : KoinComponent {
private val settings by inject<AppSettings>()
operator fun invoke() {
if (isUpdateSupported(activity) && settings.appUpdateAuto && settings.appUpdate + PERIOD < System.currentTimeMillis()) {
launch()
}
}
private fun launch() = activity.lifecycleScope.launch {
try {
val repo = GithubRepository()
val version = withContext(Dispatchers.IO) {
repo.getLatestVersion()
}
val newVersionId = VersionId.parse(version.name)
val currentVersionId = VersionId.parse(BuildConfig.VERSION_NAME)
if (newVersionId > currentVersionId) {
showUpdateDialog(version)
}
settings.appUpdate = System.currentTimeMillis()
} catch (_: CancellationException) {
} catch (e: Throwable) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
}
}
private fun showUpdateDialog(version: AppVersion) {
MaterialAlertDialogBuilder(activity)
.setTitle(R.string.app_update_available)
.setMessage(buildString {
append(activity.getString(R.string.new_version_s, version.name))
appendln()
append(activity.getString(R.string.size_s, FileSizeUtils.formatBytes(activity, version.apkSize)))
appendln()
appendln()
append(version.description)
})
.setPositiveButton(R.string.download) { _, _ ->
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(version.apkUrl)))
}
.setNegativeButton(R.string.close, null)
.create()
.show()
}
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 val PERIOD = TimeUnit.HOURS.toMillis(6)
fun isUpdateSupported(context: Context): Boolean {
return getCertificateSHA1Fingerprint(context) == CERT_SHA1
}
@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.encoded)
publicKey.byte2HexFormatted()
} catch (e: NoSuchAlgorithmException) {
e.printStackTrace()
null
} catch (e: CertificateEncodingException) {
e.printStackTrace()
null
}
}
}
}

View File

@@ -1,178 +0,0 @@
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
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.github.AppVersion
import org.koitharu.kotatsu.core.github.GithubRepository
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 {
launch(Dispatchers.IO) {
try {
val repo = GithubRepository()
val version = repo.getLatestVersion()
val newVersionId = VersionId.parse(version.name)
val currentVersionId = VersionId.parse(BuildConfig.VERSION_NAME)
if (newVersionId > currentVersionId) {
withContext(Dispatchers.Main) {
showUpdateNotification(version)
}
}
AppSettings(this@AppUpdateService).appUpdate = System.currentTimeMillis()
} catch (_: CancellationException) {
} catch (e: Throwable) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
} finally {
withContext(Dispatchers.Main) {
stopSelf(startId)
}
}
}
return START_NOT_STICKY
}
private fun showUpdateNotification(newVersion: AppVersion) {
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
&& manager.getNotificationChannel(CHANNEL_ID) == null
) {
val channel = NotificationChannel(
CHANNEL_ID,
getString(R.string.application_update),
NotificationManager.IMPORTANCE_DEFAULT
)
manager.createNotificationChannel(channel)
}
val builder = NotificationCompat.Builder(this, CHANNEL_ID)
builder.setContentTitle(getString(R.string.app_update_available))
builder.setContentText(buildString {
append(newVersion.name)
append(" (")
append(FileSizeUtils.formatBytes(this@AppUpdateService, newVersion.apkSize))
append(')')
})
builder.setContentIntent(
PendingIntent.getActivity(
this,
NOTIFICATION_ID,
Intent(Intent.ACTION_VIEW, Uri.parse(newVersion.url)),
PendingIntent.FLAG_CANCEL_CURRENT
)
)
builder.addAction(
R.drawable.ic_download, getString(R.string.download),
PendingIntent.getActivity(
this,
NOTIFICATION_ID + 1,
Intent(Intent.ACTION_VIEW, Uri.parse(newVersion.apkUrl)),
PendingIntent.FLAG_CANCEL_CURRENT
)
)
builder.setSmallIcon(R.drawable.ic_stat_update)
builder.setAutoCancel(true)
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 isUpdateSupported(context: Context): Boolean {
return getCertificateSHA1Fingerprint(context) == CERT_SHA1
}
fun startIfRequired(context: Context) {
if (!isUpdateSupported(context)) {
return
}
val settings = AppSettings(context)
if (settings.appUpdateAuto) {
val lastUpdate = settings.appUpdate
if (lastUpdate + PERIOD < System.currentTimeMillis()) {
start(context)
}
}
}
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.encoded)
publicKey.byte2HexFormatted()
} catch (e: NoSuchAlgorithmException) {
e.printStackTrace()
null
} catch (e: CertificateEncodingException) {
e.printStackTrace()
null
}
}
}
}

View File

@@ -9,6 +9,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.local.Cache import org.koitharu.kotatsu.core.local.Cache
import org.koitharu.kotatsu.domain.tracking.TrackingRepository
import org.koitharu.kotatsu.ui.common.BasePreferenceFragment import org.koitharu.kotatsu.ui.common.BasePreferenceFragment
import org.koitharu.kotatsu.ui.search.MangaSuggestionsProvider import org.koitharu.kotatsu.ui.search.MangaSuggestionsProvider
import org.koitharu.kotatsu.utils.CacheUtils import org.koitharu.kotatsu.utils.CacheUtils
@@ -17,6 +18,10 @@ import org.koitharu.kotatsu.utils.ext.getDisplayMessage
class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cache) { class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cache) {
private val trackerRepo by lazy {
TrackingRepository()
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_history) addPreferencesFromResource(R.xml.pref_history)
findPreference<Preference>(R.string.key_pages_cache_clear)?.let { pref -> findPreference<Preference>(R.string.key_pages_cache_clear)?.let { pref ->
@@ -39,10 +44,16 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
val items = MangaSuggestionsProvider.getItemsCount(p.context) val items = MangaSuggestionsProvider.getItemsCount(p.context)
p.summary = p.context.resources.getQuantityString(R.plurals.items, items, items) p.summary = p.context.resources.getQuantityString(R.plurals.items, items, items)
} }
findPreference<Preference>(R.string.key_updates_feed_clear)?.let { p ->
lifecycleScope.launchWhenResumed {
val items = trackerRepo.count()
p.summary = p.context.resources.getQuantityString(R.plurals.items, items, items)
}
}
} }
override fun onPreferenceTreeClick(preference: Preference): Boolean { override fun onPreferenceTreeClick(preference: Preference): Boolean {
return when(preference.key) { return when (preference.key) {
getString(R.string.key_pages_cache_clear) -> { getString(R.string.key_pages_cache_clear) -> {
clearCache(preference, Cache.PAGES) clearCache(preference, Cache.PAGES)
true true
@@ -53,8 +64,26 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
} }
getString(R.string.key_search_history_clear) -> { getString(R.string.key_search_history_clear) -> {
MangaSuggestionsProvider.clearHistory(preference.context) MangaSuggestionsProvider.clearHistory(preference.context)
preference.context.resources.getQuantityString(R.plurals.items, 0, 0) preference.summary = preference.context.resources
Snackbar.make(view ?: return true, R.string.search_history_cleared, Snackbar.LENGTH_SHORT).show() .getQuantityString(R.plurals.items, 0, 0)
Snackbar.make(
view ?: return true,
R.string.search_history_cleared,
Snackbar.LENGTH_SHORT
).show()
true
}
getString(R.string.key_updates_feed_clear) -> {
lifecycleScope.launch {
trackerRepo.clearLogs()
preference.summary = preference.context.resources
.getQuantityString(R.plurals.items, 0, 0)
Snackbar.make(
view ?: return@launch,
R.string.updates_feed_cleared,
Snackbar.LENGTH_SHORT
).show()
}
true true
} }
else -> super.onPreferenceTreeClick(preference) else -> super.onPreferenceTreeClick(preference)

View File

@@ -17,7 +17,7 @@ import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.ui.common.BasePreferenceFragment import org.koitharu.kotatsu.ui.common.BasePreferenceFragment
import org.koitharu.kotatsu.ui.common.dialog.StorageSelectDialog import org.koitharu.kotatsu.ui.common.dialog.StorageSelectDialog
import org.koitharu.kotatsu.ui.main.list.ListModeSelectDialog import org.koitharu.kotatsu.ui.list.ListModeSelectDialog
import org.koitharu.kotatsu.ui.settings.utils.MultiSummaryProvider import org.koitharu.kotatsu.ui.settings.utils.MultiSummaryProvider
import org.koitharu.kotatsu.ui.tracker.TrackWorker import org.koitharu.kotatsu.ui.tracker.TrackWorker
import org.koitharu.kotatsu.utils.ext.getStorageName import org.koitharu.kotatsu.utils.ext.getStorageName
@@ -42,7 +42,7 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
findPreference<MultiSelectListPreference>(R.string.key_reader_switchers)?.summaryProvider = findPreference<MultiSelectListPreference>(R.string.key_reader_switchers)?.summaryProvider =
MultiSummaryProvider(R.string.gestures_only) MultiSummaryProvider(R.string.gestures_only)
findPreference<Preference>(R.string.key_app_update_auto)?.run { findPreference<Preference>(R.string.key_app_update_auto)?.run {
isVisible = AppUpdateService.isUpdateSupported(context) isVisible = AppUpdateChecker.isUpdateSupported(context)
} }
findPreference<Preference>(R.string.key_local_storage)?.run { findPreference<Preference>(R.string.key_local_storage)?.run {
summary = settings.getStorageDir(context)?.getStorageName(context) summary = settings.getStorageDir(context)?.getStorageName(context)

View File

@@ -1,14 +1,11 @@
package org.koitharu.kotatsu.ui.settings package org.koitharu.kotatsu.ui.settings
import android.app.Activity
import android.content.Intent
import android.media.RingtoneManager import android.media.RingtoneManager
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.provider.Settings
import androidx.preference.Preference import androidx.preference.Preference
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.ui.common.BasePreferenceFragment import org.koitharu.kotatsu.ui.common.BasePreferenceFragment
import org.koitharu.kotatsu.ui.settings.utils.RingtonePickContract
import org.koitharu.kotatsu.utils.ext.toUriOrNull import org.koitharu.kotatsu.utils.ext.toUriOrNull
class NotificationSettingsLegacyFragment : BasePreferenceFragment(R.string.notifications) { class NotificationSettingsLegacyFragment : BasePreferenceFragment(R.string.notifications) {
@@ -24,43 +21,15 @@ class NotificationSettingsLegacyFragment : BasePreferenceFragment(R.string.notif
override fun onPreferenceTreeClick(preference: Preference?): Boolean { override fun onPreferenceTreeClick(preference: Preference?): Boolean {
return when (preference?.key) { return when (preference?.key) {
getString(R.string.key_notifications_sound) -> { getString(R.string.key_notifications_sound) -> {
val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER) registerForActivityResult(RingtonePickContract(preference.title.toString())) { uri ->
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, settings.notificationSound = uri?.toString().orEmpty()
RingtoneManager.TYPE_NOTIFICATION) findPreference<Preference>(R.string.key_notifications_sound)?.run {
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true) summary = RingtoneManager.getRingtone(context, uri).getTitle(context)
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true) }
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, }.launch(settings.notificationSound.toUriOrNull())
Settings.System.DEFAULT_NOTIFICATION_URI)
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, preference.title)
val existingValue = settings.notificationSound.toUriOrNull()
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, existingValue)
startActivityForResult(intent, REQUEST_RINGTONE)
true true
} }
else -> super.onPreferenceTreeClick(preference) else -> super.onPreferenceTreeClick(preference)
} }
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
REQUEST_RINGTONE -> {
if (resultCode == Activity.RESULT_OK) {
val uri =
data?.getParcelableExtra<Uri>(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
settings.notificationSound = uri?.toString().orEmpty()
findPreference<Preference>(R.string.key_notifications_sound)?.run {
summary = RingtoneManager.getRingtone(context, uri).getTitle(context)
}
}
}
else -> {
super.onActivityResult(requestCode, resultCode, data)
}
}
}
private companion object {
const val REQUEST_RINGTONE = 340
}
} }

View File

@@ -0,0 +1,34 @@
package org.koitharu.kotatsu.ui.settings.utils
import android.content.Context
import android.content.Intent
import android.media.RingtoneManager
import android.net.Uri
import android.provider.Settings
import androidx.activity.result.contract.ActivityResultContract
class RingtonePickContract(private val title: String?) : ActivityResultContract<Uri?, Uri?>() {
override fun createIntent(context: Context, input: Uri?): Intent {
val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER)
intent.putExtra(
RingtoneManager.EXTRA_RINGTONE_TYPE,
RingtoneManager.TYPE_NOTIFICATION
)
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true)
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true)
intent.putExtra(
RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI,
Settings.System.DEFAULT_NOTIFICATION_URI
)
if (title != null) {
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, title)
}
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, input)
return intent
}
override fun parseResult(resultCode: Int, intent: Intent?): Uri? {
return intent?.getParcelableExtra<Uri>(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
}
}

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