Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Jay 2020-01-03 12:05:11 -08:00
commit 8d5037ce56
37 changed files with 238 additions and 173 deletions

24
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,24 @@
---
name: "🐞 Bug report"
about: Report a bug
title: "[Bug] Write short description here"
labels: "bug"
---
### Device information
* Tachiyomi version: ?
* Android version: ?
## Steps to reproduce
1. First step
2. Second step
### Expected behavior
This should happen.
### Actual behavior
This happened instead.
### Other details
Additional details and attachments.

View File

@ -0,0 +1,12 @@
---
name: "🌟 Feature request"
about: Suggest a feature to improve Tachiyomi
title: "[Feature Request] Write short description here"
labels: "feature"
---
### Why/User Benefit/User Problem
(explain why this feature should be added)
### What/Requirements
(explain how this feature would behave)

View File

@ -2,8 +2,8 @@ dist: trusty
language: android
android:
components:
- build-tools-28.0.3
- android-27
- build-tools-29.0.2
- android-28
- extra-android-m2repository
- extra-google-m2repository
- extra-android-support
@ -11,7 +11,7 @@ android:
licenses:
- android-sdk-license-.+
before_install:
- yes | sdkmanager "platforms;android-27" # workaround for accepting the license
- yes | sdkmanager "platforms;android-28" # workaround for accepting the license
- if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then
openssl aes-256-cbc -K $encrypted_e56be693d4fd_key -iv $encrypted_e56be693d4fd_iv -in "$PWD/.travis/secrets.tar.enc" -out secrets.tar -d;
tar xf secrets.tar;

View File

@ -11,8 +11,8 @@ Tachiyomi is a free and open source manga reader for Android.
Features of Tachiyomi include:
* Online reading from sources such as KissManga, MangaDex, [and more](https://github.com/inorichi/tachiyomi-extensions)
* Local reading of downloaded manga
* Configurable reader with multiple viewers, reading directions and other settings
* [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), and [Kitsu](https://kitsu.io/explore/anime) support
* A configurable reader with multiple viewers, reading directions and other settings.
* [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/explore/anime), and [Shikimori](https://shikimori.one) support
* Categories to organize your library
* Light and dark themes
* Schedule updating your library for new chapters

View File

@ -102,7 +102,6 @@ android {
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
@ -200,9 +199,6 @@ dependencies {
// Crash reports
implementation 'ch.acra:acra:4.9.2'
// Sort
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
// UI
implementation 'com.dmitrymalkovich.android:material-design-dimens:1.4'
implementation 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'

View File

@ -19,6 +19,7 @@
android:allowBackup="true"
android:fullBackupContent="@xml/backup_rules"
android:hardwareAccelerated="true"
android:usesCleartextTraffic="true"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"

View File

@ -28,7 +28,9 @@ class DownloadProvider(private val context: Context) {
* The root directory for downloads.
*/
private var downloadsDir = preferences.downloadsDirectory().getOrDefault().let {
UniFile.fromUri(context, Uri.parse(it))
val dir = UniFile.fromUri(context, Uri.parse(it))
DiskUtil.createNoMediaFile(dir, context)
dir
}
init {

View File

@ -132,7 +132,7 @@ class DownloadService : Service() {
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ state -> onNetworkStateChanged(state)
}, { _ ->
}, {
toast(R.string.download_queue_error)
stopSelf()
})

View File

@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList
import eu.kanade.tachiyomi.util.DiskUtil
import eu.kanade.tachiyomi.util.ImageUtil
import eu.kanade.tachiyomi.util.RetryWithDelay
import eu.kanade.tachiyomi.util.launchNow
@ -431,6 +432,8 @@ class Downloader(
if (download.status == Download.DOWNLOADED) {
tmpDir.renameTo(dirname)
cache.addChapter(dirname, mangaDir, download.manga)
DiskUtil.createNoMediaFile(tmpDir, context)
}
}

View File

@ -45,8 +45,8 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
prefs.edit().putString(key, value).apply()
}
override fun getStringSet(key: String?, defValues: MutableSet<String>?): MutableSet<String> {
return prefs.getStringSet(key, defValues)!!
override fun getStringSet(key: String?, defValues: MutableSet<String>?): MutableSet<String>? {
return prefs.getStringSet(key, defValues)
}
override fun putStringSet(key: String?, values: MutableSet<String>?) {

View File

@ -22,13 +22,15 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
private val jsonMime = "application/json; charset=utf-8".toMediaTypeOrNull()
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
fun addLibManga(track: Track): Observable<Track> {
val query = """
mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status)
{ id status } }
"""
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) {
| id
| status
|}
|}
|""".trimMargin()
val variables = jsonObject(
"mangaId" to track.media_id,
"progress" to track.last_chapter_read,
@ -59,14 +61,14 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
fun updateLibManga(track: Track): Observable<Track> {
val query = """
mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) {
SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) {
id
status
progress
}
}
"""
|mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) {
|SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) {
|id
|status
|progress
|}
|}
|""".trimMargin()
val variables = jsonObject(
"listId" to track.library_id,
"progress" to track.last_chapter_read,
@ -91,29 +93,29 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
fun search(search: String): Observable<List<TrackSearch>> {
val query = """
query Search(${'$'}query: String) {
Page (perPage: 50) {
media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
id
title {
romaji
}
coverImage {
large
}
type
status
chapters
description
startDate {
year
month
day
}
}
}
}
"""
|query Search(${'$'}query: String) {
|Page (perPage: 50) {
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
|id
|title {
|romaji
|}
|coverImage {
|large
|}
|type
|status
|chapters
|description
|startDate {
|year
|month
|day
|}
|}
|}
|}
|""".trimMargin()
val variables = jsonObject(
"query" to search
)
@ -143,37 +145,37 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
}
fun findLibManga(track: Track, userid: Int) : Observable<Track?> {
fun findLibManga(track: Track, userid: Int): Observable<Track?> {
val query = """
query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
Page {
mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
id
status
scoreRaw: score(format: POINT_100)
progress
media{
id
title {
romaji
}
coverImage {
large
}
type
status
chapters
description
startDate {
year
month
day
}
}
}
}
}
"""
|query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
|Page {
|mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
|id
|status
|scoreRaw: score(format: POINT_100)
|progress
|media {
|id
|title {
|romaji
|}
|coverImage {
|large
|}
|type
|status
|chapters
|description
|startDate {
|year
|month
|day
|}
|}
|}
|}
|}
|""".trimMargin()
val variables = jsonObject(
"id" to userid,
"manga_id" to track.media_id
@ -215,16 +217,15 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
fun getCurrentUser(): Observable<Pair<Int, String>> {
val query = """
query User
{
Viewer {
id
mediaListOptions {
scoreFormat
}
}
}
"""
|query User {
|Viewer {
|id
|mediaListOptions {
|scoreFormat
|}
|}
|}
|""".trimMargin()
val payload = jsonObject(
"query" to query
)
@ -247,7 +248,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
}
}
fun jsonToALManga(struct: JsonObject): ALManga{
private fun jsonToALManga(struct: JsonObject): ALManga {
val date = try {
val date = Calendar.getInstance()
date.set(struct["startDate"]["year"].nullInt ?: 0, (struct["startDate"]["month"].nullInt ?: 0) - 1,
@ -262,11 +263,10 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
date, struct["chapters"].nullInt ?: 0)
}
fun jsonToALUserManga(struct: JsonObject): ALUserManga{
return ALUserManga(struct["id"].asLong, struct["status"].asString, struct["scoreRaw"].asInt, struct["progress"].asInt, jsonToALManga(struct["media"].obj) )
private fun jsonToALUserManga(struct: JsonObject): ALUserManga {
return ALUserManga(struct["id"].asLong, struct["status"].asString, struct["scoreRaw"].asInt, struct["progress"].asInt, jsonToALManga(struct["media"].obj))
}
companion object {
private const val clientId = "385"
private const val clientUrl = "tachiyomi://anilist-auth"

View File

@ -18,13 +18,12 @@ class KitsuSearchManga(obj: JsonObject) {
private val synopsis by obj.byString
private var startDate = obj.get("startDate").nullString?.let {
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
outputDf.format(Date(it!!.toLong() * 1000))
outputDf.format(Date(it.toLong() * 1000))
}
private val endDate = obj.get("endDate").nullString
@CallSuper
open fun toTrack() = TrackSearch.create(TrackManager.KITSU).apply {
fun toTrack() = TrackSearch.create(TrackManager.KITSU).apply {
media_id = this@KitsuSearchManga.id
title = canonicalTitle
total_chapters = chapterCount ?: 0
@ -55,7 +54,7 @@ class KitsuLibManga(obj: JsonObject, manga: JsonObject) {
private val ratingTwenty = obj["attributes"].obj.get("ratingTwenty").nullString
val progress by obj["attributes"].byInt
open fun toTrack() = TrackSearch.create(TrackManager.KITSU).apply {
fun toTrack() = TrackSearch.create(TrackManager.KITSU).apply {
media_id = libraryId
title = canonicalTitle
total_chapters = chapterCount ?: 0

View File

@ -113,11 +113,7 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
.toCompletable()
}
// Attempt to login again if cookies have been cleared but credentials are still filled
fun ensureLoggedIn() {
if (isAuthorized) return
if (!isLogged) throw Exception("MAL Login Credentials not found")
fun refreshLogin() {
val username = getUsername()
val password = getPassword()
logout()
@ -132,6 +128,14 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
}
}
// Attempt to login again if cookies have been cleared but credentials are still filled
fun ensureLoggedIn() {
if (isAuthorized) return
if (!isLogged) throw Exception("MAL Login Credentials not found")
refreshLogin()
}
override fun logout() {
super.logout()
preferences.trackToken(this).delete()

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.data.track.myanimelist
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.Response
import okio.Buffer
@ -11,18 +12,27 @@ class MyAnimeListInterceptor(private val myanimelist: Myanimelist): Interceptor
override fun intercept(chain: Interceptor.Chain): Response {
myanimelist.ensureLoggedIn()
var request = chain.request()
request.body?.let {
val request = chain.request()
var response = chain.proceed(updateRequest(request))
if (response.code == 400){
myanimelist.refreshLogin()
response = chain.proceed(updateRequest(request))
}
return response
}
private fun updateRequest(request: Request): Request {
return request.body?.let {
val contentType = it.contentType().toString()
val updatedBody = when {
contentType.contains("x-www-form-urlencoded") -> updateFormBody(it)
contentType.contains("json") -> updateJsonBody(it)
else -> it
}
request = request.newBuilder().post(updatedBody).build()
}
return chain.proceed(request)
request.newBuilder().post(updatedBody).build()
} ?: request
}
private fun bodyToString(requestBody: RequestBody): String {

View File

@ -7,6 +7,10 @@ import android.content.IntentFilter
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult
import eu.kanade.tachiyomi.util.launchNow
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
/**
* Broadcast receiver that listens for the system's packages installed, updated or removed, and only
@ -90,7 +94,7 @@ internal class ExtensionInstallReceiver(private val listener: Listener) :
private suspend fun getExtensionFromIntent(context: Context, intent: Intent?): LoadResult {
val pkgName = getPackageNameFromIntent(intent) ?:
return LoadResult.Error("Package name not found")
return ExtensionLoader.loadExtensionFromPkgName(context, pkgName)
return GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT, { ExtensionLoader.loadExtensionFromPkgName(context, pkgName) }).await()
}
/**

View File

@ -2,20 +2,24 @@ package eu.kanade.tachiyomi.source
import android.content.Context
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.*
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.ChapterRecognition
import eu.kanade.tachiyomi.util.ComparatorUtil.CaseInsensitiveNaturalComparator
import eu.kanade.tachiyomi.util.DiskUtil
import eu.kanade.tachiyomi.util.EpubFile
import eu.kanade.tachiyomi.util.ImageUtil
import junrar.Archive
import junrar.rarfile.FileHeader
import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator
import rx.Observable
import timber.log.Timber
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.util.Comparator
import java.util.Locale
import java.util.concurrent.TimeUnit
import java.util.zip.ZipEntry
@ -125,7 +129,6 @@ class LocalSource(private val context: Context) : CatalogueSource {
override fun fetchMangaDetails(manga: SManga) = Observable.just(manga)
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
val comparator = CaseInsensitiveSimpleNaturalComparator.getInstance<String>()
val chapters = getBaseDirectories(context)
.mapNotNull { File(it, manga.url).listFiles()?.toList() }
.flatten()
@ -146,7 +149,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
}
.sortedWith(Comparator { c1, c2 ->
val c = c2.chapter_number.compareTo(c1.chapter_number)
if (c == 0) comparator.compare(c2.name, c1.name) else c
if (c == 0) CaseInsensitiveNaturalComparator.compare(c2.name, c1.name) else c
})
return Observable.just(chapters)
@ -189,20 +192,19 @@ class LocalSource(private val context: Context) : CatalogueSource {
private fun updateCover(chapter: SChapter, manga: SManga): File? {
val format = getFormat(chapter)
val comparator = CaseInsensitiveSimpleNaturalComparator.getInstance<String>()
return when (format) {
is Format.Directory -> {
val entry = format.file.listFiles()
.sortedWith(Comparator<File> { f1, f2 -> comparator.compare(f1.name, f2.name) })
.find { !it.isDirectory && ImageUtil.isImage(it.name, { FileInputStream(it) }) }
.sortedWith(Comparator<File> { f1, f2 -> CaseInsensitiveNaturalComparator.compare(f1.name, f2.name) })
.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
entry?.let { updateCover(context, manga, it.inputStream())}
}
is Format.Zip -> {
ZipFile(format.file).use { zip ->
val entry = zip.entries().toList()
.sortedWith(Comparator<ZipEntry> { f1, f2 -> comparator.compare(f1.name, f2.name) })
.find { !it.isDirectory && ImageUtil.isImage(it.name, { zip.getInputStream(it) }) }
.sortedWith(Comparator<ZipEntry> { f1, f2 -> CaseInsensitiveNaturalComparator.compare(f1.name, f2.name) })
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
entry?.let { updateCover(context, manga, zip.getInputStream(it) )}
}
@ -210,8 +212,8 @@ class LocalSource(private val context: Context) : CatalogueSource {
is Format.Rar -> {
Archive(format.file).use { archive ->
val entry = archive.fileHeaders
.sortedWith(Comparator<FileHeader> { f1, f2 -> comparator.compare(f1.fileNameString, f2.fileNameString) })
.find { !it.isDirectory && ImageUtil.isImage(it.fileNameString, { archive.getInputStream(it) }) }
.sortedWith(Comparator<FileHeader> { f1, f2 -> CaseInsensitiveNaturalComparator.compare(f1.fileNameString, f2.fileNameString) })
.find { !it.isDirectory && ImageUtil.isImage(it.fileNameString) { archive.getInputStream(it) } }
entry?.let { updateCover(context, manga, archive.getInputStream(it) )}
}

View File

@ -11,6 +11,9 @@ import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.f2prateek.rx.preferences.Preference
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
@ -207,10 +210,11 @@ open class BrowseCatalogueController(bundle: Bundle) :
}
val recycler = if (presenter.isListMode) {
androidx.recyclerview.widget.RecyclerView(view.context).apply {
RecyclerView(view.context).apply {
id = R.id.recycler
layoutManager = androidx.recyclerview.widget.LinearLayoutManager(context)
addItemDecoration(androidx.recyclerview.widget.DividerItemDecoration(context, androidx.recyclerview.widget.DividerItemDecoration.VERTICAL))
layoutManager = LinearLayoutManager(context)
layoutParams = RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
}
} else {
(catalogue_view.inflate(R.layout.catalogue_recycler_autofit) as AutofitRecyclerView).apply {
@ -377,9 +381,8 @@ open class BrowseCatalogueController(bundle: Bundle) :
adapter.onLoadMoreComplete(null)
hideProgressBar()
val message = if (error is NoResultsException) "No results found" else (error.message ?: "")
snack?.dismiss()
val message = if (error is NoResultsException) catalogue_view.context.getString(R.string.no_results_found) else (error.message ?: "")
snack = catalouge_layout?.snack(message, Snackbar.LENGTH_INDEFINITE) {
setAction(R.string.action_retry) {
// If not the first page, show bottom progress bar.
@ -388,8 +391,8 @@ open class BrowseCatalogueController(bundle: Bundle) :
adapter.addScrollableFooterWithDelay(item, 0, true)
} else {
showProgressBar()
}
presenter.requestNext()
}
}
}
}

View File

@ -28,7 +28,7 @@ class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs:
val view = inflate(R.layout.catalogue_drawer_content)
((view as ViewGroup).getChildAt(1) as ViewGroup).addView(recycler)
addView(view)
title.text = context?.getString(R.string.source_search_options)
title.text = context.getString(R.string.source_search_options)
search_btn.setOnClickListener { onSearchClicked() }
reset_btn.setOnClickListener { onResetClicked() }
}

View File

@ -19,8 +19,8 @@ class CatalogueSearchAdapter(val controller: CatalogueSearchController) :
*/
private var bundle = Bundle()
override fun onBindViewHolder(holder: androidx.recyclerview.widget.RecyclerView.ViewHolder, position: Int) {
super.onBindViewHolder(holder, position)
override fun onBindViewHolder(holder: androidx.recyclerview.widget.RecyclerView.ViewHolder, position: Int, payloads: List<Any?>) {
super.onBindViewHolder(holder, position, payloads)
restoreHolderState(holder)
}

View File

@ -213,7 +213,6 @@ class MangaController : RxController, TabbedController {
}
companion object {
const val FROM_CATALOGUE_EXTRA = "from_catalogue"
const val MANGA_EXTRA = "manga"
@ -225,5 +224,4 @@ class MangaController : RxController, TabbedController {
.apply { isAccessible = true }
}
}

View File

@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import kotlinx.android.synthetic.main.catalogue_main_controller_card.*
import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.items.IFlexible
import kotlinx.android.synthetic.main.catalogue_main_controller_card.title
/**
* Item that contains the selection header.
@ -38,7 +39,7 @@ class SelectionHeader : AbstractHeaderItem<SelectionHeader.Holder>() {
class Holder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>) : BaseFlexibleViewHolder(view, adapter) {
init {
title.text = "Please select a source to migrate from"
title.text = view.context.getString(R.string.migration_selection_prompt)
}
}

View File

@ -507,7 +507,7 @@ class ReaderPresenter(
.observeOn(AndroidSchedulers.mainThread())
.subscribeFirst(
{ view, file -> view.onShareImageResult(file) },
{ view, error -> /* Empty */ }
{ _, _ -> /* Empty */ }
)
}

View File

@ -2,8 +2,8 @@ package eu.kanade.tachiyomi.ui.reader.loader
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.util.ComparatorUtil.CaseInsensitiveNaturalComparator
import eu.kanade.tachiyomi.util.ImageUtil
import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator
import rx.Observable
import java.io.File
import java.io.FileInputStream
@ -18,11 +18,9 @@ class DirectoryPageLoader(val file: File) : PageLoader() {
* comparator.
*/
override fun getPages(): Observable<List<ReaderPage>> {
val comparator = CaseInsensitiveSimpleNaturalComparator.getInstance<String>()
return file.listFiles()
.filter { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
.sortedWith(Comparator<File> { f1, f2 -> comparator.compare(f1.name, f2.name) })
.sortedWith(Comparator<File> { f1, f2 -> CaseInsensitiveNaturalComparator.compare(f1.name, f2.name) })
.mapIndexed { i, file ->
val streamFn = { FileInputStream(file) }
ReaderPage(i).apply {

View File

@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.io.InputStream
/**
* Loader used to load a chapter from the downloaded chapters.

View File

@ -2,10 +2,10 @@ package eu.kanade.tachiyomi.ui.reader.loader
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.util.ComparatorUtil.CaseInsensitiveNaturalComparator
import eu.kanade.tachiyomi.util.ImageUtil
import junrar.Archive
import junrar.rarfile.FileHeader
import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator
import rx.Observable
import java.io.File
import java.io.InputStream
@ -42,11 +42,9 @@ class RarPageLoader(file: File) : PageLoader() {
* comparator.
*/
override fun getPages(): Observable<List<ReaderPage>> {
val comparator = CaseInsensitiveSimpleNaturalComparator.getInstance<String>()
return archive.fileHeaders
.filter { !it.isDirectory && ImageUtil.isImage(it.fileNameString) { archive.getInputStream(it) } }
.sortedWith(Comparator<FileHeader> { f1, f2 -> comparator.compare(f1.fileNameString, f2.fileNameString) })
.sortedWith(Comparator<FileHeader> { f1, f2 -> CaseInsensitiveNaturalComparator.compare(f1.fileNameString, f2.fileNameString) })
.mapIndexed { i, header ->
val streamFn = { getStream(header) }

View File

@ -2,8 +2,8 @@ package eu.kanade.tachiyomi.ui.reader.loader
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.util.ComparatorUtil.CaseInsensitiveNaturalComparator
import eu.kanade.tachiyomi.util.ImageUtil
import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator
import rx.Observable
import java.io.File
import java.util.zip.ZipEntry
@ -32,11 +32,9 @@ class ZipPageLoader(file: File) : PageLoader() {
* comparator.
*/
override fun getPages(): Observable<List<ReaderPage>> {
val comparator = CaseInsensitiveSimpleNaturalComparator.getInstance<String>()
return zip.entries().toList()
.filter { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
.sortedWith(Comparator<ZipEntry> { f1, f2 -> comparator.compare(f1.name, f2.name) })
.sortedWith(Comparator<ZipEntry> { f1, f2 -> CaseInsensitiveNaturalComparator.compare(f1.name, f2.name) })
.mapIndexed { i, entry ->
val streamFn = { zip.getInputStream(entry) }
ReaderPage(i).apply {

View File

@ -50,6 +50,7 @@ class PagerTransitionHolder(
private var textView = TextView(context).apply {
//if (Build.VERSION.SDK_INT >= 23)
//setTextColor(context.getResourceColor(R.attr.))
textSize = 17.5F
wrapContent()
}

View File

@ -101,7 +101,7 @@ class WebtoonViewer(val activity: ReaderActivity) : BaseViewer {
recycler.longTapListener = f@ { event ->
if (activity.menuVisible || config.longTapEnabled) {
val child = recycler.findChildViewUnder(event.x, event.y)
if(child != null) {
if (child != null) {
val position = recycler.getChildAdapterPosition(child)
val item = adapter.items.getOrNull(position)
if (item is ReaderPage) {

View File

@ -17,7 +17,6 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.util.DiskUtil
import eu.kanade.tachiyomi.util.getFilePicker
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -45,15 +44,6 @@ class SettingsDownloadController : SettingsController() {
.subscribeUntilDestroy { path ->
val dir = UniFile.fromUri(context, Uri.parse(path))
summary = dir.filePath ?: path
// Don't display downloaded chapters in gallery apps creating .nomedia
if (dir != null && dir.exists()) {
val nomedia = dir.findFile(".nomedia")
if (nomedia == null) {
dir.createFile(".nomedia")
applicationContext?.let { DiskUtil.scanMedia(it, dir.uri) }
}
}
}
}
switchPreference {

View File

@ -91,7 +91,7 @@ object ChapterRecognition {
* @param chapter chapter object
* @return true if volume is found
*/
fun updateChapter(match: MatchResult?, chapter: SChapter): Boolean {
private fun updateChapter(match: MatchResult?, chapter: SChapter): Boolean {
match?.let {
val initial = it.groups[1]?.value?.toFloat()!!
val subChapterDecimal = it.groups[2]?.value
@ -109,12 +109,12 @@ object ChapterRecognition {
* @param alpha alpha value of regex
* @return decimal/alpha float value
*/
fun checkForDecimal(decimal: String?, alpha: String?): Float {
private fun checkForDecimal(decimal: String?, alpha: String?): Float {
if (!decimal.isNullOrEmpty())
return decimal?.toFloat()!!
return decimal.toFloat()
if (!alpha.isNullOrEmpty()) {
if (alpha!!.contains("extra"))
if (alpha.contains("extra"))
return .99f
if (alpha.contains("omake"))
@ -138,7 +138,7 @@ object ChapterRecognition {
* x.a -> x.1, x.b -> x.2, etc
*/
private fun parseAlphaPostFix(alpha: Char): Float {
return ("0." + Integer.toString(alpha.toInt() - 96)).toFloat()
return ("0." + (alpha.toInt() - 96).toString()).toFloat()
}
}

View File

@ -0,0 +1,5 @@
package eu.kanade.tachiyomi.util
object ComparatorUtil {
val CaseInsensitiveNaturalComparator = compareBy<String, String>(String.CASE_INSENSITIVE_ORDER) { it }.then(naturalOrder())
}

View File

@ -172,11 +172,11 @@ fun Context.isServiceRunning(serviceClass: Class<*>): Boolean {
*/
fun Context.openInBrowser(url: String) {
try {
val url = Uri.parse(url)
val parsedUrl = Uri.parse(url)
val intent = CustomTabsIntent.Builder()
.setToolbarColor(getResourceColor(R.attr.colorPrimary))
.build()
intent.launchUrl(this, url)
intent.launchUrl(this, parsedUrl)
} catch (e: Exception) {
toast(e.message)
}

View File

@ -9,7 +9,7 @@ import kotlinx.coroutines.launch
import kotlin.coroutines.EmptyCoroutineContext
fun launchUI(block: suspend CoroutineScope.() -> Unit): Job =
GlobalScope.launch(Dispatchers.Main,CoroutineStart.DEFAULT,block)
GlobalScope.launch(Dispatchers.Main, CoroutineStart.DEFAULT, block)
fun launchNow(block: suspend CoroutineScope.() -> Unit): Job =
GlobalScope.launch(Dispatchers.Main,CoroutineStart.UNDISPATCHED,block)
GlobalScope.launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED, block)

View File

@ -7,6 +7,7 @@ import android.os.Build
import android.os.Environment
import androidx.core.content.ContextCompat
import androidx.core.os.EnvironmentCompat
import com.hippo.unifile.UniFile
import java.io.File
object DiskUtil {
@ -54,6 +55,19 @@ object DiskUtil {
return directories
}
/**
* Don't display downloaded chapters in gallery apps creating `.nomedia`.
*/
fun createNoMediaFile(dir: UniFile?, context: Context?) {
if (dir != null && dir.exists()) {
val nomedia = dir.findFile(".nomedia")
if (nomedia == null) {
dir.createFile(".nomedia")
context?.let { scanMedia(it, dir.uri) }
}
}
}
/**
* Scans the given file so that it can be shown in gallery apps, for example.
*/

View File

@ -343,6 +343,7 @@
<string name="select_source">Select a source</string>
<string name="no_valid_sources">Please enable at least one valid source</string>
<string name="no_more_results">No more results</string>
<string name="no_results_found">No results found</string>
<string name="local_source">Local manga</string>
<string name="other_source">Other</string>
<string name="invalid_combination">Default can\'t be selected with other categories</string>
@ -477,6 +478,7 @@
<!-- Source migration screen -->
<string name="migration_info">Tap to select the source to migrate from</string>
<string name="migration_dialog_what_to_include">Select data to include</string>
<string name="migration_selection_prompt">Select a source to migrate from</string>
<string name="select">Select</string>
<string name="migrate">Migrate</string>
<string name="copy">Copy</string>

View File

@ -136,8 +136,9 @@
<style name="Theme.Widget" />
<style name="Theme.Widget.FAB">
<item name="android:layout_height">@dimen/fab_size</item>
<item name="android:layout_width">@dimen/fab_size</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_width">wrap_content</item>
<item name="fabCustomSize">@dimen/fab_size</item>
<item name="android:layout_gravity">bottom|end</item>
<item name="android:layout_margin">@dimen/fab_margin</item>
<item name="android:scaleType">fitCenter</item>

View File

@ -10,7 +10,7 @@ buildscript {
classpath 'com.android.tools.build:gradle:3.5.3'
classpath 'com.github.ben-manes:gradle-versions-plugin:0.22.0'
classpath 'com.github.zellius:android-shortcut-gradle-plugin:0.1.2'
classpath 'com.google.gms:google-services:4.3.2'
classpath 'com.google.gms:google-services:4.3.3'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}