Compare commits

..
Author SHA1 Message Date
tarsin ce823e9b20 Release 1.8.6.2
Releases / Build (push) Has been cancelled
2023-02-20 19:06:59 +08:00
FooIbar 0caa5b1a3b Fix 509 detection false positive 2023-02-20 19:06:13 +08:00
FooIbar 9edea5d584 Clean up unfinished download files 2023-02-20 18:48:49 +08:00
FooIbar 9a773aeb39 Workaround for del tag parsing in comments 2023-02-20 18:48:40 +08:00
FooIbar 9fd3196bb1 Use initializer instead of getter for coroutineContext 2023-02-20 18:48:34 +08:00
FooIbar 7580572874 Temporarily disable vpn bypass on login 2023-02-20 18:48:27 +08:00
tarsin 06acc620f4 Resolve #842 2023-02-20 18:48:19 +08:00
FooIbar 367358d277 Resolve #789 2023-02-20 18:48:13 +08:00
tarsin d61df599e8 Release 1.8.6.1
Releases / Build (push) Has been cancelled
2023-02-18 10:38:33 +08:00
tarsin 989a0a01c1 Fix quicksearch progress 2023-02-18 10:37:57 +08:00
tarsin 12108645e9 Fix a stupid memleak 2023-02-18 10:37:45 +08:00
FooIbar 4910ec194d Fix search keyword in tag mode 2023-02-18 10:37:29 +08:00
FooIbar f730f43945 Fix comment last edited time parsing 2023-02-18 10:37:14 +08:00
tarsin 6e7e668d39 Release 1.8.6.0
Releases / Build (push) Has been cancelled
2023-02-17 11:39:05 +08:00
tarsin 2d086a5c6e Resolve #820 2023-02-17 11:36:36 +08:00
renovate[bot] f7e592eac0 Update plugin com.android.application to v8.0.0-beta03 2023-02-17 10:39:32 +08:00
11 changed files with 123 additions and 107 deletions

No files matched your search

+2 -2
View File
@@ -34,8 +34,8 @@ android {
applicationId = "moe.tarsin.ehviewer"
minSdk = 28
targetSdk = 33
versionCode = 180031
versionName = "1.8.6.0-beta01"
versionCode = 180034
versionName = "1.8.6.2"
resourceConfigurations.addAll(
listOf(
"zh",
+1 -1
View File
@@ -36,7 +36,7 @@
<activity
android:name=".ui.login.LoginActivity"
android:exported="false"
android:theme="@style/AppTheme"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"/>
<activity
android:name=".ui.MainActivity"
@@ -91,7 +91,8 @@ object GalleryDetailParser {
// Error info
PATTERN_ERROR.find(body)?.run { throw EhException(groupValues[1]) }
val galleryDetail = GalleryDetail()
val document = Jsoup.parse(body)
// Temporary workaround, see https://github.com/jhy/jsoup/issues/1850
val document = Jsoup.parse(body.replace("del>", "s>"))
parseDetail(galleryDetail, document, body)
galleryDetail.tags = parseTagGroups(document)
galleryDetail.comments = parseComments(document)
@@ -376,15 +377,15 @@ object GalleryDetailParser {
}
// time
val c3 = JsoupUtils.getElementByClass(element, "c3")
var temp = c3!!.ownText()
if (temp.endsWith(':')) {
temp = temp.substring("Posted on ".length, temp.length - " by:".length)
val temp = c3!!.ownText()
val time = if (temp.endsWith(':')) {
// user
comment.user = c3.child(0).text()
temp.substring("Posted on ".length, temp.length - " by:".length)
} else {
temp = temp.substring("Posted on ".length)
temp.substring("Posted on ".length)
}
comment.time = Instant.from(WEB_COMMENT_DATE_FORMAT.parse(temp)).toEpochMilli()
comment.time = Instant.from(WEB_COMMENT_DATE_FORMAT.parse(time)).toEpochMilli()
// comment
val c6 = JsoupUtils.getElementByClass(element, "c6")
// fix underline support
@@ -407,7 +408,7 @@ object GalleryDetailParser {
val e = c8.children().first()
if (e != null) {
comment.lastEdited =
Instant.from(WEB_COMMENT_DATE_FORMAT.parse(temp)).toEpochMilli()
Instant.from(WEB_COMMENT_DATE_FORMAT.parse(e.text())).toEpochMilli()
}
}
comment
@@ -36,6 +36,7 @@ import com.hippo.yorozuya.FileUtils
import com.hippo.yorozuya.MathUtils
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import moe.tarsin.coroutines.runSuspendCatching
import okhttp3.executeAsync
import okio.buffer
import java.io.File
@@ -194,7 +195,7 @@ class SpiderDen(private val mGalleryInfo: GalleryInfo) {
return receivedSize
}
findDownloadFileForIndex(index, extension)?.runCatching {
findDownloadFileForIndex(index, extension)?.runSuspendCatching {
openOutputStream().use { outputStream ->
(outputStream as FileOutputStream).channel.use { return doSave(it) == length }
}
@@ -208,7 +209,7 @@ class SpiderDen(private val mGalleryInfo: GalleryInfo) {
val key = EhCacheKeyFactory.getImageKey(mGid, index)
val editor = sCache.edit(key) ?: return false
var received: Long = 0
runCatching {
runSuspendCatching {
editor.metadata.toFile().writeText(extension)
editor.data.toFile().outputStream().use { outputStream ->
outputStream.channel.use { received = doSave(it) }
@@ -365,7 +365,7 @@ public final class SpiderQueen implements Runnable {
request(index, false);
}
private int getPageState(int index) {
public int getPageState(int index) {
synchronized (mPageStateLock) {
if (mPageStateArray != null && index >= 0 && index < mPageStateArray.length) {
return mPageStateArray[index];
@@ -12,9 +12,10 @@ import com.hippo.ehviewer.spider.SpiderQueen.NETWORK_ERROR
import com.hippo.ehviewer.spider.SpiderQueen.PTOKEN_FAILED_MESSAGE
import com.hippo.ehviewer.spider.SpiderQueen.STATE_FAILED
import com.hippo.ehviewer.spider.SpiderQueen.STATE_FINISHED
import com.hippo.ehviewer.spider.SpiderQueen.STATE_DOWNLOADING
import com.hippo.image.Image.Companion.decode
import com.hippo.util.ExceptionUtils
import com.hippo.yorozuya.StringUtils
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -27,7 +28,6 @@ import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.sync.withPermit
import moe.tarsin.coroutines.runSuspendCatching
import kotlin.coroutines.CoroutineContext
class SpiderQueenWorker(private val queen: SpiderQueen) : CoroutineScope {
private val spiderDen
@@ -42,8 +42,7 @@ class SpiderQueenWorker(private val queen: SpiderQueen) : CoroutineScope {
private var isDownloadMode = false
private val size
get() = queen.size()
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + Job()
override val coroutineContext = Dispatchers.IO + Job()
fun cancelDecode(index: Int) {
decoder.cancel(index)
@@ -80,6 +79,15 @@ class SpiderQueenWorker(private val queen: SpiderQueen) : CoroutineScope {
mSemaphore.withPermit {
doInJob(index, force)
}
}.apply {
invokeOnCompletion {
if (queen.getPageState(index) == STATE_DOWNLOADING) {
Log.d(DEBUG_TAG, "Download image cancelled $index")
launchNonCancellable {
spiderDen.remove(index)
}
}
}
}
}
}
@@ -100,7 +108,7 @@ class SpiderQueenWorker(private val queen: SpiderQueen) : CoroutineScope {
?: queen.getPTokenFromInternet(index)
?: queen.getPTokenFromMultiPageViewer(index)
}
queen.updatePageState(index, SpiderQueen.STATE_DOWNLOADING)
queen.updatePageState(index, STATE_DOWNLOADING)
if (!force && index in spiderDen) {
return queen.updatePageState(index, STATE_FINISHED)
}
@@ -149,7 +157,7 @@ class SpiderQueenWorker(private val queen: SpiderQueen) : CoroutineScope {
}
runSuspendCatching {
getGalleryPage(pageUrl, spiderInfo.gid, spiderInfo.token).also {
if (StringUtils.endsWith(it.imageUrl, URL_509_SUFFIX_ARRAY)) {
if (check509(it.imageUrl)) {
// Get 509
queen.notifyGet509(index)
error = ERROR_509
@@ -200,7 +208,7 @@ class SpiderQueenWorker(private val queen: SpiderQueen) : CoroutineScope {
localShowKey,
previousPToken
).also {
if (StringUtils.endsWith(it.imageUrl, URL_509_SUFFIX_ARRAY)) {
if (check509(it.imageUrl)) {
// Get 509
queen.notifyGet509(index)
error = ERROR_509
@@ -270,6 +278,7 @@ class SpiderQueenWorker(private val queen: SpiderQueen) : CoroutineScope {
}.onFailure {
it.printStackTrace()
error = NETWORK_ERROR
forceHtml = true
}
Log.d(DEBUG_TAG, "End download image $index")
}
@@ -278,10 +287,13 @@ class SpiderQueenWorker(private val queen: SpiderQueen) : CoroutineScope {
}
companion object {
private val URL_509_SUFFIX_ARRAY = arrayOf(
"/509.gif",
"/509s.gif"
private val URL_509_ARRAY = arrayOf(
"https://ehgt.org/g/509.gif",
"https://ehgt.org/g/509s.gif",
"https://exhentai.org/img/509.gif",
"https://exhentai.org/img/509s.gif"
)
private fun check509(url: String) = url in URL_509_ARRAY
private const val DEBUG_TAG = "SpiderQueenWorker"
}
@@ -89,8 +89,8 @@ import java.io.OutputStream
class MainActivity : EhActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var navController: NavController
private lateinit var connectivityManager: ConnectivityManager
private val availableNetworks: MutableList<Network> = mutableListOf()
private val connectivityManager by lazy { getSystemService<ConnectivityManager>()!! }
private val availableNetworks = mutableListOf<Network>()
private fun saveImageToTempFile(uri: Uri): File? {
val src = ImageDecoder.createSource(contentResolver, uri)
@@ -223,7 +223,6 @@ class MainActivity : EhActivity() {
}
override fun onCreate(savedInstanceState: Bundle?) {
connectivityManager = getSystemService()!!
if (Settings.getDF() && Settings.getBypassVpn()) {
bypassVpn()
}
@@ -233,13 +232,7 @@ class MainActivity : EhActivity() {
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
navController = navHostFragment.navController
if (EhUtils.needSignedIn()) {
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
setNavGraph()
}.launch(Intent(this, LoginActivity::class.java))
} else {
setNavGraph()
}
if (!EhUtils.needSignedIn()) setNavGraph()
binding.drawView.addDrawerListener(mDrawerOnBackPressedCallback)
binding.navView.setupWithNavController(navController)
@@ -371,7 +364,19 @@ class MainActivity : EhActivity() {
}
}
private val loginLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (availableNetworks.isNotEmpty()) {
connectivityManager.bindProcessToNetwork(availableNetworks.last())
}
setNavGraph()
}
override fun onResume() {
if (EhUtils.needSignedIn()) {
connectivityManager.bindProcessToNetwork(null)
loginLauncher.launch(Intent(this, LoginActivity::class.java))
}
super.onResume()
lifecycleScope.launch {
delay(100)
@@ -74,7 +74,9 @@ import com.hippo.ehviewer.client.EhRequest
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.data.ListUrlBuilder
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_NORMAL
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_SUBSCRIPTION
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_TAG
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_TOPLIST
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_WHATS_HOT
import com.hippo.ehviewer.client.exception.EhException
@@ -98,13 +100,14 @@ import com.hippo.widget.FabLayout
import com.hippo.widget.FabLayout.OnClickFabListener
import com.hippo.widget.FabLayout.OnExpandListener
import com.hippo.yorozuya.AnimationUtils
import com.hippo.yorozuya.MathUtils
import com.hippo.yorozuya.SimpleAnimatorListener
import com.hippo.yorozuya.StringUtils
import com.hippo.yorozuya.ViewUtils
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.delay
import rikka.core.res.resolveColor
import java.io.File
import java.time.LocalDateTime
@@ -216,6 +219,13 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
else -> throw IllegalStateException("Wrong KEY_ACTION:${args?.getString(KEY_ACTION)} when handle args!")
}
val goto = args?.getString(KEY_GOTO)
goto?.let {
lifecycleScope.launchUI {
delay(100)
mHelper?.goTo(it, true)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
@@ -288,25 +298,6 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
})
}
private fun wrapTagKeyword(keyword: String?): String {
var keyword = keyword
keyword = keyword!!.trim { it <= ' ' }
val index1 = keyword.indexOf(':')
if (index1 == -1 || index1 >= keyword.length - 1) {
// Can't find :, or : is the last char
return keyword
}
if (keyword[index1 + 1] == '"') {
// The char after : is ", the word must be quoted
return keyword
}
val index2 = keyword.indexOf(' ')
return if (index2 <= index1) {
// Can't find space, or space is before :
keyword
} else keyword.substring(0, index1 + 1) + "\"" + keyword.substring(index1 + 1) + "$\""
}
// Update search bar title, drawer checked item
private fun onUpdateUrlBuilder() {
_binding ?: return
@@ -332,8 +323,8 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
// Update search edit text
if (!mIsTopList) {
if (mode == ListUrlBuilder.MODE_TAG) {
keyword = wrapTagKeyword(keyword)
if (mode == MODE_TAG) {
keyword = wrapTagKeyword(keyword!!)
}
setSearchBarText(keyword)
}
@@ -546,9 +537,10 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
val qsDrawerAdapter = QsDrawerAdapter(inflater)
qsDrawerAdapter.setHasStableIds(true)
drawerBinding.recyclerViewDrawer.adapter = qsDrawerAdapter
val itemTouchHelper = ItemTouchHelper(GalleryListQSItemTouchHelperCallback(qsDrawerAdapter))
itemTouchHelper.attachToRecyclerView(drawerBinding.recyclerViewDrawer)
if (!mIsTopList) {
val itemTouchHelper =
ItemTouchHelper(GalleryListQSItemTouchHelperCallback(qsDrawerAdapter))
itemTouchHelper.attachToRecyclerView(drawerBinding.recyclerViewDrawer)
drawerBinding.tip.visibility = View.VISIBLE
drawerBinding.recyclerViewDrawer.visibility = View.GONE
}
@@ -995,7 +987,7 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
val oldMode = mUrlBuilder.mode
// If it's MODE_SUBSCRIPTION, keep it
val newMode =
if (oldMode == MODE_SUBSCRIPTION) MODE_SUBSCRIPTION else ListUrlBuilder.MODE_NORMAL
if (oldMode == MODE_SUBSCRIPTION) MODE_SUBSCRIPTION else MODE_NORMAL
mUrlBuilder.reset()
mUrlBuilder.mode = newMode
mUrlBuilder.keyword = query
@@ -1155,8 +1147,7 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
val goto = if (i != -1) q.name!!.substring(i + 1) else null
val args = ListUrlBuilder().apply {
set(q)
setIndex(goto)
}.toStartArgs()
}.toStartArgs().apply { putString(KEY_GOTO, goto) }
navigate(R.id.galleryListScene, args, true)
setState(STATE_NORMAL)
closeDrawer(GravityCompat.END)
@@ -1431,6 +1422,7 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
companion object {
const val KEY_ACTION = "action"
const val KEY_GOTO = "goto"
const val ACTION_HOMEPAGE = "action_homepage"
const val ACTION_SUBSCRIPTION = "action_subscription"
const val ACTION_WHATS_HOT = "action_whats_hot"
@@ -1444,38 +1436,6 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
private const val STATE_SEARCH = 2
private const val STATE_SEARCH_SHOW_LIST = 3
private const val ANIMATE_TIME = 300L
private fun getSuitableTitleForUrlBuilder(
resources: Resources, urlBuilder: ListUrlBuilder, appName: Boolean
): String? {
val keyword = urlBuilder.keyword
val category = urlBuilder.category
return if (ListUrlBuilder.MODE_NORMAL == urlBuilder.mode && EhUtils.NONE == category &&
TextUtils.isEmpty(keyword) && urlBuilder.advanceSearch == -1 && urlBuilder.minRating == -1 && urlBuilder.pageFrom == -1 && urlBuilder.pageTo == -1
) {
resources.getString(if (appName) R.string.app_name else R.string.homepage)
} else if (MODE_SUBSCRIPTION == urlBuilder.mode && EhUtils.NONE == category &&
TextUtils.isEmpty(keyword) && urlBuilder.advanceSearch == -1 && urlBuilder.minRating == -1 && urlBuilder.pageFrom == -1 && urlBuilder.pageTo == -1
) {
resources.getString(R.string.subscription)
} else if (MODE_WHATS_HOT == urlBuilder.mode) {
resources.getString(R.string.whats_hot)
} else if (MODE_TOPLIST == urlBuilder.mode) {
when (urlBuilder.keyword) {
"11" -> resources.getString(R.string.toplist_alltime)
"12" -> resources.getString(R.string.toplist_pastyear)
"13" -> resources.getString(R.string.toplist_pastmonth)
"15" -> resources.getString(R.string.toplist_yesterday)
else -> null
}
} else if (!TextUtils.isEmpty(keyword)) {
keyword
} else if (MathUtils.hammingWeight(category) == 1) {
EhUtils.getCategory(category)
} else {
null
}
}
fun ListUrlBuilder.toStartArgs(): Bundle {
val args = Bundle()
args.putString(KEY_ACTION, ACTION_LIST_URL_BUILDER)
@@ -1483,4 +1443,40 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
return args
}
}
}
private fun getSuitableTitleForUrlBuilder(
resources: Resources, urlBuilder: ListUrlBuilder, appName: Boolean
): String? {
val keyword = urlBuilder.keyword
val category = urlBuilder.category
val mode = urlBuilder.mode
return if (mode == MODE_WHATS_HOT) {
resources.getString(R.string.whats_hot)
} else if (!keyword.isNullOrEmpty()) {
when (mode) {
MODE_TOPLIST -> {
when (keyword) {
"11" -> resources.getString(R.string.toplist_alltime)
"12" -> resources.getString(R.string.toplist_pastyear)
"13" -> resources.getString(R.string.toplist_pastmonth)
"15" -> resources.getString(R.string.toplist_yesterday)
else -> null
}
}
MODE_TAG -> wrapTagKeyword(keyword)
else -> keyword
}
} else if (category == EhUtils.NONE && urlBuilder.advanceSearch == -1) {
when (mode) {
MODE_NORMAL -> resources.getString(if (appName) R.string.app_name else R.string.homepage)
MODE_SUBSCRIPTION -> resources.getString(R.string.subscription)
else -> null
}
} else if (category.countOneBits() == 1) {
EhUtils.getCategory(category)
} else {
null
}
}
@@ -127,6 +127,7 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
fun setSearchBarText(text: String?) {
binding.toolbar.text = text
binding.searchview.setText(text)
}
fun setEditTextHint(hint: String?) {
@@ -168,18 +169,6 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
mAllowEmptySearch = allowEmptySearch
}
private fun wrapTagKeyword(keyword: String): String {
return if (keyword.endsWith(':')) {
keyword
} else if (keyword.contains(' ')) {
val tag = keyword.substringAfter(':')
val prefix = keyword.dropLast(tag.length)
"$prefix\"$tag$\" "
} else {
"$keyword$ "
}
}
interface SuggestionProvider {
fun providerSuggestions(text: String): List<Suggestion>?
@@ -268,7 +257,7 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
edittext.let {
val keywords = it.text.toString().substringBeforeLast(' ', "")
val keyword = wrapTagKeyword(mKeyword)
val newKeywords = if (keywords.isNotEmpty()) "$keywords $keyword" else keyword
val newKeywords = if (keywords.isNotEmpty()) "$keywords $keyword " else "$keyword "
it.setText(newKeywords)
it.setSelection(newKeywords.length)
}
@@ -370,3 +359,15 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
}
}
}
fun wrapTagKeyword(keyword: String): String {
return if (keyword.endsWith(':')) {
keyword
} else if (keyword.contains(' ')) {
val tag = keyword.substringAfter(':')
val prefix = keyword.dropLast(tag.length)
"$prefix\"$tag$\""
} else {
"$keyword$"
}
}
+1 -1
View File
@@ -56,8 +56,8 @@ class Image private constructor(private val src: CloseableSource) {
(mObtainedDrawable as? AnimatedImageDrawable)?.stop()
(mObtainedDrawable as? BitmapDrawable)?.bitmap?.recycle()
mObtainedDrawable?.callback = null
mObtainedDrawable = null
(mObtainedDrawable as? AnimatedImageDrawable)?.let { src.close() }
mObtainedDrawable = null
}
companion object {
+1 -1
View File
@@ -1,5 +1,5 @@
plugins {
id("com.android.application") version "8.0.0-beta02" apply false
id("com.android.application") version "8.0.0-beta03" apply false
kotlin("android") version "1.8.10" apply false
kotlin("plugin.serialization") version "1.8.10" apply false
id("com.google.devtools.ksp") version "1.8.10-1.0.9" apply false