فهرست منبع

Added option to download page or set page as cover

Bram van de Kerkhof 8 سال پیش
والد
کامیت
2991906a85

+ 3 - 3
app/src/main/AndroidManifest.xml

@@ -86,9 +86,9 @@
 
         <receiver android:name=".data.updater.UpdateNotificationReceiver"/>
 
-        <receiver
-            android:name=".data.library.LibraryUpdateService$CancelUpdateReceiver">
-        </receiver>
+        <receiver android:name=".data.library.LibraryUpdateService$CancelUpdateReceiver" />
+
+        <receiver android:name=".data.download.ImageNotificationReceiver" />
 
         <meta-data
             android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"

+ 1 - 0
app/src/main/java/eu/kanade/tachiyomi/Constants.kt

@@ -5,4 +5,5 @@ object Constants {
     const val NOTIFICATION_UPDATER_ID = 2
     const val NOTIFICATION_DOWNLOAD_CHAPTER_ID = 3
     const val NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID = 4
+    const val NOTIFICATION_DOWNLOAD_IMAGE_ID = 5
 }

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt

@@ -297,7 +297,7 @@ class DownloadManager(
     }
 
     // Get the filename for an image given the page
-    private fun getImageFilename(page: Page): String {
+    fun getImageFilename(page: Page): String {
         val url = page.imageUrl
         val number = String.format("%03d", page.pageNumber + 1)
 

+ 90 - 0
app/src/main/java/eu/kanade/tachiyomi/data/download/ImageNotificationReceiver.kt

@@ -0,0 +1,90 @@
+package eu.kanade.tachiyomi.data.download
+
+import android.app.PendingIntent
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.util.notificationManager
+import java.io.File
+
+class ImageNotificationReceiver : BroadcastReceiver() {
+    override fun onReceive(context: Context, intent: Intent) {
+        when (intent.action) {
+            ACTION_SHARE_IMAGE -> {
+                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)
+        }
+    }
+}

+ 124 - 0
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)
+    }
+
+}

+ 1 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.kt

@@ -184,10 +184,9 @@ class MangaInfoFragment : BaseRxFragment<MangaInfoPresenter>() {
             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)
         }

+ 6 - 4
app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt

@@ -145,6 +145,8 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
         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<ReaderPresenter>() {
 
     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
         }
     }

+ 124 - 3
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<ReaderActivity>() {
 
+    /**
+     * Network helper
+     */
+    private val network: NetworkHelper by injectLazy()
+
     /**
      * Preferences.
      */
@@ -61,6 +78,11 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
      */
     val chapterCache: ChapterCache by injectLazy()
 
+    /**
+     * Cover cache.
+     */
+    val coverCache: CoverCache by injectLazy()
+
     /**
      * Manga being read.
      */
@@ -88,6 +110,20 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
      */
     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<ReaderActivity>() {
                 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<ReaderActivity>() {
                 Timber.e(error)
             }
         }
-        .subscribeOn(Schedulers.io())
-        .subscribe()
+                .subscribeOn(Schedulers.io())
+                .subscribe()
     }
 
     /**
@@ -508,4 +546,87 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
         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)
+                        })
+            }
+        }
+    }
 }

+ 9 - 0
app/src/main/res/drawable/ic_insert_photo_black_24dp.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z" />
+</vector>

+ 8 - 1
app/src/main/res/values/strings.xml

@@ -223,7 +223,6 @@
     <string name="manga_info_status_label">Status</string>
     <string name="manga_info_source_label">Source</string>
     <string name="manga_info_genres_label">Genres</string>
-    <string name="share_subject">Share…</string>
     <string name="share_text">Check out %1$s! at %2$s</string>
     <string name="circular_icon">Circular icon</string>
     <string name="rounded_icon">Rounded icon</string>
@@ -267,10 +266,18 @@
     <string name="status">Status</string>
     <string name="chapters">Chapters</string>
 
+    <!-- Reader Activity -->
+    <string name="custom_filter">Custom filter</string>
+    <string name="save_page">Download page</string>
+    <string name="set_as_cover">Set as cover</string>
+    <string name="cover_updated">Cover updated</string>
     <!-- Dialog remove recently view -->
     <string name="dialog_remove_recently_description">This will remove the read date of this chapter. Are you sure?</string>
     <string name="dialog_remove_recently_reset">Reset all chapters for this manga</string>
 
+    <!-- Image notifier -->
+    <string name="picture_saved">Picture saved</string>
+    <string name="saving_picture">Saving picture</string>
 
     <!-- Reader activity -->
     <string name="custom_filter">Custom filter</string>