From fcc05506196855521c2acc0a1a1f16ac62ac495c 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 | 5 +- .../arkmemo/repo/graphics/GraphicNotesRepo.kt | 86 ++++++++++++++++++- .../arkmemo/ui/adapters/NotesListAdapter.kt | 28 ++++-- .../arkmemo/ui/dialogs/CommonActionDialog.kt | 1 + .../arkmemo/ui/fragments/NotesFragment.kt | 11 +-- 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, 168 insertions(+), 29 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 c543bc8e..0dea34ac 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") } } \ No newline at end of file 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 5ab1dc85..c4fefb9e 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 @@ -14,5 +15,7 @@ data class GraphicNote( val svg: SVG? = null, @IgnoredOnParcel override var resource: Resource? = null, - override var pendingForDelete: Boolean = false + override var pendingForDelete: Boolean = false, + var thumb: Bitmap? = null + ) : Note, Parcelable \ No newline at end of file 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 da5bdf47..3b143e43 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,31 @@ 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.SaveNoteResult 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 @@ -26,11 +39,19 @@ import kotlin.io.path.exists class GraphicNotesRepo @Inject constructor( private val memoPreferences: MemoPreferences, @Named(IO_DISPATCHER) private val iODispatcher: CoroutineDispatcher, - private val helper: NotesRepoHelper + 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() @@ -102,15 +123,76 @@ class GraphicNotesRepo @Inject constructor( ) val userNoteProperties = helper.readProperties(id, "") + val bitmap = exportBitmapFromSvg(fileName = id.toString(), svg = svg) GraphicNote( title = userNoteProperties.title, description = userNoteProperties.description, svg = svg, - resource = resource + resource = resource, + thumb = bitmap ) + }.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" 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 a4f3cc85..1dfc3094 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,7 +33,6 @@ 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() { private lateinit var activity: MainActivity @@ -42,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 } @@ -64,6 +69,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) { @@ -105,12 +111,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() } @@ -194,7 +212,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 1dda9126..6f00dc8d 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 @@ -40,6 +40,7 @@ class CommonActionDialog(@StringRes private val title: Int, 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 33c3e67b..8fdde145 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 @@ -192,7 +190,7 @@ class NotesFragment: BaseFragment() { onPlayPauseClick = { path, pos, onStop -> playingAudioPath = path if (playingAudioPosition >= 0) { - refreshVoiceNoteItem(playingAudioPosition) + refreshNoteItem(playingAudioPosition) } if (playingAudioPosition >= 0 && playingAudioPosition != pos) { @@ -210,11 +208,6 @@ class NotesFragment: BaseFragment() { } arkMediaPlayerViewModel.onPlayOrPauseClick(path, pos, onStop) - }, - onThumbPrepare = { graphicNote, noteCanvas -> - val tempNoteViewModel: GraphicNotesViewModel by viewModels() - noteCanvas.setViewModel(viewModel = tempNoteViewModel) - } ) @@ -261,7 +254,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