Improve profile item

This commit is contained in:
世界 2024-01-14 16:13:52 +08:00
parent 923a3789d0
commit cac0714587
No known key found for this signature in database
GPG key ID: CD109927C34A63C4
13 changed files with 199 additions and 86 deletions

View file

@ -91,10 +91,11 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.6'
implementation 'androidx.navigation:navigation-ui-ktx:2.7.6'
implementation 'com.google.zxing:core:3.4.1'
implementation 'androidx.room:room-runtime:2.6.1'
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
implementation 'androidx.preference:preference-ktx:1.2.1'

View file

@ -0,0 +1,14 @@
package io.nekohasekai.sfa.ktx
import android.content.res.Resources
import kotlin.math.ceil
private val density = Resources.getSystem().displayMetrics.density
fun dp2pxf(dpValue: Int): Float {
return density * dpValue
}
fun dp2px(dpValue: Int): Int {
return ceil(dp2pxf(dpValue)).toInt()
}

View file

@ -2,11 +2,18 @@ package io.nekohasekai.sfa.ktx
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import androidx.core.content.FileProvider
import androidx.fragment.app.FragmentActivity
import com.google.android.material.R
import com.google.zxing.BarcodeFormat
import com.google.zxing.qrcode.QRCodeWriter
import io.nekohasekai.libbox.Libbox
import io.nekohasekai.libbox.ProfileContent
import io.nekohasekai.sfa.database.Profile
import io.nekohasekai.sfa.database.TypedProfile
import io.nekohasekai.sfa.ui.shared.QRCodeDialog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
@ -43,4 +50,28 @@ suspend fun Context.shareProfile(profile: Profile) {
)
)
}
}
suspend fun FragmentActivity.shareProfileURL(profile: Profile) {
val link = Libbox.generateRemoteProfileImportLink(
profile.name,
profile.typed.remoteURL
)
val imageSize = dp2px(256)
val color = getAttrColor(com.google.android.material.R.attr.colorPrimary)
val image = QRCodeWriter().encode(link, BarcodeFormat.QR_CODE, imageSize, imageSize, null)
val imageWidth = image.width
val imageHeight = image.height
val imageArray = IntArray(imageWidth * imageHeight)
for (y in 0 until imageHeight) {
val offset = y * imageWidth
for (x in 0 until imageWidth) {
imageArray[offset + x] = if (image.get(x, y)) color else Color.TRANSPARENT
}
}
val bitmap = Bitmap.createBitmap(imageWidth, imageHeight, Bitmap.Config.ARGB_8888)
bitmap.setPixels(imageArray, 0, imageSize, 0, 0, imageWidth, imageHeight)
val dialog = QRCodeDialog(bitmap)
dialog.show(supportFragmentManager, "share-profile-url")
}

View file

@ -8,6 +8,7 @@ import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
@ -15,13 +16,14 @@ import androidx.recyclerview.widget.RecyclerView
import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.database.Profile
import io.nekohasekai.sfa.database.ProfileManager
import io.nekohasekai.sfa.database.TypedProfile
import io.nekohasekai.sfa.databinding.FragmentConfigurationBinding
import io.nekohasekai.sfa.databinding.ViewConfigutationItemBinding
import io.nekohasekai.sfa.ktx.errorDialogBuilder
import io.nekohasekai.sfa.ktx.shareProfile
import io.nekohasekai.sfa.ktx.shareProfileURL
import io.nekohasekai.sfa.ui.profile.EditProfileActivity
import io.nekohasekai.sfa.ui.profile.NewProfileActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@ -36,7 +38,7 @@ class ConfigurationFragment : Fragment() {
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
val binding = FragmentConfigurationBinding.inflate(inflater, container, false)
val adapter = Adapter(lifecycleScope, binding)
val adapter = Adapter(binding)
this.adapter = adapter
binding.profileList.also {
it.layoutManager = LinearLayoutManager(requireContext())
@ -89,16 +91,17 @@ class ConfigurationFragment : Fragment() {
adapter?.reload()
}
class Adapter(
internal val scope: CoroutineScope,
internal val parent: FragmentConfigurationBinding
inner class Adapter(
private val parent: FragmentConfigurationBinding
) :
RecyclerView.Adapter<Holder>() {
internal var items: MutableList<Profile> = mutableListOf()
internal val scope = lifecycleScope
internal val fragmentActivity = requireActivity() as FragmentActivity
internal fun reload() {
scope.launch(Dispatchers.IO) {
lifecycleScope.launch(Dispatchers.IO) {
val newItems = ProfileManager.list().toMutableList()
withContext(Dispatchers.Main) {
items = newItems
@ -163,6 +166,15 @@ class ConfigurationFragment : Fragment() {
internal fun bind(profile: Profile) {
binding.profileName.text = profile.name
if (profile.typed.type == TypedProfile.Type.Remote) {
binding.profileLastUpdated.isVisible = true
binding.profileLastUpdated.text = binding.root.context.getString(
R.string.profile_item_last_updated,
profile.typed.lastUpdated.toLocaleString()
)
} else {
binding.profileLastUpdated.isVisible = false
}
binding.root.setOnClickListener {
val intent = Intent(binding.root.context, EditProfileActivity::class.java)
intent.putExtra("profile_id", profile.id)
@ -172,6 +184,9 @@ class ConfigurationFragment : Fragment() {
val popup = PopupMenu(button.context, button)
popup.setForceShowIcon(true)
popup.menuInflater.inflate(R.menu.profile_menu, popup.menu)
if (profile.typed.type != TypedProfile.Type.Remote) {
popup.menu.removeItem(R.id.action_share_url)
}
popup.setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_share -> {
@ -187,6 +202,19 @@ class ConfigurationFragment : Fragment() {
true
}
R.id.action_share_url -> {
adapter.scope.launch(Dispatchers.IO) {
try {
adapter.fragmentActivity.shareProfileURL(profile)
} catch (e: Exception) {
withContext(Dispatchers.Main) {
button.context.errorDialogBuilder(e).show()
}
}
}
true
}
R.id.action_delete -> {
adapter.items.remove(profile)
adapter.notifyItemRemoved(adapterPosition)

View file

@ -95,13 +95,11 @@ class EditProfileActivity : AbstractActivity() {
TypedProfile.Type.Local -> {
binding.editButton.isVisible = true
binding.remoteFields.isVisible = false
binding.shareURLButton.isVisible = false
}
TypedProfile.Type.Remote -> {
binding.editButton.isVisible = false
binding.remoteFields.isVisible = true
binding.shareURLButton.isVisible = true
binding.remoteURL.text = profile.typed.remoteURL
binding.lastUpdated.text =
DateFormat.getDateTimeInstance().format(profile.typed.lastUpdated)
@ -115,9 +113,6 @@ class EditProfileActivity : AbstractActivity() {
binding.autoUpdate.addTextChangedListener(this@EditProfileActivity::updateAutoUpdate)
binding.autoUpdateInterval.addTextChangedListener(this@EditProfileActivity::updateAutoUpdateInterval)
binding.updateButton.setOnClickListener(this@EditProfileActivity::updateProfile)
binding.checkButton.setOnClickListener(this@EditProfileActivity::checkProfile)
binding.shareButton.setOnClickListener(this@EditProfileActivity::shareProfile)
binding.shareURLButton.setOnClickListener(this@EditProfileActivity::shareProfileURL)
binding.profileLayout.isVisible = true
binding.progressView.isVisible = false
}
@ -208,24 +203,6 @@ class EditProfileActivity : AbstractActivity() {
}
}
private fun checkProfile(button: View) {
val binding = binding ?: return
binding.progressView.isVisible = true
lifecycleScope.launch(Dispatchers.IO) {
delay(200L)
try {
Libbox.checkConfig(File(profile.typed.path).readText())
} catch (e: Exception) {
withContext(Dispatchers.Main) {
errorDialogBuilder(e).show()
}
}
withContext(Dispatchers.Main) {
binding.progressView.isVisible = false
}
}
}
private fun shareProfile(button: View) {
lifecycleScope.launch(Dispatchers.IO) {
try {
@ -238,25 +215,4 @@ class EditProfileActivity : AbstractActivity() {
}
}
private fun shareProfileURL(button: View) {
try {
startActivity(
Intent.createChooser(
Intent(Intent.ACTION_SEND).setType("application/octet-stream")
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.putExtra(
Intent.EXTRA_STREAM,
Libbox.generateRemoteProfileImportLink(
profile.name,
profile.typed.remoteURL
)
),
getString(com.google.android.material.R.string.abc_shareactionprovider_share_with)
)
)
} catch (e: Exception) {
errorDialogBuilder(e).show()
}
}
}

View file

@ -0,0 +1,26 @@
package io.nekohasekai.sfa.ui.shared
import android.graphics.Bitmap
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import io.nekohasekai.sfa.databinding.FragmentQrcodeDialogBinding
class QRCodeDialog(private val bitmap: Bitmap) :
BottomSheetDialogFragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = FragmentQrcodeDialogBinding.inflate(inflater, container, false)
val behavior = BottomSheetBehavior.from(binding.qrcodeLayout)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
binding.qrCode.setImageBitmap(bitmap)
return binding.root
}
}

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:tint="#000000"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp">
<path
android:fillColor="@android:color/white"
android:pathData="M15,21h-2v-2h2V21zM13,14h-2v5h2V14zM21,12h-2v4h2V12zM19,10h-2v2h2V10zM7,12H5v2h2V12zM5,10H3v2h2V10zM12,5h2V3h-2V5zM4.5,4.5v3h3v-3H4.5zM9,9H3V3h6V9zM4.5,16.5v3h3v-3H4.5zM9,21H3v-6h6V21zM16.5,4.5v3h3v-3H16.5zM21,9h-6V3h6V9zM19,19v-3l-4,0v2h2v3h4v-2H19zM17,12l-4,0v2h4V12zM13,10H7v2h2v2h2v-2h2V10zM14,9V7h-2V5h-2v4L14,9zM6.75,5.25h-1.5v1.5h1.5V5.25zM6.75,17.25h-1.5v1.5h1.5V17.25zM18.75,5.25h-1.5v1.5h1.5V5.25z" />
</vector>

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:tint="#000000"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp">
<path
android:fillColor="@android:color/white"
android:pathData="M21,10.12h-6.78l2.74,-2.82c-2.73,-2.7 -7.15,-2.8 -9.88,-0.1c-2.73,2.71 -2.73,7.08 0,9.79s7.15,2.71 9.88,0C18.32,15.65 19,14.08 19,12.1h2c0,1.98 -0.88,4.55 -2.64,6.29c-3.51,3.48 -9.21,3.48 -12.72,0c-3.5,-3.47 -3.53,-9.11 -0.02,-12.58s9.14,-3.47 12.65,0L21,3V10.12zM12.5,8v4.25l3.5,2.08l-0.72,1.21L11,13V8H12.5z" />
</vector>

View file

@ -140,29 +140,6 @@
</LinearLayout>
<Button
android:id="@+id/shareButton"
style="@style/Widget.Material3.Button.ElevatedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/profile_share" />
<Button
android:id="@+id/shareURLButton"
style="@style/Widget.Material3.Button.ElevatedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/profile_share_url" />
<Button
android:id="@+id/checkButton"
style="@style/Widget.Material3.Button.ElevatedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/profile_check" />
</LinearLayout>

View file

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/qrcode_layout"
style="@style/Widget.Material3.BottomSheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:id="@+id/drag_handle"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<ImageView
android:paddingTop="16dp"
android:id="@+id/qr_code"
android:scaleType="centerCrop"
android:adjustViewBounds="true"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -14,21 +14,38 @@
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="4dp">
<TextView
android:id="@+id/profile_name"
android:layout_width="wrap_content"
<LinearLayout
android:layout_weight="1"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary"
tools:text="Profile name" />
android:layout_width="0dp"
android:orientation="vertical"
android:padding="10dp">
<TextView
android:id="@+id/profile_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?textAppearanceTitleMedium"
android:textColor="?android:attr/textColorPrimary"
tools:text="Profile name" />
<TextView
tools:visibility="gone"
android:id="@+id/profile_last_updated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textAppearance="?textAppearanceBodySmall"
android:textColor="?android:attr/textColorPrimary"
tools:text="Last updated at now" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical|end"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="top|end"
android:orientation="horizontal">
<Button

View file

@ -9,6 +9,13 @@
app:iconTintMode="src_in"
app:iconTint="?colorPrimary" />
<item
android:id="@+id/action_share_url"
android:icon="@drawable/ic_qr_code_2_24"
android:title="@string/profile_share_url"
app:iconTint="?colorPrimary"
app:iconTintMode="src_in" />
<item
android:id="@+id/action_delete"
android:title="@string/menu_delete"

View file

@ -26,9 +26,10 @@
<string name="profile_edit_content">Edit Content</string>
<string name="profile_check">Check</string>
<string name="profile_share">Share</string>
<string name="profile_share_url">Share URL</string>
<string name="profile_share_url">Share URL as QR Code</string>
<string name="profile_input_required">Required</string>
<string name="profile_empty">Empty profiles</string>
<string name="profile_item_last_updated">Last Updated: %s</string>
<string name="profile_last_updated">Last Updated</string>
<string name="profile_update">Update</string>
<string name="profile_auto_update">Auto Update</string>