add logo, fix product size, detail product

This commit is contained in:
shaulascr
2025-08-26 13:40:21 +07:00
parent 9273e01324
commit 442f9fc10c
28 changed files with 39823 additions and 32 deletions

View File

@ -1,4 +1,5 @@
import java.util.Properties
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.kotlin.android)
@ -127,4 +128,8 @@ dependencies {
//Splash screen
implementation("androidx.core:core-splashscreen:1.0.0")
//pdf compression
implementation("com.tom-roush:pdfbox-android:2.0.27.0")
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 182 KiB

View File

@ -194,7 +194,7 @@ class ProductRepository(private val apiService: ApiService) {
if (response.isSuccessful) {
Result.Success(response.body()!!)
} else {
Result.Error(Exception("Failed to create product: ${response.code()}"))
Result.Error(Exception("Failed to create product: ${response.code()} message:${response.message()}"))
}
} catch (e: Exception) {
Result.Error(e)

View File

@ -1,18 +1,26 @@
package com.alya.ecommerce_serang.ui.product
import android.app.Dialog
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.widget.Button
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SwitchCompat
import androidx.core.graphics.drawable.toDrawable
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
@ -36,6 +44,9 @@ import com.alya.ecommerce_serang.ui.product.storeDetail.StoreDetailActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import com.google.android.material.bottomsheet.BottomSheetDialog
import java.text.NumberFormat
import java.util.Locale
@ -51,6 +62,8 @@ class DetailProductActivity : AppCompatActivity() {
private var isWholesaleSelected: Boolean = false
private var minOrder: Int = 0
private var TAG = "DetailProductActivity"
private val viewModel: ProductUserViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
@ -292,6 +305,16 @@ class DetailProductActivity : AppCompatActivity() {
.placeholder(R.drawable.placeholder_image)
.into(binding.ivProductImage)
binding.ivProductImage.setOnClickListener {
val img = product.image
if (!img.isNullOrEmpty()){
showDetailProduct(img)
}else {
Toast.makeText(this, "Gambar tidak tersedia", Toast.LENGTH_SHORT).show()
Log.e(TAG, "There is no photo product")
}
}
val ratingStr = product.rating
val ratingValue = ratingStr?.toFloatOrNull()
@ -533,6 +556,59 @@ class DetailProductActivity : AppCompatActivity() {
)
}
private fun showDetailProduct(photoProduct: String) {
val dialog = Dialog(this)
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
dialog.setContentView(R.layout.dialog_image_viewer)
dialog.setCancelable(true)
// Set dialog to fullscreen
val window = dialog.window
window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
window?.setBackgroundDrawable(Color.WHITE.toDrawable())
// Get views from dialog
val imageView = dialog.findViewById<ImageView>(R.id.iv_payment_evidence)
val btnClose = dialog.findViewById<ImageButton>(R.id.btn_close)
val tvTitle = dialog.findViewById<TextView>(R.id.tv_title)
val progressBar = dialog.findViewById<ProgressBar>(R.id.progress_bar)
tvTitle.text = "Gambar Produk"
val fullImageUrl =
if (photoProduct.startsWith("/")) BASE_URL + photoProduct.substring(1)
else photoProduct
progressBar.visibility = View.VISIBLE
Glide.with(this)
.load(fullImageUrl)
.placeholder(R.drawable.placeholder_image)
.error(R.drawable.placeholder_image)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(object : CustomTarget<Drawable>() {
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
progressBar.visibility = View.GONE
imageView.setImageDrawable(resource)
}
override fun onLoadCleared(placeholder: Drawable?) {
progressBar.visibility = View.GONE
imageView.setImageDrawable(placeholder)
}
override fun onLoadFailed(errorDrawable: Drawable?) {
progressBar.visibility = View.GONE
imageView.setImageDrawable(errorDrawable)
Toast.makeText(this@DetailProductActivity, "Gagal memuat gambar", Toast.LENGTH_SHORT).show()
}
})
btnClose.setOnClickListener { dialog.dismiss() }
imageView.setOnClickListener { dialog.dismiss() }
dialog.show()
}
override fun onResume() {
super.onResume()
loadData()

View File

@ -27,7 +27,8 @@ import com.alya.ecommerce_serang.data.repository.ProductRepository
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.databinding.ActivityDetailStoreProductBinding
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.FileUtils.compressFile
import com.alya.ecommerce_serang.utils.CompressionResult
import com.alya.ecommerce_serang.utils.FileUtils.compressFileToMax1MB
import com.alya.ecommerce_serang.utils.ImageUtils.compressImage
import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.ProductViewModel
@ -49,6 +50,8 @@ class DetailStoreProductActivity : AppCompatActivity() {
private var productId: Int? = null
private var hasImage: Boolean = false
private var TAG="DetailStoreProduct"
private var isEditing = false
private var hasExistingImage = false
@ -78,20 +81,32 @@ class DetailStoreProductActivity : AppCompatActivity() {
private val sppirtLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
if (uri != null && isValidFile(uri)) {
compressFile(this, uri).let { compressedFile ->
sppirtUri = compressedFile?.toUri()
binding.tvSppirtName.text = getFileName(sppirtUri!!)
binding.switcherSppirt.showNext()
when (val result = compressFileToMax1MB(this, uri)) {
is CompressionResult.Success -> {
sppirtUri = result.file.toUri()
binding.tvSppirtName.text = getFileName(sppirtUri!!)
binding.switcherSppirt.showNext()
}
is CompressionResult.Error -> {
Toast.makeText(this, result.reason, Toast.LENGTH_LONG).show()
Log.e(TAG, "Compression failed: ${result.reason}")
}
}
}
}
private val halalLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
if (uri != null && isValidFile(uri)) {
compressFile(this, uri).let { compressedFile ->
halalUri = compressedFile?.toUri()
binding.tvHalalName.text = getFileName(halalUri!!)
binding.switcherHalal.showNext()
when (val result = compressFileToMax1MB(this, uri)) {
is CompressionResult.Success -> {
halalUri = result.file.toUri()
binding.tvHalalName.text = getFileName(halalUri!!)
binding.switcherHalal.showNext()
}
is CompressionResult.Error -> {
Toast.makeText(this, result.reason, Toast.LENGTH_LONG).show()
Log.e(TAG, "Compression failed: ${result.reason}")
}
}
}
}
@ -260,12 +275,12 @@ class DetailStoreProductActivity : AppCompatActivity() {
}
private fun togglePreOrderVisibility(isChecked: Boolean) {
Log.d("DEBUG", "togglePreOrderVisibility: $isChecked")
Log.d(TAG, "togglePreOrderVisibility: $isChecked")
binding.layoutDurasi.visibility = if (isChecked) View.VISIBLE else View.GONE
}
private fun toggleWholesaleVisibility(isChecked: Boolean) {
Log.d("DEBUG", "toggleWholesaleVisibility: $isChecked")
Log.d(TAG, "toggleWholesaleVisibility: $isChecked")
binding.layoutMinPesanGrosir.visibility = if (isChecked) View.VISIBLE else View.GONE
binding.layoutHargaGrosir.visibility = if (isChecked) View.VISIBLE else View.GONE
}
@ -401,8 +416,10 @@ class DetailStoreProductActivity : AppCompatActivity() {
val sppirtFile = sppirtUri?.let { uriToNamedFile(it, this, "sppirt") }
val halalFile = halalUri?.let { uriToNamedFile(it, this, "halal") }
Log.d("File URI", "SPPIRT URI: ${sppirtUri.toString()}")
Log.d("File URI", "Halal URI: ${halalUri.toString()}")
Log.d(TAG, "SPPIRT URI: ${sppirtUri.toString()}")
Log.d(TAG, "Halal URI: ${halalUri.toString()}")
logFileInfo("Sppirt Size", sppirtFile!!)
logFileInfo("Halal Size", halalFile!!)
val imagePart = imageFile?.let { createPartFromFile("productimg", it) }
val sppirtPart = sppirtFile?.let { createPartFromFile("sppirt", it) }
@ -428,7 +445,7 @@ class DetailStoreProductActivity : AppCompatActivity() {
finish()
}
is Result.Error -> {
Log.e("ProductDetailActivity", "Error: ${result.exception.message}")
Log.e(TAG, "Error: ${result.exception.message}")
binding.btnSaveProduct.isEnabled = true
}
}
@ -506,7 +523,7 @@ class DetailStoreProductActivity : AppCompatActivity() {
finish()
}
is Result.Error -> {
Log.e("ProductDetailActivity", "Error: ${result.exception.message}")
Log.e(TAG, "Error: ${result.exception.message}")
binding.btnSaveProduct.isEnabled = true
}
}
@ -515,4 +532,10 @@ class DetailStoreProductActivity : AppCompatActivity() {
private fun toRequestBody(value: String): RequestBody =
RequestBody.create("text/plain".toMediaTypeOrNull(), value)
private fun logFileInfo(tag: String, file: File) {
val sizeKb = file.length() / 1024.0
val sizeMb = sizeKb / 1024.0
Log.d(TAG, "$tag → Name: ${file.name}, Size: ${"%.2f".format(sizeKb)} KB (${String.format("%.2f", sizeMb)} MB)")
}
}

View File

@ -1,12 +1,17 @@
package com.alya.ecommerce_serang.utils
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.util.Log
import android.webkit.MimeTypeMap
import com.tom_roush.pdfbox.pdmodel.PDDocument
import com.tom_roush.pdfbox.rendering.PDFRenderer
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
@ -135,4 +140,115 @@ object FileUtils {
else -> "application/octet-stream"
}
}
// for Uri format
// fun compressFileToMax1MB(context: Context, uri: Uri): File? {
// val mimeType = context.contentResolver.getType(uri)
//
// return if (mimeType?.startsWith("image/") == true) {
// // Handle images (jpg/png)
// compressImageToMax1MB(context, uri)
// } else if (mimeType == "application/pdf") {
// // Handle PDFs
// val file = createTempFileFromUri(context, uri, "pdf")
// return if (file != null && file.length() <= 1_048_576) {
// file
// } else {
// // 🚨 Without a PDF compression lib, you can only reject if > 1 MB
// null
// }
// } else {
// // Unsupported type
// null
// }
// }
fun compressFileToMax1MB(context: Context, uri: Uri): CompressionResult {
val mimeType = context.contentResolver.getType(uri) ?: return CompressionResult.Error("Tipe file tidak diketahui")
return if (mimeType.startsWith("image/")) {
val compressed = compressImageToMax1MB(context, uri)
if (compressed != null) {
CompressionResult.Success(compressed)
} else {
CompressionResult.Error("Ukuran gambar terlalu besar. Max 1MB.")
}
} else if (mimeType == "application/pdf") {
val file = createTempFileFromUri(context, uri, "pdf")
if (file == null) {
return CompressionResult.Error("Tidak bisa membaca file pdf")
}
if (file.length() <= 1_048_576) {
return CompressionResult.Success(file)
}
val compressed = compressPdfToMax1MB(context, file)
if (compressed != null) {
CompressionResult.Success(compressed)
} else {
CompressionResult.Error("Ukuran pdf terlalu besar. Max 1MB.")
}
} else {
CompressionResult.Error("Tipe file tidak didukung: $mimeType")
}
}
fun compressImageToMax1MB(context: Context, uri: Uri): File? {
val inputStream = context.contentResolver.openInputStream(uri) ?: return null
val originalBitmap = BitmapFactory.decodeStream(inputStream)
var quality = 100
var compressedFile: File
var outputStream: ByteArrayOutputStream
do {
outputStream = ByteArrayOutputStream()
originalBitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
val compressedBytes = outputStream.toByteArray()
compressedFile = File(context.cacheDir, "compressed_${System.currentTimeMillis()}.jpg")
FileOutputStream(compressedFile).use { it.write(compressedBytes) }
quality -= 5
} while (compressedFile.length() > 1_048_576 && quality > 10)
return if (compressedFile.length() <= 1_048_576) compressedFile else null
}
fun compressPdfToMax1MB(context: Context, inputFile: File): File? {
return try {
val document = PDDocument.load(inputFile)
val renderer = PDFRenderer(document)
val compressedDoc = PDDocument()
for (pageIndex in 0 until document.numberOfPages) {
val bitmap = renderer.renderImageWithDPI(pageIndex, 72f) // low DPI → smaller size
val outPage = com.tom_roush.pdfbox.pdmodel.PDPage(
com.tom_roush.pdfbox.pdmodel.common.PDRectangle(bitmap.width.toFloat(), bitmap.height.toFloat())
)
compressedDoc.addPage(outPage)
val pdImage = com.tom_roush.pdfbox.pdmodel.graphics.image.LosslessFactory.createFromImage(compressedDoc, bitmap)
val contentStream = com.tom_roush.pdfbox.pdmodel.PDPageContentStream(compressedDoc, outPage)
contentStream.drawImage(pdImage, 0f, 0f, bitmap.width.toFloat(), bitmap.height.toFloat())
contentStream.close()
}
val compressedFile = File(context.cacheDir, "compressed_${System.currentTimeMillis()}.pdf")
compressedDoc.save(compressedFile)
compressedDoc.close()
document.close()
// Check size
return if (compressedFile.length() <= 1_048_576) compressedFile else null
} catch (e: Exception) {
e.printStackTrace()
null
}
}
}
sealed class CompressionResult {
data class Success(val file: File) : CompressionResult()
data class Error(val reason: String) : CompressionResult()
}

File diff suppressed because one or more lines are too long

View File

@ -31,8 +31,9 @@
<ImageView
android:id="@+id/ivProductImage"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_height="360dp"
android:scaleType="centerCrop"
android:layout_marginTop="4dp"
android:contentDescription="@string/product_image"
tools:src="@drawable/placeholder_image" />
@ -218,7 +219,7 @@
android:layout_weight="1"
android:text="@string/ulasan_pembeli"
android:textColor="@color/black"
android:textSize="14sp"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
@ -276,7 +277,7 @@
android:layout_height="wrap_content"
android:text="@string/detail_produk"
android:textColor="@color/black"
android:textSize="14sp"
android:textSize="16sp"
android:textStyle="bold" />
<TableLayout
@ -357,7 +358,7 @@
android:layout_height="wrap_content"
android:text="@string/deskripsi_produk"
android:textColor="@color/black"
android:textSize="14sp"
android:textSize="16sp"
android:layout_marginTop="16dp"
android:textStyle="bold" />
@ -421,7 +422,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/black"
android:textSize="14sp"
android:textSize="16sp"
android:textStyle="bold"
tools:text="SnackEnak" />
@ -484,7 +485,7 @@
android:layout_weight="1"
android:text="@string/produk_lainnya"
android:textColor="@color/black"
android:textSize="14sp"
android:textSize="16sp"
android:textStyle="bold" />
<TextView

View File

@ -22,6 +22,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="14sp"
android:text="Bukti Pembayaran"
style="@style/title_large" />

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -1,6 +1,5 @@
<resources>
<string name="app_name">PasarKlik</string>
<string name="app_name">Plaza Serang Bahagia</string>
<!--Placeholder-->
<string name="fragment_home_search">Cari produk</string>

View File

@ -15,7 +15,6 @@
<item name="colorSurface">@color/white</item>
<item name="colorOnSurface">@color/black</item>
<item name="android:colorBackground">@color/white</item>
<!-- <item name="colorBackground">@color/white</item>-->
<!-- Container Colors -->
<item name="colorPrimaryContainer">@color/blue_50</item>
@ -39,8 +38,6 @@
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<!-- <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>-->
<item name="android:textViewStyle">@style/body_medium</item>
</style>
@ -319,7 +316,6 @@
</style>
<!-- Styles -->
<style name="circular_image">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">50%</item>
@ -330,10 +326,9 @@
<item name="cornerSize">5dp</item>
</style>
<style name="Theme.App.SplashScreen" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/white</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/logo_bisa_umkm</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/logo_psb</item>
<item name="windowSplashScreenAnimationDuration">1000</item>
<item name="android:windowSplashScreenBehavior" tools:targetApi="33">icon_preferred</item>
<item name="postSplashScreenTheme">@style/Theme.Ecommerce_serang</item>