From bd4dfda200d90e921384cd1a8f4a7741916621f4 Mon Sep 17 00:00:00 2001 From: tuancoltech Date: Wed, 16 Oct 2024 22:54:54 +0700 Subject: [PATCH] Implement graphics note's thumb in Note list --- .../dev/arkbuilders/arkmemo/di/AppModule.kt | 21 +++ .../arkbuilders/arkmemo/graphics/ColorCode.kt | 1 + .../arkbuilders/arkmemo/models/GraphicNote.kt | 3 + .../arkmemo/repo/graphics/GraphicNotesRepo.kt | 125 +++++++++++++++--- .../arkmemo/ui/adapters/NotesListAdapter.kt | 31 ++++- .../arkmemo/ui/dialogs/CommonActionDialog.kt | 1 + .../arkmemo/ui/fragments/NotesFragment.kt | 30 ++--- app/src/main/res/drawable/bg_big_radius.xml | 2 +- app/src/main/res/layout/adapter_text_note.xml | 31 +++-- app/src/main/res/values/colors.xml | 1 + app/src/main/res/values/dimens.xml | 2 + app/src/main/res/values/styles.xml | 8 ++ 12 files changed, 199 insertions(+), 57 deletions(-) create mode 100644 app/src/main/java/dev/arkbuilders/arkmemo/di/AppModule.kt diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/di/AppModule.kt b/app/src/main/java/dev/arkbuilders/arkmemo/di/AppModule.kt new file mode 100644 index 00000000..9768d557 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/arkmemo/di/AppModule.kt @@ -0,0 +1,21 @@ +package dev.arkbuilders.arkmemo.di + +import android.content.Context +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object AppModule { + + @Provides + @Singleton + fun provideApplicationContext(@ApplicationContext context: Context): Context { + return context + } + +} \ No newline at end of file diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/graphics/ColorCode.kt b/app/src/main/java/dev/arkbuilders/arkmemo/graphics/ColorCode.kt index 5aff56ed..dec148ae 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/graphics/ColorCode.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/graphics/ColorCode.kt @@ -10,4 +10,5 @@ internal object ColorCode { val purple by lazy { android.graphics.Color.parseColor("#7A5AF8") } val white by lazy { android.graphics.Color.parseColor("#FFFFFF") } val brown by lazy { android.graphics.Color.parseColor("#B54708") } + val lightYellow by lazy { android.graphics.Color.parseColor("#f8f6ed") } } diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/models/GraphicNote.kt b/app/src/main/java/dev/arkbuilders/arkmemo/models/GraphicNote.kt index bb571f69..8e6f47f5 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/models/GraphicNote.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/models/GraphicNote.kt @@ -1,5 +1,6 @@ package dev.arkbuilders.arkmemo.models +import android.graphics.Bitmap import android.os.Parcelable import dev.arkbuilders.arklib.data.index.Resource import dev.arkbuilders.arkmemo.graphics.SVG @@ -15,4 +16,6 @@ data class GraphicNote( @IgnoredOnParcel override var resource: Resource? = null, override var pendingForDelete: Boolean = false, + var thumb: Bitmap? = null + ) : Note, Parcelable diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/repo/graphics/GraphicNotesRepo.kt b/app/src/main/java/dev/arkbuilders/arkmemo/repo/graphics/GraphicNotesRepo.kt index 5853ff87..1f4c31b9 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/repo/graphics/GraphicNotesRepo.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/repo/graphics/GraphicNotesRepo.kt @@ -1,18 +1,33 @@ package dev.arkbuilders.arkmemo.repo.graphics +import android.content.Context +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.os.Environment import android.util.Log +import dagger.hilt.android.qualifiers.ApplicationContext import dev.arkbuilders.arklib.computeId import dev.arkbuilders.arklib.data.index.Resource +import dev.arkbuilders.arkmemo.R import dev.arkbuilders.arkmemo.di.IO_DISPATCHER +import dev.arkbuilders.arkmemo.graphics.ColorCode +import dev.arkbuilders.arkmemo.models.GraphicNote +import dev.arkbuilders.arkmemo.preferences.MemoPreferences import dev.arkbuilders.arkmemo.graphics.SVG import dev.arkbuilders.arkmemo.models.GraphicNote import dev.arkbuilders.arkmemo.models.SaveNoteResult import dev.arkbuilders.arkmemo.preferences.MemoPreferences import dev.arkbuilders.arkmemo.repo.NotesRepo import dev.arkbuilders.arkmemo.repo.NotesRepoHelper +import dev.arkbuilders.arkmemo.utils.dpToPx import dev.arkbuilders.arkmemo.utils.listFiles import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext +import java.io.File +import java.io.FileOutputStream +import java.io.IOException import java.nio.file.Path import javax.inject.Inject import javax.inject.Named @@ -23,19 +38,26 @@ import kotlin.io.path.fileSize import kotlin.io.path.getLastModifiedTime import kotlin.io.path.name -class GraphicNotesRepo - @Inject - constructor( - private val memoPreferences: MemoPreferences, - @Named(IO_DISPATCHER) private val iODispatcher: CoroutineDispatcher, - private val helper: NotesRepoHelper, - ) : NotesRepo { - private lateinit var root: Path - - override suspend fun init() { - helper.init() - root = memoPreferences.getNotesStorage() - } +class GraphicNotesRepo @Inject constructor( + private val memoPreferences: MemoPreferences, + @Named(IO_DISPATCHER) private val iODispatcher: CoroutineDispatcher, + private val helper: NotesRepoHelper, + @ApplicationContext private val context: Context +): NotesRepo { + + private lateinit var root: Path + + private val displayMetrics by lazy { Resources.getSystem().displayMetrics } + private val screenWidth by lazy { displayMetrics.widthPixels } + private val screenHeight by lazy { displayMetrics.heightPixels - 150.dpToPx() } + private val thumbViewWidth by lazy { context.resources.getDimension(R.dimen.graphic_thumb_width) } + + private val thumbDirectory by lazy { context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) } + + override suspend fun init() { + helper.init() + root = memoPreferences.getNotesStorage() + } override suspend fun save( note: GraphicNote, @@ -108,17 +130,78 @@ class GraphicNotesRepo modified = path.getLastModifiedTime(), ) - val userNoteProperties = helper.readProperties(id, "") + val userNoteProperties = helper.readProperties(id, "") + val bitmap = exportBitmapFromSvg(fileName = id.toString(), svg = svg) + + GraphicNote( + title = userNoteProperties.title, + description = userNoteProperties.description, + svg = svg, + resource = resource, + thumb = bitmap + ) - GraphicNote( - title = userNoteProperties.title, - description = userNoteProperties.description, - svg = svg, - resource = resource, - ) - }.filter { graphicNote -> graphicNote.svg != null } + }.filter { graphicNote -> graphicNote.svg != null } + } + + private fun exportBitmapFromSvg(fileName: String, svg: SVG?): Bitmap? { + + // Check if thumb bitmap already exists + val file = File(thumbDirectory, "$fileName.png") + try { + if (file.exists()) { + return BitmapFactory.decodeFile(file.absolutePath) } + } catch (e: Exception) { + e.printStackTrace() + } + + // If thumb doesn't exist, create a bitmap and a canvas for offscreen drawing + val bitmap = Bitmap.createBitmap( + thumbViewWidth.toInt(), thumbViewWidth.toInt(), Bitmap.Config.ARGB_8888 + ) + val canvas = Canvas(bitmap) + + canvas.drawColor(ColorCode.lightYellow) + svg?.getPaths()?.forEach { path -> + + canvas.save() + + // Scale factor to fit the SVG path into the view + val scaleX = thumbViewWidth / screenWidth + val scaleY = thumbViewWidth / screenHeight + + // Find the smallest scale to maintain the aspect ratio + val scale = minOf(scaleX, scaleY) + + // Center the path in the view + val dx = (thumbViewWidth - screenWidth * scale) / 2f + val dy = (thumbViewWidth - screenHeight * scale) / 2f + + // Apply scaling and translation to center the path + canvas.translate(dx, dy) + canvas.scale(scale, scale) + + canvas.drawPath(path.path, path.paint) + canvas.restore() + } ?: let { + return null + } + + // Save the bitmap to a file + try { + + // Open an output stream and write the bitmap to the file + FileOutputStream(file).use { outputStream -> + bitmap.compress(Bitmap.CompressFormat.PNG, 80, outputStream) // Save as PNG + } + return bitmap + } catch (e: IOException) { + e.printStackTrace() + return null + } } +} private const val GRAPHICS_REPO = "GraphicNotesRepo" private const val SVG_EXT = "svg" diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/ui/adapters/NotesListAdapter.kt b/app/src/main/java/dev/arkbuilders/arkmemo/ui/adapters/NotesListAdapter.kt index 2ff844ec..f3a743d2 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/ui/adapters/NotesListAdapter.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/ui/adapters/NotesListAdapter.kt @@ -1,5 +1,6 @@ package dev.arkbuilders.arkmemo.ui.adapters +import android.graphics.drawable.BitmapDrawable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -8,6 +9,8 @@ import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat import androidx.recyclerview.widget.RecyclerView import by.kirich1409.viewbindingdelegate.viewBinding +import com.google.android.material.shape.CornerFamily +import com.google.android.material.shape.ShapeAppearanceModel import dev.arkbuilders.arkmemo.R import dev.arkbuilders.arkmemo.databinding.AdapterTextNoteBinding import dev.arkbuilders.arkmemo.models.GraphicNote @@ -20,7 +23,6 @@ import dev.arkbuilders.arkmemo.ui.fragments.EditGraphicNotesFragment import dev.arkbuilders.arkmemo.ui.fragments.EditTextNotesFragment import dev.arkbuilders.arkmemo.ui.viewmodels.ArkMediaPlayerSideEffect import dev.arkbuilders.arkmemo.ui.viewmodels.ArkMediaPlayerState -import dev.arkbuilders.arkmemo.ui.views.NotesCanvas import dev.arkbuilders.arkmemo.utils.getAutoTitle import dev.arkbuilders.arkmemo.utils.gone import dev.arkbuilders.arkmemo.utils.highlightWord @@ -31,8 +33,8 @@ import dev.arkbuilders.arkmemo.utils.visible class NotesListAdapter( private var notes: MutableList, private val onPlayPauseClick: (path: String, pos: Int?, stopCallback: ((pos: Int) -> Unit)?) -> Unit, - private val onThumbPrepare: (note: GraphicNote, holder: NotesCanvas) -> Unit, -) : RecyclerView.Adapter() { +): RecyclerView.Adapter() { + private lateinit var activity: MainActivity lateinit var observeItemSideEffect: () -> ArkMediaPlayerSideEffect @@ -41,6 +43,10 @@ class NotesListAdapter( private var isFromSearch: Boolean = false private var searchKeyWord: String = "" + private val cornerRadius by lazy { + activity.resources.getDimension(R.dimen.corner_radius_big) + } + fun setActivity(activity: AppCompatActivity) { this.activity = activity as MainActivity } @@ -69,6 +75,7 @@ class NotesListAdapter( holder.contentPreview.text = note.text } holder.layoutAudioView.root.gone() + holder.ivGraphicThumb.gone() if (note is VoiceNote) { val isRecordingExist = note.path.toFile().length() > 0L if (isRecordingExist) { @@ -109,12 +116,24 @@ class NotesListAdapter( } } } else if (note is GraphicNote) { - holder.canvasGraphicThumb.visible() - onThumbPrepare(note, holder.canvasGraphicThumb) + holder.ivGraphicThumb.background = BitmapDrawable( + holder.itemView.context.resources, note.thumb + ) + holder.ivGraphicThumb.visible() + holder.ivGraphicThumb.shapeAppearanceModel = ShapeAppearanceModel.builder() + .setBottomLeftCornerSize(0f) + .setTopLeftCornerSize(0f) + .setTopRightCorner(CornerFamily.ROUNDED, cornerRadius) + .setBottomRightCorner(CornerFamily.ROUNDED, cornerRadius) + .build() } if (note.pendingForDelete) { holder.tvDelete.visible() + if (note is GraphicNote) { + holder.ivGraphicThumb.shapeAppearanceModel = ShapeAppearanceModel.builder() + .setAllCorners(CornerFamily.ROUNDED, 0f).build() + } } else { holder.tvDelete.gone() } @@ -207,7 +226,7 @@ class NotesListAdapter( val btnPlayPause = binding.layoutAudioView.ivPlayAudio val layoutAudioView = binding.layoutAudioView val tvPlayingPosition = binding.layoutAudioView.tvPlayingPosition - val canvasGraphicThumb = binding.canvasGraphicThumb + val ivGraphicThumb = binding.ivGraphicsThumb val tvDelete = binding.tvDelete var isSwiping: Boolean = false diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/ui/dialogs/CommonActionDialog.kt b/app/src/main/java/dev/arkbuilders/arkmemo/ui/dialogs/CommonActionDialog.kt index 0367a678..adb90cd2 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/ui/dialogs/CommonActionDialog.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/ui/dialogs/CommonActionDialog.kt @@ -41,6 +41,7 @@ class CommonActionDialog( private fun initViews() { dialog?.setCanceledOnTouchOutside(false) + dialog?.setCancelable(false) if (isAlert) { mBinding.tvPositive.setTextAppearance(R.style.AlertButton) diff --git a/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/NotesFragment.kt b/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/NotesFragment.kt index ff076d5f..f263f3ad 100644 --- a/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/NotesFragment.kt +++ b/app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/NotesFragment.kt @@ -9,7 +9,6 @@ import android.widget.Toast import androidx.core.content.ContextCompat import androidx.core.widget.addTextChangedListener import androidx.fragment.app.activityViewModels -import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle @@ -27,7 +26,6 @@ import dev.arkbuilders.arkmemo.ui.adapters.NotesListAdapter import dev.arkbuilders.arkmemo.ui.dialogs.CommonActionDialog import dev.arkbuilders.arkmemo.ui.viewmodels.ArkMediaPlayerSideEffect import dev.arkbuilders.arkmemo.ui.viewmodels.ArkMediaPlayerViewModel -import dev.arkbuilders.arkmemo.ui.viewmodels.GraphicNotesViewModel import dev.arkbuilders.arkmemo.ui.viewmodels.NotesViewModel import dev.arkbuilders.arkmemo.ui.views.toast import dev.arkbuilders.arkmemo.utils.getTextFromClipBoard @@ -204,14 +202,13 @@ class NotesFragment : BaseFragment() { private fun onNotesLoaded(notes: List) { binding.pbLoading.gone() if (notesAdapter == null) { - notesAdapter = - NotesListAdapter( - notes.toMutableList(), - onPlayPauseClick = { path, pos, onStop -> - playingAudioPath = path - if (playingAudioPosition >= 0) { - refreshVoiceNoteItem(playingAudioPosition) - } + notesAdapter = NotesListAdapter( + notes.toMutableList(), + onPlayPauseClick = { path, pos, onStop -> + playingAudioPath = path + if (playingAudioPosition >= 0) { + refreshNoteItem(playingAudioPosition) + } if (playingAudioPosition >= 0 && playingAudioPosition != pos) { // Another Voice note is being played compared to the previously played one @@ -227,13 +224,10 @@ class NotesFragment : BaseFragment() { mItemTouchHelper?.attachToRecyclerView(null) } - arkMediaPlayerViewModel.onPlayOrPauseClick(path, pos, onStop) - }, - onThumbPrepare = { graphicNote, noteCanvas -> - val tempNoteViewModel: GraphicNotesViewModel by viewModels() - noteCanvas.setViewModel(viewModel = tempNoteViewModel) - }, - ) + arkMediaPlayerViewModel.onPlayOrPauseClick(path, pos, onStop) + } + ) + } else { notesAdapter?.setNotes(notes) } @@ -278,7 +272,7 @@ class NotesFragment : BaseFragment() { (notesAdapter?.getNotes()?.getOrNull(pos) as? VoiceNote)?.waitToBeResumed = true } - private fun refreshVoiceNoteItem(position: Int) { + private fun refreshNoteItem(position: Int) { notesAdapter?.notifyItemChanged(position) } diff --git a/app/src/main/res/drawable/bg_big_radius.xml b/app/src/main/res/drawable/bg_big_radius.xml index 88d6c4a4..5376d261 100644 --- a/app/src/main/res/drawable/bg_big_radius.xml +++ b/app/src/main/res/drawable/bg_big_radius.xml @@ -1,7 +1,7 @@ - + \ No newline at end of file diff --git a/app/src/main/res/layout/adapter_text_note.xml b/app/src/main/res/layout/adapter_text_note.xml index c73d7e13..6d05f9a2 100644 --- a/app/src/main/res/layout/adapter_text_note.xml +++ b/app/src/main/res/layout/adapter_text_note.xml @@ -27,7 +27,7 @@ android:textColor="@color/text_primary" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintEnd_toStartOf="@+id/canvas_graphic_thumb" + app:layout_constraintEnd_toStartOf="@+id/iv_graphics_thumb" android:gravity="start" app:layout_constraintTop_toBottomOf="@+id/layout_audio_view" android:maxLines="1" @@ -46,7 +46,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@+id/tv_title" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toStartOf="@+id/canvas_graphic_thumb" + app:layout_constraintEnd_toStartOf="@+id/iv_graphics_thumb" android:maxLines="2" android:ellipsize="end" android:layout_marginTop="4dp" @@ -55,17 +55,18 @@ android:id="@+id/tv_content_preview" tools:text="How do you create compelling presentations that wow your colleagues and impress your managers?"/> - + tools:visibility="visible"/> + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 9a7e455c..b4c5e616 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -37,6 +37,7 @@ #17B26A #0BA5EC #7A5AF8 + #FBF8F0 #FFAAAAAA diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 8a9c4f75..6acf12c1 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -19,6 +19,7 @@ 12dp 12dp 36dp + 1dp 30dp @@ -38,6 +39,7 @@ 2dp 23dp 14dp + 90dp 20dp diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index ad9fc18c..a9c3e1ff 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -85,4 +85,12 @@ 26dp + + \ No newline at end of file