Image viewer
This commit is contained in:
parent
64612dc47f
commit
79053c62fb
15 changed files with 78 additions and 42 deletions
|
@ -93,4 +93,7 @@ dependencies {
|
||||||
// LiveData
|
// LiveData
|
||||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$rootProject.liveDataVersion"
|
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$rootProject.liveDataVersion"
|
||||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||||
|
|
||||||
|
// Image viewer
|
||||||
|
implementation 'com.github.stfalcon-studio:StfalconImageViewer:v1.0.1'
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,8 @@
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <!-- Only required on SDK <= 28 -->
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/> <!-- Required to install packages downloaded through ntfy; craazyy! -->
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".app.Application"
|
android:name=".app.Application"
|
||||||
|
|
|
@ -2,6 +2,7 @@ package io.heckel.ntfy.ui
|
||||||
|
|
||||||
import android.app.DownloadManager
|
import android.app.DownloadManager
|
||||||
import android.content.*
|
import android.content.*
|
||||||
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
|
@ -17,6 +18,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.work.OneTimeWorkRequest
|
import androidx.work.OneTimeWorkRequest
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import androidx.work.workDataOf
|
import androidx.work.workDataOf
|
||||||
|
import com.stfalcon.imageviewer.StfalconImageViewer
|
||||||
import io.heckel.ntfy.R
|
import io.heckel.ntfy.R
|
||||||
import io.heckel.ntfy.data.Attachment
|
import io.heckel.ntfy.data.Attachment
|
||||||
import io.heckel.ntfy.data.Notification
|
import io.heckel.ntfy.data.Notification
|
||||||
|
@ -25,7 +27,6 @@ import io.heckel.ntfy.data.PROGRESS_NONE
|
||||||
import io.heckel.ntfy.msg.AttachmentDownloadWorker
|
import io.heckel.ntfy.msg.AttachmentDownloadWorker
|
||||||
import io.heckel.ntfy.util.*
|
import io.heckel.ntfy.util.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.math.exp
|
|
||||||
|
|
||||||
|
|
||||||
class DetailAdapter(private val onClick: (Notification) -> Unit, private val onLongClick: (Notification) -> Unit) :
|
class DetailAdapter(private val onClick: (Notification) -> Unit, private val onLongClick: (Notification) -> Unit) :
|
||||||
|
@ -131,21 +132,13 @@ class DetailAdapter(private val onClick: (Notification) -> Unit, private val onL
|
||||||
}
|
}
|
||||||
val attachment = notification.attachment
|
val attachment = notification.attachment
|
||||||
val exists = if (attachment.contentUri != null) fileExists(context, attachment.contentUri) else false
|
val exists = if (attachment.contentUri != null) fileExists(context, attachment.contentUri) else false
|
||||||
maybeRenderAttachmentImage(context, attachment, exists)
|
val image = attachment.contentUri != null && exists && supportedImage(attachment.type)
|
||||||
renderAttachmentBox(context, notification, attachment, exists)
|
maybeRenderMenu(context, notification, attachment, exists)
|
||||||
|
maybeRenderAttachmentImage(context, attachment, image)
|
||||||
|
maybeRenderAttachmentBox(context, notification, attachment, exists, image)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderAttachmentBox(context: Context, notification: Notification, attachment: Attachment, exists: Boolean) {
|
private fun maybeRenderMenu(context: Context, notification: Notification, attachment: Attachment, exists: Boolean) {
|
||||||
attachmentInfoView.text = formatAttachmentDetails(context, attachment, exists)
|
|
||||||
attachmentIconView.setImageResource(if (attachment.type?.startsWith("image/") == true) {
|
|
||||||
R.drawable.ic_file_image_gray_24dp
|
|
||||||
} else if (attachment.type?.startsWith("video/") == true) {
|
|
||||||
R.drawable.ic_file_video_gray_24dp
|
|
||||||
} else if (attachment.type?.startsWith("audio/") == true) {
|
|
||||||
R.drawable.ic_file_audio_gray_24dp
|
|
||||||
} else {
|
|
||||||
R.drawable.ic_file_document_gray_24dp
|
|
||||||
})
|
|
||||||
val menuButtonPopupMenu = createAttachmentPopup(context, menuButton, notification, attachment, exists) // Heavy lifting not during on-click
|
val menuButtonPopupMenu = createAttachmentPopup(context, menuButton, notification, attachment, exists) // Heavy lifting not during on-click
|
||||||
if (menuButtonPopupMenu != null) {
|
if (menuButtonPopupMenu != null) {
|
||||||
menuButton.setOnClickListener { menuButtonPopupMenu.show() }
|
menuButton.setOnClickListener { menuButtonPopupMenu.show() }
|
||||||
|
@ -153,6 +146,25 @@ class DetailAdapter(private val onClick: (Notification) -> Unit, private val onL
|
||||||
} else {
|
} else {
|
||||||
menuButton.visibility = View.GONE
|
menuButton.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun maybeRenderAttachmentBox(context: Context, notification: Notification, attachment: Attachment, exists: Boolean, image: Boolean) {
|
||||||
|
if (image) {
|
||||||
|
attachmentBoxView.visibility = View.GONE
|
||||||
|
return
|
||||||
|
}
|
||||||
|
attachmentInfoView.text = formatAttachmentDetails(context, attachment, exists)
|
||||||
|
attachmentIconView.setImageResource(if (attachment.type?.startsWith("image/") == true) {
|
||||||
|
R.drawable.ic_file_image_red_24dp
|
||||||
|
} else if (attachment.type?.startsWith("video/") == true) {
|
||||||
|
R.drawable.ic_file_video_orange_24dp
|
||||||
|
} else if (attachment.type?.startsWith("audio/") == true) {
|
||||||
|
R.drawable.ic_file_audio_purple_24dp
|
||||||
|
} else if ("application/vnd.android.package-archive" == attachment.type) {
|
||||||
|
R.drawable.ic_file_app_gray_24dp
|
||||||
|
} else {
|
||||||
|
R.drawable.ic_file_document_blue_24dp
|
||||||
|
})
|
||||||
val attachmentBoxPopupMenu = createAttachmentPopup(context, attachmentBoxView, notification, attachment, exists) // Heavy lifting not during on-click
|
val attachmentBoxPopupMenu = createAttachmentPopup(context, attachmentBoxView, notification, attachment, exists) // Heavy lifting not during on-click
|
||||||
if (attachmentBoxPopupMenu != null) {
|
if (attachmentBoxPopupMenu != null) {
|
||||||
attachmentBoxView.setOnClickListener { attachmentBoxPopupMenu.show() }
|
attachmentBoxView.setOnClickListener { attachmentBoxPopupMenu.show() }
|
||||||
|
@ -231,7 +243,7 @@ class DetailAdapter(private val onClick: (Notification) -> Unit, private val onL
|
||||||
if (expired) {
|
if (expired) {
|
||||||
infos.add("not downloaded, link expired")
|
infos.add("not downloaded, link expired")
|
||||||
} else if (expires) {
|
} else if (expires) {
|
||||||
infos.add("not downloaded, link expires ${formatDateShort(attachment.expires!!)}")
|
infos.add("not downloaded, expires ${formatDateShort(attachment.expires!!)}")
|
||||||
} else {
|
} else {
|
||||||
infos.add("not downloaded")
|
infos.add("not downloaded")
|
||||||
}
|
}
|
||||||
|
@ -270,17 +282,24 @@ class DetailAdapter(private val onClick: (Notification) -> Unit, private val onL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun maybeRenderAttachmentImage(context: Context, att: Attachment, exists: Boolean) {
|
private fun maybeRenderAttachmentImage(context: Context, attachment: Attachment, image: Boolean) {
|
||||||
val fileIsImage = att.contentUri != null && exists && supportedImage(att.type)
|
if (!image) {
|
||||||
if (!fileIsImage) {
|
|
||||||
attachmentImageView.visibility = View.GONE
|
attachmentImageView.visibility = View.GONE
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
val resolver = context.applicationContext.contentResolver
|
val resolver = context.applicationContext.contentResolver
|
||||||
val bitmapStream = resolver.openInputStream(Uri.parse(att.contentUri))
|
val bitmapStream = resolver.openInputStream(Uri.parse(attachment.contentUri))
|
||||||
val bitmap = BitmapFactory.decodeStream(bitmapStream)
|
val bitmap = BitmapFactory.decodeStream(bitmapStream)
|
||||||
attachmentImageView.setImageBitmap(bitmap)
|
attachmentImageView.setImageBitmap(bitmap)
|
||||||
|
attachmentImageView.setOnClickListener {
|
||||||
|
val loadImage = { view: ImageView, image: Bitmap -> view.setImageBitmap(image) }
|
||||||
|
StfalconImageViewer.Builder(context, listOf(bitmap), loadImage)
|
||||||
|
.allowZooming(true)
|
||||||
|
.withTransitionFrom(attachmentImageView)
|
||||||
|
.withHiddenStatusBar(false)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
attachmentImageView.visibility = View.VISIBLE
|
attachmentImageView.visibility = View.VISIBLE
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
attachmentImageView.visibility = View.GONE
|
attachmentImageView.visibility = View.GONE
|
||||||
|
|
9
app/src/main/res/drawable/ic_file_app_gray_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_file_app_gray_24dp.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:pathData="M17.6,9.48l1.84,-3.18c0.16,-0.31 0.04,-0.69 -0.26,-0.85c-0.29,-0.15 -0.65,-0.06 -0.83,0.22l-1.88,3.24c-2.86,-1.21 -6.08,-1.21 -8.94,0L5.65,5.67c-0.19,-0.29 -0.58,-0.38 -0.87,-0.2C4.5,5.65 4.41,6.01 4.56,6.3L6.4,9.48C3.3,11.25 1.28,14.44 1,18h22C22.72,14.44 20.7,11.25 17.6,9.48zM7,15.25c-0.69,0 -1.25,-0.56 -1.25,-1.25c0,-0.69 0.56,-1.25 1.25,-1.25S8.25,13.31 8.25,14C8.25,14.69 7.69,15.25 7,15.25zM17,15.25c-0.69,0 -1.25,-0.56 -1.25,-1.25c0,-0.69 0.56,-1.25 1.25,-1.25s1.25,0.56 1.25,1.25C18.25,14.69 17.69,15.25 17,15.25z"
|
||||||
|
android:fillColor="#555555"/>
|
||||||
|
</vector>
|
|
@ -1,9 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:pathData="M14,2H6C4.9,2 4,2.9 4,4v16c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V8L14,2zM6,20V4h7v5h5v11H6zM16,11h-4v3.88c-0.36,-0.24 -0.79,-0.38 -1.25,-0.38c-1.24,0 -2.25,1.01 -2.25,2.25c0,1.24 1.01,2.25 2.25,2.25S13,17.99 13,16.75V13h3V11z"
|
|
||||||
android:fillColor="#555555"/>
|
|
||||||
</vector>
|
|
9
app/src/main/res/drawable/ic_file_audio_purple_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_file_audio_purple_24dp.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:pathData="M12,3l0.01,10.55c-0.59,-0.34 -1.27,-0.55 -2,-0.55C7.79,13 6,14.79 6,17s1.79,4 4.01,4S14,19.21 14,17L14,7h4L18,3h-6zM10.01,19c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z"
|
||||||
|
android:fillColor="#B300FF"/>
|
||||||
|
</vector>
|
|
@ -5,5 +5,5 @@
|
||||||
android:viewportHeight="24">
|
android:viewportHeight="24">
|
||||||
<path
|
<path
|
||||||
android:pathData="M8,16h8v2L8,18zM8,12h8v2L8,14zM14,2L6,2c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6zM18,20L6,20L6,4h7v5h5v11z"
|
android:pathData="M8,16h8v2L8,18zM8,12h8v2L8,14zM14,2L6,2c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6zM18,20L6,20L6,4h7v5h5v11z"
|
||||||
android:fillColor="#555555"/>
|
android:fillColor="#00ADFF"/>
|
||||||
</vector>
|
</vector>
|
|
@ -5,5 +5,5 @@
|
||||||
android:viewportHeight="24">
|
android:viewportHeight="24">
|
||||||
<path
|
<path
|
||||||
android:pathData="M19,5v14L5,19L5,5h14m0,-2L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM14.14,11.86l-3,3.87L9,13.14 6,17h12l-3.86,-5.14z"
|
android:pathData="M19,5v14L5,19L5,5h14m0,-2L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM14.14,11.86l-3,3.87L9,13.14 6,17h12l-3.86,-5.14z"
|
||||||
android:fillColor="#555555"/>
|
android:fillColor="#E30000"/>
|
||||||
</vector>
|
</vector>
|
|
@ -1,9 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:pathData="M14,2H6C4.9,2 4,2.9 4,4v16c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V8L14,2zM6,20V4h7v5h5v11H6zM14,14l2,-1.06v4.12L14,16v1c0,0.55 -0.45,1 -1,1H9c-0.55,0 -1,-0.45 -1,-1v-4c0,-0.55 0.45,-1 1,-1h4c0.55,0 1,0.45 1,1V14z"
|
|
||||||
android:fillColor="#555555"/>
|
|
||||||
</vector>
|
|
9
app/src/main/res/drawable/ic_file_video_orange_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_file_video_orange_24dp.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:pathData="M4,6.47L5.76,10H20v8H4V6.47M22,4h-4l2,4h-3l-2,-4h-2l2,4h-3l-2,-4H8l2,4H7L5,4H4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V4z"
|
||||||
|
android:fillColor="#FF9800"/>
|
||||||
|
</vector>
|
|
@ -89,7 +89,7 @@
|
||||||
app:layout_constraintTop_toBottomOf="@id/detail_item_attachment_image"
|
app:layout_constraintTop_toBottomOf="@id/detail_item_attachment_image"
|
||||||
app:layout_constraintBottom_toTopOf="@id/detail_item_attachment_box"
|
app:layout_constraintBottom_toTopOf="@id/detail_item_attachment_box"
|
||||||
app:layout_constraintHorizontal_bias="0.0" android:layout_marginTop="2dp"
|
app:layout_constraintHorizontal_bias="0.0" android:layout_marginTop="2dp"
|
||||||
android:layout_marginBottom="3dp"/>
|
/>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@id/detail_item_tags_text"
|
android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@id/detail_item_tags_text"
|
||||||
|
@ -104,7 +104,7 @@
|
||||||
android:layout_height="wrap_content" app:srcCompat="@drawable/ic_cancel_gray_24dp"
|
android:layout_height="wrap_content" app:srcCompat="@drawable/ic_cancel_gray_24dp"
|
||||||
android:id="@+id/detail_item_attachment_icon" app:layout_constraintStart_toStartOf="parent"
|
android:id="@+id/detail_item_attachment_icon" app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/detail_item_attachment_info" android:layout_marginEnd="3dp"
|
app:layout_constraintEnd_toStartOf="@+id/detail_item_attachment_info" android:layout_marginEnd="5dp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
/>
|
/>
|
||||||
<TextView
|
<TextView
|
||||||
|
|
1
assets/android_black_24dp.svg
Normal file
1
assets/android_black_24dp.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><path d="M0,0h24v24H0V0z" fill="none"/></g><g><g><path d="M17.6,9.48l1.84-3.18c0.16-0.31,0.04-0.69-0.26-0.85c-0.29-0.15-0.65-0.06-0.83,0.22l-1.88,3.24 c-2.86-1.21-6.08-1.21-8.94,0L5.65,5.67c-0.19-0.29-0.58-0.38-0.87-0.2C4.5,5.65,4.41,6.01,4.56,6.3L6.4,9.48 C3.3,11.25,1.28,14.44,1,18h22C22.72,14.44,20.7,11.25,17.6,9.48z M7,15.25c-0.69,0-1.25-0.56-1.25-1.25 c0-0.69,0.56-1.25,1.25-1.25S8.25,13.31,8.25,14C8.25,14.69,7.69,15.25,7,15.25z M17,15.25c-0.69,0-1.25-0.56-1.25-1.25 c0-0.69,0.56-1.25,1.25-1.25s1.25,0.56,1.25,1.25C18.25,14.69,17.69,15.25,17,15.25z"/></g></g></svg>
|
After Width: | Height: | Size: 711 B |
1
assets/movie_black_24dp.svg
Normal file
1
assets/movie_black_24dp.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M4 6.47L5.76 10H20v8H4V6.47M22 4h-4l2 4h-3l-2-4h-2l2 4h-3l-2-4H8l2 4H7L5 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V4z"/></svg>
|
After Width: | Height: | Size: 296 B |
1
assets/music_note_black_24dp.svg
Normal file
1
assets/music_note_black_24dp.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M12 3l.01 10.55c-.59-.34-1.27-.55-2-.55C7.79 13 6 14.79 6 17s1.79 4 4.01 4S14 19.21 14 17V7h4V3h-6zm-1.99 16c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z"/></svg>
|
After Width: | Height: | Size: 311 B |
|
@ -18,6 +18,7 @@ allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
jcenter()
|
jcenter()
|
||||||
|
maven { url "https://jitpack.io" } // For StfalconImageViewer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue