Improve profile item

This commit is contained in:
世界
2024-01-14 16:13:52 +08:00
parent 923a3789d0
commit cac0714587
13 changed files with 199 additions and 86 deletions

View File

@@ -91,10 +91,11 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0' implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.6' implementation 'androidx.navigation:navigation-fragment-ktx:2.7.6'
implementation 'androidx.navigation:navigation-ui-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.room:room-runtime:2.6.1'
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0' implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
implementation 'androidx.preference:preference-ktx:1.2.1' 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.Context
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.fragment.app.FragmentActivity
import com.google.android.material.R 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.libbox.ProfileContent
import io.nekohasekai.sfa.database.Profile import io.nekohasekai.sfa.database.Profile
import io.nekohasekai.sfa.database.TypedProfile import io.nekohasekai.sfa.database.TypedProfile
import io.nekohasekai.sfa.ui.shared.QRCodeDialog
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File 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.appcompat.widget.PopupMenu
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@@ -15,13 +16,14 @@ import androidx.recyclerview.widget.RecyclerView
import io.nekohasekai.sfa.R import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.database.Profile import io.nekohasekai.sfa.database.Profile
import io.nekohasekai.sfa.database.ProfileManager import io.nekohasekai.sfa.database.ProfileManager
import io.nekohasekai.sfa.database.TypedProfile
import io.nekohasekai.sfa.databinding.FragmentConfigurationBinding import io.nekohasekai.sfa.databinding.FragmentConfigurationBinding
import io.nekohasekai.sfa.databinding.ViewConfigutationItemBinding import io.nekohasekai.sfa.databinding.ViewConfigutationItemBinding
import io.nekohasekai.sfa.ktx.errorDialogBuilder import io.nekohasekai.sfa.ktx.errorDialogBuilder
import io.nekohasekai.sfa.ktx.shareProfile 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.EditProfileActivity
import io.nekohasekai.sfa.ui.profile.NewProfileActivity import io.nekohasekai.sfa.ui.profile.NewProfileActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -36,7 +38,7 @@ class ConfigurationFragment : Fragment() {
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View { ): View {
val binding = FragmentConfigurationBinding.inflate(inflater, container, false) val binding = FragmentConfigurationBinding.inflate(inflater, container, false)
val adapter = Adapter(lifecycleScope, binding) val adapter = Adapter(binding)
this.adapter = adapter this.adapter = adapter
binding.profileList.also { binding.profileList.also {
it.layoutManager = LinearLayoutManager(requireContext()) it.layoutManager = LinearLayoutManager(requireContext())
@@ -89,16 +91,17 @@ class ConfigurationFragment : Fragment() {
adapter?.reload() adapter?.reload()
} }
class Adapter( inner class Adapter(
internal val scope: CoroutineScope, private val parent: FragmentConfigurationBinding
internal val parent: FragmentConfigurationBinding
) : ) :
RecyclerView.Adapter<Holder>() { RecyclerView.Adapter<Holder>() {
internal var items: MutableList<Profile> = mutableListOf() internal var items: MutableList<Profile> = mutableListOf()
internal val scope = lifecycleScope
internal val fragmentActivity = requireActivity() as FragmentActivity
internal fun reload() { internal fun reload() {
scope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
val newItems = ProfileManager.list().toMutableList() val newItems = ProfileManager.list().toMutableList()
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
items = newItems items = newItems
@@ -163,6 +166,15 @@ class ConfigurationFragment : Fragment() {
internal fun bind(profile: Profile) { internal fun bind(profile: Profile) {
binding.profileName.text = profile.name 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 { binding.root.setOnClickListener {
val intent = Intent(binding.root.context, EditProfileActivity::class.java) val intent = Intent(binding.root.context, EditProfileActivity::class.java)
intent.putExtra("profile_id", profile.id) intent.putExtra("profile_id", profile.id)
@@ -172,6 +184,9 @@ class ConfigurationFragment : Fragment() {
val popup = PopupMenu(button.context, button) val popup = PopupMenu(button.context, button)
popup.setForceShowIcon(true) popup.setForceShowIcon(true)
popup.menuInflater.inflate(R.menu.profile_menu, popup.menu) 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 { popup.setOnMenuItemClickListener {
when (it.itemId) { when (it.itemId) {
R.id.action_share -> { R.id.action_share -> {
@@ -187,6 +202,19 @@ class ConfigurationFragment : Fragment() {
true 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 -> { R.id.action_delete -> {
adapter.items.remove(profile) adapter.items.remove(profile)
adapter.notifyItemRemoved(adapterPosition) adapter.notifyItemRemoved(adapterPosition)

View File

@@ -95,13 +95,11 @@ class EditProfileActivity : AbstractActivity() {
TypedProfile.Type.Local -> { TypedProfile.Type.Local -> {
binding.editButton.isVisible = true binding.editButton.isVisible = true
binding.remoteFields.isVisible = false binding.remoteFields.isVisible = false
binding.shareURLButton.isVisible = false
} }
TypedProfile.Type.Remote -> { TypedProfile.Type.Remote -> {
binding.editButton.isVisible = false binding.editButton.isVisible = false
binding.remoteFields.isVisible = true binding.remoteFields.isVisible = true
binding.shareURLButton.isVisible = true
binding.remoteURL.text = profile.typed.remoteURL binding.remoteURL.text = profile.typed.remoteURL
binding.lastUpdated.text = binding.lastUpdated.text =
DateFormat.getDateTimeInstance().format(profile.typed.lastUpdated) DateFormat.getDateTimeInstance().format(profile.typed.lastUpdated)
@@ -115,9 +113,6 @@ class EditProfileActivity : AbstractActivity() {
binding.autoUpdate.addTextChangedListener(this@EditProfileActivity::updateAutoUpdate) binding.autoUpdate.addTextChangedListener(this@EditProfileActivity::updateAutoUpdate)
binding.autoUpdateInterval.addTextChangedListener(this@EditProfileActivity::updateAutoUpdateInterval) binding.autoUpdateInterval.addTextChangedListener(this@EditProfileActivity::updateAutoUpdateInterval)
binding.updateButton.setOnClickListener(this@EditProfileActivity::updateProfile) 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.profileLayout.isVisible = true
binding.progressView.isVisible = false 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) { private fun shareProfile(button: View) {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
try { 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> </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> </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:layout_height="wrap_content"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="4dp"> android:paddingEnd="4dp">
<TextView <LinearLayout
android:id="@+id/profile_name" android:layout_weight="1"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall" android:layout_width="0dp"
android:textColor="?android:attr/textColorPrimary" android:orientation="vertical"
tools:text="Profile name" /> 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 <LinearLayout
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:gravity="center_vertical|end" android:gravity="top|end"
android:orientation="horizontal"> android:orientation="horizontal">
<Button <Button

View File

@@ -9,6 +9,13 @@
app:iconTintMode="src_in" app:iconTintMode="src_in"
app:iconTint="?colorPrimary" /> 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 <item
android:id="@+id/action_delete" android:id="@+id/action_delete"
android:title="@string/menu_delete" android:title="@string/menu_delete"

View File

@@ -26,9 +26,10 @@
<string name="profile_edit_content">Edit Content</string> <string name="profile_edit_content">Edit Content</string>
<string name="profile_check">Check</string> <string name="profile_check">Check</string>
<string name="profile_share">Share</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_input_required">Required</string>
<string name="profile_empty">Empty profiles</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_last_updated">Last Updated</string>
<string name="profile_update">Update</string> <string name="profile_update">Update</string>
<string name="profile_auto_update">Auto Update</string> <string name="profile_auto_update">Auto Update</string>