Save manga page image
This commit is contained in:
@@ -85,6 +85,8 @@ dependencies {
|
|||||||
implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0'
|
implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0'
|
||||||
implementation 'com.tomclaw.cache:cache:1.0'
|
implementation 'com.tomclaw.cache:cache:1.0'
|
||||||
|
|
||||||
|
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.2'
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.13'
|
testImplementation 'junit:junit:4.13'
|
||||||
testImplementation 'androidx.test:core:1.2.0'
|
testImplementation 'androidx.test:core:1.2.0'
|
||||||
testImplementation 'org.mockito:mockito-core:2.23.0'
|
testImplementation 'org.mockito:mockito-core:2.23.0'
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name="org.koitharu.kotatsu.KotatsuApp"
|
android:name="org.koitharu.kotatsu.KotatsuApp"
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
package org.koitharu.kotatsu.ui.common
|
package org.koitharu.kotatsu.ui.common
|
||||||
|
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.view.KeyEvent
|
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 androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
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.BuildConfig
|
||||||
@@ -11,6 +14,8 @@ import org.koitharu.kotatsu.R
|
|||||||
|
|
||||||
abstract class BaseActivity : MvpAppCompatActivity(), KoinComponent {
|
abstract class BaseActivity : MvpAppCompatActivity(), KoinComponent {
|
||||||
|
|
||||||
|
private var permissionCallback: ((Boolean) -> Unit)? = null
|
||||||
|
|
||||||
override fun setContentView(layoutResID: Int) {
|
override fun setContentView(layoutResID: Int) {
|
||||||
super.setContentView(layoutResID)
|
super.setContentView(layoutResID)
|
||||||
setupToolbar()
|
setupToolbar()
|
||||||
@@ -30,6 +35,33 @@ abstract class BaseActivity : MvpAppCompatActivity(), KoinComponent {
|
|||||||
true
|
true
|
||||||
} else super.onOptionsItemSelected(item)
|
} else super.onOptionsItemSelected(item)
|
||||||
|
|
||||||
|
fun requestPermission(permission: String, callback: (Boolean) -> Unit) {
|
||||||
|
if (ContextCompat.checkSelfPermission(
|
||||||
|
this,
|
||||||
|
permission
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
callback(true)
|
||||||
|
} else {
|
||||||
|
permissionCallback = callback
|
||||||
|
ActivityCompat.requestPermissions(this, arrayOf(permission), REQUEST_PERMISSION)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(
|
||||||
|
requestCode: Int,
|
||||||
|
permissions: Array<out String>,
|
||||||
|
grantResults: IntArray
|
||||||
|
) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
|
if (requestCode == REQUEST_PERMISSION) {
|
||||||
|
grantResults.singleOrNull()?.let {
|
||||||
|
permissionCallback?.invoke(it == PackageManager.PERMISSION_GRANTED)
|
||||||
|
}
|
||||||
|
permissionCallback = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||||
//TODO remove. Just for testing
|
//TODO remove. Just for testing
|
||||||
if (BuildConfig.DEBUG && keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
|
if (BuildConfig.DEBUG && keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
|
||||||
@@ -38,4 +70,9 @@ abstract class BaseActivity : MvpAppCompatActivity(), KoinComponent {
|
|||||||
}
|
}
|
||||||
return super.onKeyDown(keyCode, event)
|
return super.onKeyDown(keyCode, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
|
||||||
|
const val REQUEST_PERMISSION = 30
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,8 @@ abstract class BaseRecyclerAdapter<T, E>(private val onItemClickListener: OnRecy
|
|||||||
|
|
||||||
val items get() = dataSet.toImmutableList()
|
val items get() = dataSet.toImmutableList()
|
||||||
|
|
||||||
|
val hasItems get() = dataSet.isNotEmpty()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@Suppress("LeakingThis")
|
@Suppress("LeakingThis")
|
||||||
setHasStableIds(true)
|
setHasStableIds(true)
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
package org.koitharu.kotatsu.ui.reader
|
package org.koitharu.kotatsu.ui.reader
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.Gravity
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.widget.Button
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import kotlinx.android.synthetic.main.activity_reader.*
|
import kotlinx.android.synthetic.main.activity_reader.*
|
||||||
import moxy.ktx.moxyPresenter
|
import moxy.ktx.moxyPresenter
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
@@ -21,6 +24,7 @@ import org.koitharu.kotatsu.ui.common.BaseFullscreenActivity
|
|||||||
import org.koitharu.kotatsu.ui.reader.thumbnails.OnPageSelectListener
|
import org.koitharu.kotatsu.ui.reader.thumbnails.OnPageSelectListener
|
||||||
import org.koitharu.kotatsu.ui.reader.thumbnails.PagesThumbnailsSheet
|
import org.koitharu.kotatsu.ui.reader.thumbnails.PagesThumbnailsSheet
|
||||||
import org.koitharu.kotatsu.utils.GridTouchHelper
|
import org.koitharu.kotatsu.utils.GridTouchHelper
|
||||||
|
import org.koitharu.kotatsu.utils.ShareHelper
|
||||||
import org.koitharu.kotatsu.utils.anim.Motion
|
import org.koitharu.kotatsu.utils.anim.Motion
|
||||||
import org.koitharu.kotatsu.utils.ext.*
|
import org.koitharu.kotatsu.utils.ext.*
|
||||||
|
|
||||||
@@ -94,10 +98,26 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.action_pages_thumbs -> {
|
R.id.action_pages_thumbs -> {
|
||||||
PagesThumbnailsSheet.show(
|
if (adapter.hasItems) {
|
||||||
supportFragmentManager, adapter.items,
|
PagesThumbnailsSheet.show(
|
||||||
state.chapter?.name ?: title?.toString().orEmpty()
|
supportFragmentManager, adapter.items,
|
||||||
)
|
state.chapter?.name ?: title?.toString().orEmpty()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
showWaitWhileLoading()
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.action_save_page -> {
|
||||||
|
if (adapter.hasItems) {
|
||||||
|
requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) {
|
||||||
|
if (it) {
|
||||||
|
presenter.savePage(contentResolver, adapter.getItem(pager.currentItem))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showWaitWhileLoading()
|
||||||
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
@@ -117,6 +137,11 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
|
|||||||
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)
|
||||||
|
if (!adapter.hasItems) {
|
||||||
|
setOnDismissListener {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,8 +177,8 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
|
|||||||
) {
|
) {
|
||||||
false
|
false
|
||||||
} else {
|
} else {
|
||||||
val target = rootLayout.hitTest(rawX, rawY)
|
val targets = rootLayout.hitTest(rawX, rawY)
|
||||||
target !is Button
|
targets.none { it.hasOnClickListeners() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,6 +203,23 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPageSaved(uri: Uri?) {
|
||||||
|
if (uri != null) {
|
||||||
|
Snackbar.make(pager, R.string.page_saved, Snackbar.LENGTH_LONG)
|
||||||
|
.setAction(R.string.share) {
|
||||||
|
ShareHelper.shareImage(this, uri)
|
||||||
|
}.show()
|
||||||
|
} else {
|
||||||
|
Snackbar.make(pager, R.string.error_occurred, Snackbar.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showWaitWhileLoading() {
|
||||||
|
Toast.makeText(this, R.string.wait_for_loading_finish, Toast.LENGTH_SHORT).apply {
|
||||||
|
setGravity(Gravity.CENTER, 0, 0)
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val EXTRA_STATE = "state"
|
private const val EXTRA_STATE = "state"
|
||||||
|
|||||||
@@ -1,13 +1,23 @@
|
|||||||
package org.koitharu.kotatsu.ui.reader
|
package org.koitharu.kotatsu.ui.reader
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.webkit.URLUtil
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import moxy.InjectViewState
|
import moxy.InjectViewState
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
import org.koitharu.kotatsu.domain.history.HistoryRepository
|
import org.koitharu.kotatsu.core.model.MangaPage
|
||||||
import org.koitharu.kotatsu.domain.MangaProviderFactory
|
import org.koitharu.kotatsu.domain.MangaProviderFactory
|
||||||
|
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.utils.MediaStoreCompat
|
||||||
|
import org.koitharu.kotatsu.utils.ext.await
|
||||||
|
import org.koitharu.kotatsu.utils.ext.contentDisposition
|
||||||
|
import org.koitharu.kotatsu.utils.ext.mimeType
|
||||||
|
|
||||||
@InjectViewState
|
@InjectViewState
|
||||||
class ReaderPresenter : BasePresenter<ReaderView>() {
|
class ReaderPresenter : BasePresenter<ReaderView>() {
|
||||||
@@ -20,7 +30,7 @@ class ReaderPresenter : BasePresenter<ReaderView>() {
|
|||||||
val repo = MangaProviderFactory.create(state.manga.source)
|
val repo = MangaProviderFactory.create(state.manga.source)
|
||||||
val chapter = state.chapter ?: repo.getDetails(state.manga).chapters
|
val chapter = state.chapter ?: repo.getDetails(state.manga).chapters
|
||||||
?.first { it.id == state.chapterId }
|
?.first { it.id == state.chapterId }
|
||||||
?: throw RuntimeException("Chapter ${state.chapterId} not found")
|
?: throw RuntimeException("Chapter ${state.chapterId} not found")
|
||||||
repo.getPages(chapter)
|
repo.getPages(chapter)
|
||||||
}
|
}
|
||||||
viewState.onPagesReady(pages, state.page)
|
viewState.onPagesReady(pages, state.page)
|
||||||
@@ -45,4 +55,32 @@ class ReaderPresenter : BasePresenter<ReaderView>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun savePage(resolver: ContentResolver, page: MangaPage) {
|
||||||
|
launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val repo = MangaProviderFactory.create(page.source)
|
||||||
|
val url = repo.getPageFullUrl(page)
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.get()
|
||||||
|
.build()
|
||||||
|
val uri = getKoin().get<OkHttpClient>().newCall(request).await().use { response ->
|
||||||
|
val fileName =
|
||||||
|
URLUtil.guessFileName(url, response.contentDisposition, response.mimeType)
|
||||||
|
MediaStoreCompat.insertImage(resolver, fileName) {
|
||||||
|
response.body!!.byteStream().copyTo(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
viewState.onPageSaved(uri)
|
||||||
|
}
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
} catch (e: Exception) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
viewState.onPageSaved(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.koitharu.kotatsu.ui.reader
|
package org.koitharu.kotatsu.ui.reader
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import moxy.MvpView
|
import moxy.MvpView
|
||||||
import moxy.viewstate.strategy.AddToEndSingleStrategy
|
import moxy.viewstate.strategy.AddToEndSingleStrategy
|
||||||
import moxy.viewstate.strategy.OneExecutionStateStrategy
|
import moxy.viewstate.strategy.OneExecutionStateStrategy
|
||||||
@@ -16,4 +17,7 @@ interface ReaderView : MvpView {
|
|||||||
|
|
||||||
@StateStrategyType(OneExecutionStateStrategy::class)
|
@StateStrategyType(OneExecutionStateStrategy::class)
|
||||||
fun onError(e: Exception)
|
fun onError(e: Exception)
|
||||||
|
|
||||||
|
@StateStrategyType(OneExecutionStateStrategy::class)
|
||||||
|
fun onPageSaved(uri: Uri?)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package org.koitharu.kotatsu.utils
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
|
import java.io.OutputStream
|
||||||
|
|
||||||
|
object MediaStoreCompat {
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun insertImage(
|
||||||
|
resolver: ContentResolver,
|
||||||
|
fileName: String,
|
||||||
|
block: (OutputStream) -> Unit
|
||||||
|
): Uri? {
|
||||||
|
val name = fileName.substringBeforeLast('.')
|
||||||
|
val cv = ContentValues(7)
|
||||||
|
cv.put(MediaStore.Images.Media.DISPLAY_NAME, name)
|
||||||
|
cv.put(MediaStore.Images.Media.TITLE, name)
|
||||||
|
cv.put(
|
||||||
|
MediaStore.Images.Media.MIME_TYPE,
|
||||||
|
MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileName.substringAfterLast('.'))
|
||||||
|
)
|
||||||
|
cv.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis())
|
||||||
|
cv.put(MediaStore.Images.Media.DATE_MODIFIED, System.currentTimeMillis())
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
cv.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis())
|
||||||
|
cv.put(MediaStore.Images.Media.IS_PENDING, 1)
|
||||||
|
}
|
||||||
|
var uri: Uri? = null
|
||||||
|
try {
|
||||||
|
uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, cv)
|
||||||
|
resolver.openOutputStream(uri!!)?.use(block)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
cv.clear()
|
||||||
|
cv.put(MediaStore.Images.Media.IS_PENDING, 0)
|
||||||
|
resolver.update(uri, cv, null, null)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
uri?.let {
|
||||||
|
resolver.delete(it, null, null)
|
||||||
|
}
|
||||||
|
uri = null
|
||||||
|
}
|
||||||
|
return uri
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.utils
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
@@ -19,7 +20,8 @@ object ShareHelper {
|
|||||||
append("\n \n")
|
append("\n \n")
|
||||||
append(manga.url)
|
append(manga.url)
|
||||||
})
|
})
|
||||||
val shareIntent = Intent.createChooser(intent, context.getString(R.string.share_s, manga.title))
|
val shareIntent =
|
||||||
|
Intent.createChooser(intent, context.getString(R.string.share_s, manga.title))
|
||||||
context.startActivity(shareIntent)
|
context.startActivity(shareIntent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,7 +31,17 @@ object ShareHelper {
|
|||||||
val intent = Intent(Intent.ACTION_SEND)
|
val intent = Intent(Intent.ACTION_SEND)
|
||||||
intent.setDataAndType(uri, context.contentResolver.getType(uri))
|
intent.setDataAndType(uri, context.contentResolver.getType(uri))
|
||||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
val shareIntent = Intent.createChooser(intent, context.getString(R.string.share_s, file.name))
|
val shareIntent =
|
||||||
|
Intent.createChooser(intent, context.getString(R.string.share_s, file.name))
|
||||||
|
context.startActivity(shareIntent)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun shareImage(context: Context, uri: Uri) {
|
||||||
|
val intent = Intent(Intent.ACTION_SEND)
|
||||||
|
intent.setDataAndType(uri, context.contentResolver.getType(uri))
|
||||||
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
val shareIntent = Intent.createChooser(intent, context.getString(R.string.share_image))
|
||||||
context.startActivity(shareIntent)
|
context.startActivity(shareIntent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package org.koitharu.kotatsu.utils.ext
|
||||||
|
|
||||||
|
import okhttp3.Response
|
||||||
|
|
||||||
|
val Response.mimeType: String?
|
||||||
|
get() = body?.contentType()?.run { "$type/$subtype" }
|
||||||
|
|
||||||
|
val Response.contentDisposition: String?
|
||||||
|
get() = header("Content-Disposition")
|
||||||
@@ -12,6 +12,7 @@ import androidx.annotation.MenuRes
|
|||||||
import androidx.appcompat.widget.PopupMenu
|
import androidx.appcompat.widget.PopupMenu
|
||||||
import androidx.core.view.children
|
import androidx.core.view.children
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.postDelayed
|
import androidx.core.view.postDelayed
|
||||||
import androidx.drawerlayout.widget.DrawerLayout
|
import androidx.drawerlayout.widget.DrawerLayout
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
@@ -109,20 +110,21 @@ fun View.showPopupMenu(@MenuRes menuRes: Int, onPrepare:((Menu) -> Unit)? = null
|
|||||||
menu.show()
|
menu.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ViewGroup.hitTest(x: Int, y: Int): View? {
|
fun ViewGroup.hitTest(x: Int, y: Int): Set<View> {
|
||||||
|
val result = HashSet<View>(4)
|
||||||
val rect = Rect()
|
val rect = Rect()
|
||||||
for (child in children) {
|
for (child in children) {
|
||||||
if (child.getGlobalVisibleRect(rect)) {
|
if (child.isVisible && child.getGlobalVisibleRect(rect)) {
|
||||||
if (rect.contains(x, y)) {
|
if (rect.contains(x, y)) {
|
||||||
return if (child is ViewGroup) {
|
if (child is ViewGroup) {
|
||||||
child.hitTest(x, y)
|
result += child.hitTest(x, y)
|
||||||
} else {
|
} else {
|
||||||
child
|
result += child
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
fun View.hasGlobalPoint(x: Int, y: Int): Boolean {
|
fun View.hasGlobalPoint(x: Int, y: Int): Boolean {
|
||||||
|
|||||||
11
app/src/main/res/drawable/ic_page_image.xml
Normal file
11
app/src/main/res/drawable/ic_page_image.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<vector
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:tint="?attr/colorControlNormal"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#000"
|
||||||
|
android:pathData="M14,2L20,8V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2H14M18,20V9H13V4H6V20H18M17,13V19H7L12,14L14,16M10,10.5A1.5,1.5 0 0,1 8.5,12A1.5,1.5 0 0,1 7,10.5A1.5,1.5 0 0,1 8.5,9A1.5,1.5 0 0,1 10,10.5Z" />
|
||||||
|
</vector>
|
||||||
@@ -10,6 +10,12 @@
|
|||||||
android:visible="false"
|
android:visible="false"
|
||||||
app:showAsAction="ifRoom" />
|
app:showAsAction="ifRoom" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_save_page"
|
||||||
|
android:icon="@drawable/ic_page_image"
|
||||||
|
android:title="@string/save_page"
|
||||||
|
app:showAsAction="ifRoom" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_pages_thumbs"
|
android:id="@+id/action_pages_thumbs"
|
||||||
android:icon="@drawable/ic_grid"
|
android:icon="@drawable/ic_grid"
|
||||||
|
|||||||
@@ -65,4 +65,8 @@
|
|||||||
<string name="text_clear_history_prompt">Are you rally want to clear all your reading history? This action cannot be undone.</string>
|
<string name="text_clear_history_prompt">Are you rally want to clear all your reading history? This action cannot be undone.</string>
|
||||||
<string name="remove">Remove</string>
|
<string name="remove">Remove</string>
|
||||||
<string name="_s_removed_from_history">\"%s\" removed from history</string>
|
<string name="_s_removed_from_history">\"%s\" removed from history</string>
|
||||||
|
<string name="wait_for_loading_finish">Wait for the load to finish</string>
|
||||||
|
<string name="save_page">Save page</string>
|
||||||
|
<string name="page_saved">Page saved successful</string>
|
||||||
|
<string name="share_image">Share image</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user