diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ca07c95dfa..c9aaa32fd1 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -86,9 +86,9 @@
-
-
+
+
+
{
+ shareImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION))
+ context.notificationManager.cancel(intent.getIntExtra(NOTIFICATION_ID, 5))
+ }
+ ACTION_SHOW_IMAGE ->
+ showImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION))
+ ACTION_DELETE_IMAGE -> {
+ deleteImage(intent.getStringExtra(EXTRA_FILE_LOCATION))
+ context.notificationManager.cancel(intent.getIntExtra(NOTIFICATION_ID, 5))
+ }
+ }
+ }
+
+ fun deleteImage(path: String) {
+ val file = File(path)
+ if (file.exists()) file.delete()
+ }
+
+ fun shareImage(context: Context, path: String) {
+ val shareIntent = Intent().apply {
+ action = Intent.ACTION_SEND
+ putExtra(Intent.EXTRA_STREAM, Uri.parse(path))
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ type = "image/jpeg"
+ }
+ context.startActivity(Intent.createChooser(shareIntent, context.resources.getText(R.string.action_share))
+ .apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK })
+ }
+
+ fun showImage(context: Context, path: String) {
+ val intent = Intent().apply {
+ action = Intent.ACTION_VIEW
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK
+ setDataAndType(Uri.parse("file://" + path), "image/*")
+ }
+ context.startActivity(intent)
+ }
+
+ companion object {
+ const val ACTION_SHARE_IMAGE = "eu.kanade.SHARE_IMAGE"
+
+ const val ACTION_SHOW_IMAGE = "eu.kanade.SHOW_IMAGE"
+
+ const val ACTION_DELETE_IMAGE = "eu.kanade.DELETE_IMAGE"
+
+ const val EXTRA_FILE_LOCATION = "file_location"
+
+ const val NOTIFICATION_ID = "notification_id"
+
+ fun shareImageIntent(context: Context, path: String, notificationId: Int): PendingIntent {
+ val intent = Intent(context, ImageNotificationReceiver::class.java).apply {
+ action = ACTION_SHARE_IMAGE
+ putExtra(EXTRA_FILE_LOCATION, path)
+ putExtra(NOTIFICATION_ID, notificationId)
+ }
+ return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ fun showImageIntent(context: Context, path: String): PendingIntent {
+ val intent = Intent(context, ImageNotificationReceiver::class.java).apply {
+ action = ACTION_SHOW_IMAGE
+ putExtra(EXTRA_FILE_LOCATION, path)
+ }
+ return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ fun deleteImageIntent(context: Context, path: String, notificationId: Int): PendingIntent {
+ val intent = Intent(context, ImageNotificationReceiver::class.java).apply {
+ action = ACTION_DELETE_IMAGE
+ putExtra(EXTRA_FILE_LOCATION, path)
+ putExtra(NOTIFICATION_ID, notificationId)
+ }
+ return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/ImageNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/ImageNotifier.kt
new file mode 100644
index 0000000000..dbd4a5cecf
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/ImageNotifier.kt
@@ -0,0 +1,124 @@
+package eu.kanade.tachiyomi.data.download
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.support.v4.app.NotificationCompat
+import eu.kanade.tachiyomi.Constants
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.util.notificationManager
+import java.io.File
+
+
+class ImageNotifier(private val context: Context) {
+ /**
+ * Notification builder.
+ */
+ private val notificationBuilder = NotificationCompat.Builder(context)
+
+ /**
+ * Id of the notification.
+ */
+ private val notificationId: Int
+ get() = Constants.NOTIFICATION_DOWNLOAD_IMAGE_ID
+
+ /**
+ * Status of download. Used for correct notification icon.
+ */
+ private var isDownloading = false
+
+ /**
+ * Called when download progress changes.
+ * @param progress progress value in range [0,100]
+ */
+ fun onProgressChange(progress: Int) {
+ with(notificationBuilder) {
+ if (!isDownloading) {
+ setContentTitle(context.getString(R.string.saving_picture))
+ setSmallIcon(android.R.drawable.stat_sys_download)
+ setLargeIcon(null)
+ setStyle(null)
+ // Clear old actions if they exist
+ if (!mActions.isEmpty())
+ mActions.clear()
+ isDownloading = true
+ }
+
+ setProgress(100, progress, false)
+ }
+ // Displays the progress bar on notification
+ context.notificationManager.notify(notificationId, notificationBuilder.build())
+ }
+
+ /**
+ * Called when image download is complete
+ * @param bitmap image file containing downloaded page image
+ */
+ fun onComplete(bitmap: Bitmap, file: File) {
+ with(notificationBuilder) {
+ if (isDownloading) {
+ setProgress(0, 0, false)
+ isDownloading = false
+ }
+ setContentTitle(context.getString(R.string.picture_saved))
+ setSmallIcon(R.drawable.ic_insert_photo_black_24dp)
+ setLargeIcon(bitmap)
+ setStyle(NotificationCompat.BigPictureStyle().bigPicture(bitmap))
+ setAutoCancel(true)
+
+ // Clear old actions if they exist
+ if (!mActions.isEmpty())
+ mActions.clear()
+
+ setContentIntent(ImageNotificationReceiver.showImageIntent(context, file.absolutePath))
+ // Share action
+ addAction(R.drawable.ic_share_white_24dp,
+ context.getString(R.string.action_share),
+ ImageNotificationReceiver.shareImageIntent(context, file.absolutePath, notificationId))
+ // Delete action
+ addAction(R.drawable.ic_delete_white_24dp,
+ context.getString(R.string.action_delete),
+ ImageNotificationReceiver.deleteImageIntent(context, file.absolutePath, notificationId))
+ }
+ // Displays the progress bar on notification
+ context.notificationManager.notify(notificationId, notificationBuilder.build())
+ }
+
+ fun onComplete(file: File) {
+ onComplete(convertToBitmap(file), file)
+ }
+
+ /**
+ * Clears the notification message
+ */
+ internal fun onClear() {
+ context.notificationManager.cancel(notificationId)
+ }
+
+
+ /**
+ * Called on error while downloading image
+ * @param error string containing error information
+ */
+ internal fun onError(error: String?) {
+ // Create notification
+ with(notificationBuilder) {
+ setContentTitle(context.getString(R.string.download_notifier_title_error))
+ setContentText(error ?: context.getString(R.string.download_notifier_unkown_error))
+ setSmallIcon(android.R.drawable.ic_menu_report_image)
+ setProgress(0, 0, false)
+ }
+ context.notificationManager.notify(notificationId, notificationBuilder.build())
+ isDownloading = false
+ }
+
+ /**
+ * Converts file to bitmap
+ */
+ fun convertToBitmap(image: File): Bitmap {
+ val options = BitmapFactory.Options()
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888
+ return BitmapFactory.decodeFile(image.absolutePath, options)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.kt
index 2f67fd5fb9..9cf1221982 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.kt
@@ -184,10 +184,9 @@ class MangaInfoFragment : BaseRxFragment() {
val url = source.mangaDetailsRequest(presenter.manga).url().toString()
val sharingIntent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
- putExtra(android.content.Intent.EXTRA_SUBJECT, presenter.manga.title)
putExtra(android.content.Intent.EXTRA_TEXT, resources.getString(R.string.share_text, presenter.manga.title, url))
}
- startActivity(Intent.createChooser(sharingIntent, resources.getText(R.string.share_subject)))
+ startActivity(Intent.createChooser(sharingIntent, resources.getText(R.string.action_share)))
} catch (e: Exception) {
context.toast(e.message)
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
index 0b935c8735..4c1c64a780 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
@@ -145,6 +145,8 @@ class ReaderActivity : BaseRxActivity() {
when (item.itemId) {
R.id.action_settings -> ReaderSettingsDialog().show(supportFragmentManager, "settings")
R.id.action_custom_filter -> ReaderCustomFilterDialog().show(supportFragmentManager, "filter")
+ R.id.action_save_page -> presenter.savePage()
+ R.id.action_set_as_cover -> presenter.setCover()
else -> return super.onOptionsItemSelected(item)
}
return true
@@ -393,16 +395,16 @@ class ReaderActivity : BaseRxActivity() {
private fun setRotation(rotation: Int) {
when (rotation) {
- // Rotation free
+ // Rotation free
1 -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
- // Lock in current rotation
+ // Lock in current rotation
2 -> {
val currentOrientation = resources.configuration.orientation
setRotation(if (currentOrientation == Configuration.ORIENTATION_PORTRAIT) 3 else 4)
}
- // Lock in portrait
+ // Lock in portrait
3 -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
- // Lock in landscape
+ // Lock in landscape
4 -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
}
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt
index 69cd6dff15..448e1fc0c5 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt
@@ -1,15 +1,23 @@
package eu.kanade.tachiyomi.ui.reader
import android.os.Bundle
+import android.os.Environment
+import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.ChapterCache
+import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaSync
import eu.kanade.tachiyomi.data.download.DownloadManager
+import eu.kanade.tachiyomi.data.download.ImageNotifier
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
import eu.kanade.tachiyomi.data.mangasync.UpdateMangaSyncService
+import eu.kanade.tachiyomi.data.network.GET
+import eu.kanade.tachiyomi.data.network.NetworkHelper
+import eu.kanade.tachiyomi.data.network.ProgressListener
+import eu.kanade.tachiyomi.data.network.newCallWithProgress
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.source.SourceManager
import eu.kanade.tachiyomi.data.source.model.Page
@@ -17,6 +25,8 @@ import eu.kanade.tachiyomi.data.source.online.OnlineSource
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.RetryWithDelay
import eu.kanade.tachiyomi.util.SharedData
+import eu.kanade.tachiyomi.util.saveTo
+import eu.kanade.tachiyomi.util.toast
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
@@ -24,6 +34,8 @@ import rx.schedulers.Schedulers
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
import java.io.File
+import java.io.IOException
+import java.io.InputStream
import java.util.*
/**
@@ -31,6 +43,11 @@ import java.util.*
*/
class ReaderPresenter : BasePresenter() {
+ /**
+ * Network helper
+ */
+ private val network: NetworkHelper by injectLazy()
+
/**
* Preferences.
*/
@@ -61,6 +78,11 @@ class ReaderPresenter : BasePresenter() {
*/
val chapterCache: ChapterCache by injectLazy()
+ /**
+ * Cover cache.
+ */
+ val coverCache: CoverCache by injectLazy()
+
/**
* Manga being read.
*/
@@ -88,6 +110,20 @@ class ReaderPresenter : BasePresenter() {
*/
private val source by lazy { sourceManager.get(manga.source)!! }
+ /**
+ *
+ */
+ val imageNotifier by lazy { ImageNotifier(context) }
+
+ /**
+ * Directory of pictures
+ */
+ private val pictureDirectory: String by lazy {
+ Environment.getExternalStorageDirectory().absolutePath + File.separator +
+ Environment.DIRECTORY_PICTURES + File.separator +
+ context.getString(R.string.app_name) + File.separator
+ }
+
/**
* Chapter list for the active manga. It's retrieved lazily and should be accessed for the first
* time in a background thread to avoid blocking the UI.
@@ -365,7 +401,9 @@ class ReaderPresenter : BasePresenter() {
val removeAfterReadSlots = prefs.removeAfterReadSlots()
when (removeAfterReadSlots) {
// Setting disabled
- -1 -> { /**Empty function**/ }
+ -1 -> {
+ /**Empty function**/
+ }
// Remove current read chapter
0 -> deleteChapter(chapter, manga)
// Remove previous chapter specified by user in settings.
@@ -384,8 +422,8 @@ class ReaderPresenter : BasePresenter() {
Timber.e(error)
}
}
- .subscribeOn(Schedulers.io())
- .subscribe()
+ .subscribeOn(Schedulers.io())
+ .subscribe()
}
/**
@@ -508,4 +546,87 @@ class ReaderPresenter : BasePresenter() {
db.insertManga(manga).executeAsBlocking()
}
+ /**
+ * Update cover with page file.
+ */
+ internal fun setCover() {
+ chapter.pages?.get(chapter.last_page_read)?.let {
+ // Update cover to selected file, show error if something went wrong
+ try {
+ if (editCoverWithStream(File(it.imagePath).inputStream(), manga)) {
+ context.toast(R.string.cover_updated)
+ } else {
+ throw Exception("Stream copy failed")
+ }
+ } catch(e: Exception) {
+ context.toast(R.string.notification_manga_update_failed)
+ Timber.e(e.message)
+ }
+ }
+ }
+
+ /**
+ * Called to copy image to cache
+ * @param inputStream the new cover.
+ * @param manga the manga edited.
+ * @return true if the cover is updated, false otherwise
+ */
+ @Throws(IOException::class)
+ private fun editCoverWithStream(inputStream: InputStream, manga: Manga): Boolean {
+ if (manga.thumbnail_url != null && manga.favorite) {
+ coverCache.copyToCache(manga.thumbnail_url!!, inputStream)
+ return true
+ }
+ return false
+ }
+
+ /**
+ * Save page to local storage
+ * @throws IOException
+ */
+ @Throws(IOException::class)
+ internal fun savePage() {
+ chapter.pages?.get(chapter.last_page_read)?.let { page ->
+ // File where the image will be saved
+ val destFile = File(pictureDirectory, manga.title + " - " + chapter.name +
+ " - " + downloadManager.getImageFilename(page))
+
+ if (destFile.exists()) {
+ imageNotifier.onComplete(destFile)
+ } else {
+ // Progress of the download
+ var savedProgress = 0
+
+ val progressListener = object : ProgressListener {
+ override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
+ val progress = (100 * bytesRead / contentLength).toInt()
+ if (progress > savedProgress) {
+ savedProgress = progress
+ imageNotifier.onProgressChange(progress)
+ }
+ }
+ }
+
+ // Download and save the image.
+ Observable.fromCallable { ->
+ network.client.newCallWithProgress(GET(page.imageUrl!!), progressListener).execute()
+ }.map {
+ response ->
+ if (response.isSuccessful) {
+ response.body().source().saveTo(destFile)
+ imageNotifier.onComplete(destFile)
+ } else {
+ response.close()
+ throw Exception("Unsuccessful response")
+ }
+ }
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribeOn(Schedulers.io())
+ .subscribe({}, { error ->
+ Timber.e(error.message)
+ imageNotifier.onError(error.message)
+ })
+ }
+ }
+ }
}
diff --git a/app/src/main/res/drawable/ic_insert_photo_black_24dp.xml b/app/src/main/res/drawable/ic_insert_photo_black_24dp.xml
new file mode 100644
index 0000000000..7c62bb3073
--- /dev/null
+++ b/app/src/main/res/drawable/ic_insert_photo_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 91b4af0914..642b4c9028 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -223,7 +223,6 @@
Status
Source
Genres
- Share…
Check out %1$s! at %2$s
Circular icon
Rounded icon
@@ -267,10 +266,18 @@
Status
Chapters
+
+ Custom filter
+ Download page
+ Set as cover
+ Cover updated
This will remove the read date of this chapter. Are you sure?
Reset all chapters for this manga
+
+ Picture saved
+ Saving picture
Custom filter