From 560a7c2e3a5609c6c1937d497e20297884b4e40d Mon Sep 17 00:00:00 2001 From: roihershberg Date: Thu, 9 Dec 2021 21:17:47 +0200 Subject: [PATCH 01/11] Created AvatarMaker for handling avatar images Added AvatarMaker class for handling both contact avatar and user avatar making. Added avatarUri to User class for future image support. --- atox/src/main/kotlin/ui/Util.kt | 90 ++++++++++++------- atox/src/main/kotlin/ui/call/CallFragment.kt | 4 +- atox/src/main/kotlin/ui/chat/ChatFragment.kt | 4 +- .../contact_profile/ContactProfileFragment.kt | 4 +- .../kotlin/ui/contactlist/ContactAdapter.kt | 4 +- .../ltd.evilcorp.core.db.Database/5.json | 40 +++++---- core/src/main/kotlin/db/UserDao.kt | 3 + core/src/main/kotlin/vo/User.kt | 3 + gradle/wrapper/gradle-wrapper.properties | 1 - 9 files changed, 93 insertions(+), 60 deletions(-) diff --git a/atox/src/main/kotlin/ui/Util.kt b/atox/src/main/kotlin/ui/Util.kt index 08bc2dd8..9d4307a2 100644 --- a/atox/src/main/kotlin/ui/Util.kt +++ b/atox/src/main/kotlin/ui/Util.kt @@ -19,8 +19,64 @@ import kotlin.math.abs import ltd.evilcorp.atox.R import ltd.evilcorp.core.vo.ConnectionStatus import ltd.evilcorp.core.vo.Contact +import ltd.evilcorp.core.vo.User import ltd.evilcorp.core.vo.UserStatus +private const val DEFAULT_AVATAR_SIZE_DP = 50 +internal class AvatarMaker { + + private var name: String = "" + private var publicKey: String = "" + private var avatarUri: String = "" + private var initials: String = "" + + constructor(contact: Contact) { + name = contact.name + publicKey = contact.publicKey + avatarUri = contact.avatarUri + initials = getInitials() + } + constructor(user: User) { + name = user.name + publicKey = user.publicKey + avatarUri = user.avatarUri + initials = getInitials() + } + + private fun getInitials(): String { + val segments = name.split(" ") + if (segments.size == 1) return segments.first().take(1) + return segments.first().take(1) + segments[1][0] + } + + fun setAvatar(imageView: ImageView, sizeDp: Int = DEFAULT_AVATAR_SIZE_DP) = + if (avatarUri.isNotEmpty()) { + imageView.setImageURI(Uri.parse(avatarUri)) + } else { + val side = (sizeDp * imageView.resources.displayMetrics.density).toInt() + val bitmap = Bitmap.createBitmap(side, side, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + val rect = RectF(0f, 0f, bitmap.width.toFloat(), bitmap.height.toFloat()) + val colors = imageView.resources.getIntArray(R.array.contactBackgrounds) + val backgroundPaint = Paint().apply { color = colors[abs(publicKey.hashCode()).rem(colors.size)] } + + val textScale = sizeDp.toFloat() / DEFAULT_AVATAR_SIZE_DP + val textPaint = Paint().apply { + color = Color.WHITE + textSize = imageView.resources.getDimension(R.dimen.contact_avatar_placeholder_text) * textScale + textAlign = Paint.Align.CENTER + isAntiAlias = true + typeface = Typeface.create("sans-serif-light", Typeface.NORMAL) + } + + val textBounds = Rect() + textPaint.getTextBounds(initials, 0, initials.length, textBounds) + canvas.drawRoundRect(rect, rect.bottom, rect.right, backgroundPaint) + canvas.drawText(initials, rect.centerX(), rect.centerY() - textBounds.exactCenterY(), textPaint) + imageView.setImageBitmap(bitmap) + } +} + internal fun colorByStatus(resources: Resources, contact: Contact): Int { if (contact.connectionStatus == ConnectionStatus.None) return ResourcesCompat.getColor( resources, @@ -33,37 +89,3 @@ internal fun colorByStatus(resources: Resources, contact: Contact): Int { UserStatus.Busy -> ResourcesCompat.getColor(resources, R.color.statusBusy, null) } } - -private fun getInitials(contact: Contact): String { - val segments = contact.name.split(" ") - if (segments.size == 1) return segments.first().take(1) - return segments.first().take(1) + segments[1][0] -} - -private const val DEFAULT_AVATAR_SIZE_DP = 50 -internal fun setAvatarFromContact(imageView: ImageView, contact: Contact, sizeDp: Int = DEFAULT_AVATAR_SIZE_DP) = - if (contact.avatarUri.isNotEmpty()) { - imageView.setImageURI(Uri.parse(contact.avatarUri)) - } else { - val side = (sizeDp * imageView.resources.displayMetrics.density).toInt() - val bitmap = Bitmap.createBitmap(side, side, Bitmap.Config.ARGB_8888) - val canvas = Canvas(bitmap) - val rect = RectF(0f, 0f, bitmap.width.toFloat(), bitmap.height.toFloat()) - val colors = imageView.resources.getIntArray(R.array.contactBackgrounds) - val backgroundPaint = Paint().apply { color = colors[abs(contact.publicKey.hashCode()).rem(colors.size)] } - - val textScale = sizeDp.toFloat() / DEFAULT_AVATAR_SIZE_DP - val textPaint = Paint().apply { - color = Color.WHITE - textSize = imageView.resources.getDimension(R.dimen.contact_avatar_placeholder_text) * textScale - textAlign = Paint.Align.CENTER - isAntiAlias = true - typeface = Typeface.create("sans-serif-light", Typeface.NORMAL) - } - val initials = getInitials(contact) - val textBounds = Rect() - textPaint.getTextBounds(initials, 0, initials.length, textBounds) - canvas.drawRoundRect(rect, rect.bottom, rect.right, backgroundPaint) - canvas.drawText(initials, rect.centerX(), rect.centerY() - textBounds.exactCenterY(), textPaint) - imageView.setImageBitmap(bitmap) - } diff --git a/atox/src/main/kotlin/ui/call/CallFragment.kt b/atox/src/main/kotlin/ui/call/CallFragment.kt index ab41738d..d18446d2 100644 --- a/atox/src/main/kotlin/ui/call/CallFragment.kt +++ b/atox/src/main/kotlin/ui/call/CallFragment.kt @@ -21,7 +21,7 @@ import ltd.evilcorp.atox.hasPermission import ltd.evilcorp.atox.requireStringArg import ltd.evilcorp.atox.ui.BaseFragment import ltd.evilcorp.atox.ui.chat.CONTACT_PUBLIC_KEY -import ltd.evilcorp.atox.ui.setAvatarFromContact +import ltd.evilcorp.atox.ui.AvatarMaker import ltd.evilcorp.atox.vmFactory import ltd.evilcorp.domain.feature.CallState import ltd.evilcorp.domain.tox.PublicKey @@ -52,7 +52,7 @@ class CallFragment : BaseFragment(FragmentCallBinding::infl vm.setActiveContact(PublicKey(requireStringArg(CONTACT_PUBLIC_KEY))) vm.contact.observe(viewLifecycleOwner) { - setAvatarFromContact(callBackground, it, CALL_BACKGROUND_SIZE_DP) + AvatarMaker(it).setAvatar(callBackground, CALL_BACKGROUND_SIZE_DP) } endCall.setOnClickListener { diff --git a/atox/src/main/kotlin/ui/chat/ChatFragment.kt b/atox/src/main/kotlin/ui/chat/ChatFragment.kt index 3e8f2e3f..3a22a8c7 100644 --- a/atox/src/main/kotlin/ui/chat/ChatFragment.kt +++ b/atox/src/main/kotlin/ui/chat/ChatFragment.kt @@ -43,7 +43,7 @@ import ltd.evilcorp.atox.requireStringArg import ltd.evilcorp.atox.truncated import ltd.evilcorp.atox.ui.BaseFragment import ltd.evilcorp.atox.ui.colorByStatus -import ltd.evilcorp.atox.ui.setAvatarFromContact +import ltd.evilcorp.atox.ui.AvatarMaker import ltd.evilcorp.atox.vmFactory import ltd.evilcorp.core.vo.ConnectionStatus import ltd.evilcorp.core.vo.FileTransfer @@ -190,7 +190,7 @@ class ChatFragment : BaseFragment(FragmentChatBinding::infl }.lowercase(Locale.getDefault()) profileLayout.statusIndicator.setColorFilter(colorByStatus(resources, it)) - setAvatarFromContact(profileLayout.profileImage, it) + AvatarMaker(it).setAvatar(profileLayout.profileImage) if (it.draftMessage.isNotEmpty() && outgoingMessage.text.isEmpty()) { outgoingMessage.setText(it.draftMessage) diff --git a/atox/src/main/kotlin/ui/contact_profile/ContactProfileFragment.kt b/atox/src/main/kotlin/ui/contact_profile/ContactProfileFragment.kt index f1c01f5e..04bc1a20 100644 --- a/atox/src/main/kotlin/ui/contact_profile/ContactProfileFragment.kt +++ b/atox/src/main/kotlin/ui/contact_profile/ContactProfileFragment.kt @@ -16,7 +16,7 @@ import ltd.evilcorp.atox.requireStringArg import ltd.evilcorp.atox.ui.BaseFragment import ltd.evilcorp.atox.ui.chat.CONTACT_PUBLIC_KEY import ltd.evilcorp.atox.ui.colorByStatus -import ltd.evilcorp.atox.ui.setAvatarFromContact +import ltd.evilcorp.atox.ui.AvatarMaker import ltd.evilcorp.atox.vmFactory import ltd.evilcorp.domain.tox.PublicKey @@ -41,7 +41,7 @@ class ContactProfileFragment : BaseFragment(Fragm contact.name = contact.name.ifEmpty { getString(R.string.contact_default_name) } headerMainText.text = contact.name - setAvatarFromContact(profileLayout.profileImage, contact) + AvatarMaker(contact).setAvatar(profileLayout.profileImage) profileLayout.statusIndicator.setColorFilter(colorByStatus(resources, contact)) contactPublicKey.text = contact.publicKey diff --git a/atox/src/main/kotlin/ui/contactlist/ContactAdapter.kt b/atox/src/main/kotlin/ui/contactlist/ContactAdapter.kt index 0e00e7e1..d5c000d4 100644 --- a/atox/src/main/kotlin/ui/contactlist/ContactAdapter.kt +++ b/atox/src/main/kotlin/ui/contactlist/ContactAdapter.kt @@ -17,7 +17,7 @@ import ltd.evilcorp.atox.R import ltd.evilcorp.atox.databinding.ContactListViewItemBinding import ltd.evilcorp.atox.databinding.FriendRequestItemBinding import ltd.evilcorp.atox.ui.colorByStatus -import ltd.evilcorp.atox.ui.setAvatarFromContact +import ltd.evilcorp.atox.ui.AvatarMaker import ltd.evilcorp.core.vo.Contact import ltd.evilcorp.core.vo.FriendRequest @@ -112,7 +112,7 @@ class ContactAdapter( } } vh.status.setColorFilter(colorByStatus(resources, this)) - setAvatarFromContact(vh.image, this) + AvatarMaker(this).setAvatar(vh.image) vh.unreadIndicator.visibility = if (hasUnreadMessages) { View.VISIBLE } else { diff --git a/core/schemas/ltd.evilcorp.core.db.Database/5.json b/core/schemas/ltd.evilcorp.core.db.Database/5.json index 7a247cc1..a2fa5d2b 100644 --- a/core/schemas/ltd.evilcorp.core.db.Database/5.json +++ b/core/schemas/ltd.evilcorp.core.db.Database/5.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 5, - "identityHash": "e4dc8f9f9d5264db6d914cb0deefb517", + "identityHash": "e5acbeae0a3d3479e8e4d1e722bfc1c4", "entities": [ { "tableName": "contacts", @@ -80,14 +80,8 @@ }, { "tableName": "file_transfers", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `public_key` TEXT NOT NULL, `file_number` INTEGER NOT NULL, `file_kind` INTEGER NOT NULL, `file_size` INTEGER NOT NULL, `file_name` TEXT NOT NULL, `outgoing` INTEGER NOT NULL, `progress` INTEGER NOT NULL, `destination` TEXT NOT NULL)", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`public_key` TEXT NOT NULL, `file_number` INTEGER NOT NULL, `file_kind` INTEGER NOT NULL, `file_size` INTEGER NOT NULL, `file_name` TEXT NOT NULL, `outgoing` INTEGER NOT NULL, `progress` INTEGER NOT NULL, `destination` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, { "fieldPath": "publicKey", "columnName": "public_key", @@ -135,6 +129,12 @@ "columnName": "destination", "affinity": "TEXT", "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true } ], "primaryKey": { @@ -174,14 +174,8 @@ }, { "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `conversation` TEXT NOT NULL, `message` TEXT NOT NULL, `sender` INTEGER NOT NULL, `type` INTEGER NOT NULL, `correlation_id` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL)", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`conversation` TEXT NOT NULL, `message` TEXT NOT NULL, `sender` INTEGER NOT NULL, `type` INTEGER NOT NULL, `correlation_id` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, { "fieldPath": "publicKey", "columnName": "conversation", @@ -217,6 +211,12 @@ "columnName": "timestamp", "affinity": "INTEGER", "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true } ], "primaryKey": { @@ -230,7 +230,7 @@ }, { "tableName": "users", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`public_key` TEXT NOT NULL, `name` TEXT NOT NULL, `status_message` TEXT NOT NULL, `status` INTEGER NOT NULL, `connection_status` INTEGER NOT NULL, `password` TEXT NOT NULL, PRIMARY KEY(`public_key`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`public_key` TEXT NOT NULL, `name` TEXT NOT NULL, `status_message` TEXT NOT NULL, `status` INTEGER NOT NULL, `connection_status` INTEGER NOT NULL, `avatar_uri` TEXT NOT NULL, `password` TEXT NOT NULL, PRIMARY KEY(`public_key`))", "fields": [ { "fieldPath": "publicKey", @@ -262,6 +262,12 @@ "affinity": "INTEGER", "notNull": true }, + { + "fieldPath": "avatarUri", + "columnName": "avatar_uri", + "affinity": "TEXT", + "notNull": true + }, { "fieldPath": "password", "columnName": "password", @@ -282,7 +288,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e4dc8f9f9d5264db6d914cb0deefb517')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e5acbeae0a3d3479e8e4d1e722bfc1c4')" ] } } \ No newline at end of file diff --git a/core/src/main/kotlin/db/UserDao.kt b/core/src/main/kotlin/db/UserDao.kt index 75d3d186..acc6221a 100644 --- a/core/src/main/kotlin/db/UserDao.kt +++ b/core/src/main/kotlin/db/UserDao.kt @@ -34,6 +34,9 @@ interface UserDao { @Query("UPDATE users SET status = :status WHERE public_key == :publicKey") fun updateStatus(publicKey: String, status: UserStatus) + @Query("UPDATE contacts SET avatar_uri = :uri WHERE public_key = :publicKey") + fun updateAvatarUri(publicKey: String, uri: String) + @Query("SELECT COUNT(*) FROM users WHERE public_key = :publicKey") fun exists(publicKey: String): Boolean diff --git a/core/src/main/kotlin/vo/User.kt b/core/src/main/kotlin/vo/User.kt index 9f89bccd..1cb5ab1a 100644 --- a/core/src/main/kotlin/vo/User.kt +++ b/core/src/main/kotlin/vo/User.kt @@ -26,6 +26,9 @@ data class User( @ColumnInfo(name = "connection_status") var connectionStatus: ConnectionStatus = ConnectionStatus.None, + @ColumnInfo(name = "avatar_uri") + var avatarUri: String = "", + @ColumnInfo(name = "password") var password: String = "" ) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e64c4192..e750102e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=de8f52ad49bdc759164f72439a3bf56ddb1589c4cde802d3cec7d6ad0e0ee410 distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 88bf7955a427c537e3203a22a0c2e590608323a4 Mon Sep 17 00:00:00 2001 From: roihershberg Date: Sun, 12 Dec 2021 02:55:12 +0200 Subject: [PATCH 02/11] Redesigned profile view screen + Related code cleanup Redesigned the profile view screen and cleaned up some related code. The edit profile view is to be implemented. --- atox/build.gradle.kts | 4 +- atox/src/main/AndroidManifest.xml | 2 +- atox/src/main/kotlin/ui/AvatarMaker.kt | 89 ++++++ atox/src/main/kotlin/ui/NotificationHelper.kt | 2 +- atox/src/main/kotlin/ui/Util.kt | 160 ++++++----- .../ui/addcontact/AddContactFragment.kt | 2 +- atox/src/main/kotlin/ui/chat/ChatFragment.kt | 12 +- .../contact_profile/ContactProfileFragment.kt | 6 +- .../kotlin/ui/contactlist/ContactAdapter.kt | 4 +- .../friend_request/FriendRequestFragment.kt | 2 +- .../kotlin/ui/settings/SettingsFragment.kt | 2 +- .../ui/user_profile/UserProfileFragment.kt | 224 ++++++++++----- .../{attach_file.xml => ic_attach_file.xml} | 0 .../drawable/{back.xml => ic_back_black.xml} | 2 +- atox/src/main/res/drawable/ic_back_white.xml | 9 + atox/src/main/res/drawable/ic_copy_black.xml | 9 + atox/src/main/res/drawable/ic_copy_white.xml | 5 + atox/src/main/res/drawable/ic_edit_black.xml | 9 + atox/src/main/res/drawable/ic_edit_white.xml | 5 + .../res/drawable/{send.xml => ic_send.xml} | 0 .../res/drawable/side_nav_bar_background.xml | 2 +- .../main/res/layout/dialog_receive_share.xml | 2 +- atox/src/main/res/layout/dialog_status.xml | 10 +- .../main/res/layout/fragment_add_contact.xml | 2 +- atox/src/main/res/layout/fragment_chat.xml | 6 +- .../main/res/layout/fragment_contact_list.xml | 6 +- .../res/layout/fragment_friend_request.xml | 2 +- atox/src/main/res/layout/fragment_profile.xml | 2 +- .../src/main/res/layout/fragment_settings.xml | 2 +- .../main/res/layout/fragment_user_profile.xml | 269 ++++++++++-------- .../main/res/layout/profile_image_layout.xml | 10 +- atox/src/main/res/layout/profile_options.xml | 58 ---- .../user_profile_share_id_context_menu.xml | 6 - atox/src/main/res/values-night-v21/styles.xml | 16 +- atox/src/main/res/values-night-v23/styles.xml | 19 ++ atox/src/main/res/values-night-v29/styles.xml | 11 - atox/src/main/res/values-v21/styles.xml | 16 +- atox/src/main/res/values-v23/styles.xml | 20 ++ atox/src/main/res/values-v29/styles.xml | 11 - atox/src/main/res/values/colors.xml | 11 +- atox/src/main/res/values/strings.xml | 6 + atox/src/main/res/values/styles.xml | 12 +- atox/src/main/res/xml/file_paths.xml | 2 + 43 files changed, 638 insertions(+), 411 deletions(-) create mode 100644 atox/src/main/kotlin/ui/AvatarMaker.kt rename atox/src/main/res/drawable/{attach_file.xml => ic_attach_file.xml} (100%) rename atox/src/main/res/drawable/{back.xml => ic_back_black.xml} (89%) create mode 100644 atox/src/main/res/drawable/ic_back_white.xml create mode 100644 atox/src/main/res/drawable/ic_copy_black.xml create mode 100644 atox/src/main/res/drawable/ic_copy_white.xml create mode 100644 atox/src/main/res/drawable/ic_edit_black.xml create mode 100644 atox/src/main/res/drawable/ic_edit_white.xml rename atox/src/main/res/drawable/{send.xml => ic_send.xml} (100%) delete mode 100644 atox/src/main/res/layout/profile_options.xml delete mode 100644 atox/src/main/res/menu/user_profile_share_id_context_menu.xml create mode 100644 atox/src/main/res/values-night-v23/styles.xml delete mode 100644 atox/src/main/res/values-night-v29/styles.xml create mode 100644 atox/src/main/res/values-v23/styles.xml delete mode 100644 atox/src/main/res/values-v29/styles.xml diff --git a/atox/build.gradle.kts b/atox/build.gradle.kts index d97f5c61..8b7d6dc2 100644 --- a/atox/build.gradle.kts +++ b/atox/build.gradle.kts @@ -19,10 +19,10 @@ android { multiDexEnabled = true } buildTypes { - getByName("debug") { + debug { applicationIdSuffix = ".debug" } - getByName("release") { + release { isMinifyEnabled = true proguardFiles("proguard-tox4j.pro", getDefaultProguardFile("proguard-android-optimize.txt")) } diff --git a/atox/src/main/AndroidManifest.xml b/atox/src/main/AndroidManifest.xml index 31258873..7487de10 100644 --- a/atox/src/main/AndroidManifest.xml +++ b/atox/src/main/AndroidManifest.xml @@ -17,7 +17,7 @@ android:label="@string/app_name" android:roundIcon="@mipmap/launcher_icon_round" android:supportsRtl="true" - android:theme="@style/AppTheme"> + android:theme="@style/Theme.aTox.DayNight"> diff --git a/atox/src/main/kotlin/ui/AvatarMaker.kt b/atox/src/main/kotlin/ui/AvatarMaker.kt new file mode 100644 index 00000000..b2abda80 --- /dev/null +++ b/atox/src/main/kotlin/ui/AvatarMaker.kt @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: 2019-2021 aTox contributors +// +// SPDX-License-Identifier: GPL-3.0-only + +package ltd.evilcorp.atox.ui + +import android.graphics.* +import android.net.Uri +import android.widget.ImageView +import ltd.evilcorp.atox.R +import ltd.evilcorp.core.vo.Contact +import ltd.evilcorp.core.vo.User +import kotlin.math.abs + + +/** + * Class for creating an avatar for user or contact and setting it in the ImageView + */ +internal class AvatarMaker { + + companion object { + private const val DEFAULT_AVATAR_SIZE_DP = 50 + } + + private var name: String = "" + private var publicKey: String = "" + private var avatarUri: String = "" + private var initials: String = "" + + constructor(contact: Contact) { + name = contact.name + publicKey = contact.publicKey + avatarUri = contact.avatarUri + initials = getInitials() + } + constructor(user: User) { + name = user.name + publicKey = user.publicKey + avatarUri = user.avatarUri + initials = getInitials() + } + + + /** + * Method will get the initial characters of the name + * @return The initial characters of the name. + */ + private fun getInitials(): String { + val segments = name.split(" ") + if (segments.size == 1) return segments.first().take(1) + return segments.first().take(1) + segments[1][0] + } + + + /** + * Method will set an avatar to an image view. If avatar image exists then it will be set to the image view, + * otherwise a new avatar image will be created based on the initials of the name + * and the public key for the background color. + * @param imageView The image view for whom to set the avatar image. + * @param sizeDp The size of the avatar image in dp units. + */ + fun setAvatar(imageView: ImageView, sizeDp: Int = DEFAULT_AVATAR_SIZE_DP) = + if (avatarUri.isNotEmpty()) { + imageView.setImageURI(Uri.parse(avatarUri)) + } else { + val side = (sizeDp * imageView.resources.displayMetrics.density).toInt() + val bitmap = Bitmap.createBitmap(side, side, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + val rect = RectF(0f, 0f, bitmap.width.toFloat(), bitmap.height.toFloat()) + val colors = imageView.resources.getIntArray(R.array.contactBackgrounds) + val backgroundPaint = Paint().apply { color = colors[abs(publicKey.hashCode()).rem(colors.size)] } + + val textScale = sizeDp.toFloat() / DEFAULT_AVATAR_SIZE_DP + val textPaint = Paint().apply { + color = Color.WHITE + textSize = imageView.resources.getDimension(R.dimen.contact_avatar_placeholder_text) * textScale + textAlign = Paint.Align.CENTER + isAntiAlias = true + typeface = Typeface.create("sans-serif-light", Typeface.NORMAL) + } + + val textBounds = Rect() + textPaint.getTextBounds(initials, 0, initials.length, textBounds) + canvas.drawRoundRect(rect, rect.bottom, rect.right, backgroundPaint) + canvas.drawText(initials, rect.centerX(), rect.centerY() - textBounds.exactCenterY(), textPaint) + imageView.setImageBitmap(bitmap) + } + +} diff --git a/atox/src/main/kotlin/ui/NotificationHelper.kt b/atox/src/main/kotlin/ui/NotificationHelper.kt index e2cd77b5..92bd43d1 100644 --- a/atox/src/main/kotlin/ui/NotificationHelper.kt +++ b/atox/src/main/kotlin/ui/NotificationHelper.kt @@ -111,7 +111,7 @@ class NotificationHelper @Inject constructor( .addAction( NotificationCompat.Action .Builder( - IconCompat.createWithResource(context, R.drawable.send), + IconCompat.createWithResource(context, R.drawable.ic_send), context.getString(R.string.reply), PendingIntentCompat.getBroadcast( context, diff --git a/atox/src/main/kotlin/ui/Util.kt b/atox/src/main/kotlin/ui/Util.kt index 9d4307a2..49581918 100644 --- a/atox/src/main/kotlin/ui/Util.kt +++ b/atox/src/main/kotlin/ui/Util.kt @@ -4,88 +4,110 @@ package ltd.evilcorp.atox.ui +import android.content.Context +import android.content.res.ColorStateList +import android.content.res.Configuration import android.content.res.Resources -import android.graphics.Bitmap -import android.graphics.Canvas import android.graphics.Color -import android.graphics.Paint -import android.graphics.Rect -import android.graphics.RectF -import android.graphics.Typeface -import android.net.Uri -import android.widget.ImageView +import android.graphics.drawable.GradientDrawable +import android.graphics.drawable.RippleDrawable +import android.os.Build +import android.view.View +import android.view.ViewGroup +import android.widget.ImageButton import androidx.core.content.res.ResourcesCompat -import kotlin.math.abs import ltd.evilcorp.atox.R import ltd.evilcorp.core.vo.ConnectionStatus import ltd.evilcorp.core.vo.Contact -import ltd.evilcorp.core.vo.User import ltd.evilcorp.core.vo.UserStatus -private const val DEFAULT_AVATAR_SIZE_DP = 50 -internal class AvatarMaker { - private var name: String = "" - private var publicKey: String = "" - private var avatarUri: String = "" - private var initials: String = "" +/** + * Function will return the color of the status of the input contact + * @param resources The resources of the app + * @param contact The contact for whom to retrieve the status color. + * @return The color int. + */ +internal fun colorByContactStatus(resources: Resources, contact: Contact) = + if (contact.connectionStatus == ConnectionStatus.None) + ResourcesCompat.getColor( + resources, + R.color.statusOffline, + null + ) + else colorFromStatus(resources, contact.status) - constructor(contact: Contact) { - name = contact.name - publicKey = contact.publicKey - avatarUri = contact.avatarUri - initials = getInitials() - } - constructor(user: User) { - name = user.name - publicKey = user.publicKey - avatarUri = user.avatarUri - initials = getInitials() - } - private fun getInitials(): String { - val segments = name.split(" ") - if (segments.size == 1) return segments.first().take(1) - return segments.first().take(1) + segments[1][0] - } +/** + * Function will return the color of the status of the input user status + * @param resources The resources of the app + * @param status The user status for whom to return the corresponding color. + * @return The color int. + */ +internal fun colorFromStatus(resources: Resources, status: UserStatus) = when (status) { + UserStatus.None -> ResourcesCompat.getColor(resources, R.color.statusAvailable, null) + UserStatus.Away -> ResourcesCompat.getColor(resources, R.color.statusAway, null) + UserStatus.Busy -> ResourcesCompat.getColor(resources, R.color.statusBusy, null) +} + + +/** + * Function will return whether or not night mode is set. + * @param context The related context. + * @return Boolean indicating whether or not night mode is set. + */ +internal fun isNightMode(context: Context) = context.resources.configuration.uiMode + .and(Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES + + +/** + * Function will set a transparent background to an image button with a ripple with color according + * to whether or not night mode is set. + * @param context The related context. + * @param imageButton The ImageButton for whom to set the transparent background with ripple. + */ +internal fun setImageButtonRippleDayNight(context: Context, imageButton: ImageButton) = + if (isNightMode(context)) + setImageButtonRipple(imageButton, Color.argb(51, 255, 255, 255)) + else setImageButtonRipple(imageButton, Color.argb(31, 0, 0, 0)) - fun setAvatar(imageView: ImageView, sizeDp: Int = DEFAULT_AVATAR_SIZE_DP) = - if (avatarUri.isNotEmpty()) { - imageView.setImageURI(Uri.parse(avatarUri)) - } else { - val side = (sizeDp * imageView.resources.displayMetrics.density).toInt() - val bitmap = Bitmap.createBitmap(side, side, Bitmap.Config.ARGB_8888) - val canvas = Canvas(bitmap) - val rect = RectF(0f, 0f, bitmap.width.toFloat(), bitmap.height.toFloat()) - val colors = imageView.resources.getIntArray(R.array.contactBackgrounds) - val backgroundPaint = Paint().apply { color = colors[abs(publicKey.hashCode()).rem(colors.size)] } - - val textScale = sizeDp.toFloat() / DEFAULT_AVATAR_SIZE_DP - val textPaint = Paint().apply { - color = Color.WHITE - textSize = imageView.resources.getDimension(R.dimen.contact_avatar_placeholder_text) * textScale - textAlign = Paint.Align.CENTER - isAntiAlias = true - typeface = Typeface.create("sans-serif-light", Typeface.NORMAL) - } - - val textBounds = Rect() - textPaint.getTextBounds(initials, 0, initials.length, textBounds) - canvas.drawRoundRect(rect, rect.bottom, rect.right, backgroundPaint) - canvas.drawText(initials, rect.centerX(), rect.centerY() - textBounds.exactCenterY(), textPaint) - imageView.setImageBitmap(bitmap) - } + +/** + * Function will set a transparent background to an image button with a ripple with the input color. + * @param imageButton The ImageButton for whom to set the transparent background with ripple. + * @param colorInt The color of the ripple. + */ +internal fun setImageButtonRipple(imageButton: ImageButton, colorInt: Int) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + val background = GradientDrawable() + background.shape = GradientDrawable.OVAL + background.setColor(0x0FFFFFF) + + background.cornerRadius = 10f + + val mask = GradientDrawable() + mask.shape = GradientDrawable.OVAL + mask.setColor(-0x1000000) + mask.cornerRadius = 5f + + val rippleColorLst = ColorStateList.valueOf(colorInt) + val ripple = RippleDrawable(rippleColorLst, background, mask) + imageButton.background = ripple + } } -internal fun colorByStatus(resources: Resources, contact: Contact): Int { - if (contact.connectionStatus == ConnectionStatus.None) return ResourcesCompat.getColor( - resources, - R.color.statusOffline, - null - ) - return when (contact.status) { - UserStatus.None -> ResourcesCompat.getColor(resources, R.color.statusAvailable, null) - UserStatus.Away -> ResourcesCompat.getColor(resources, R.color.statusAway, null) - UserStatus.Busy -> ResourcesCompat.getColor(resources, R.color.statusBusy, null) + +/** + * Function will release the resources of a view, hopefully prevent memory leaks. + * @param view The view for whom to release the resources. + */ +internal fun unbindDrawables(view: View) { + if (view.background != null) { + view.background.callback = null + } + if (view is ViewGroup) { + for (i in 0 until view.childCount) unbindDrawables(view.getChildAt(i)) + view.removeAllViews() + view.setBackgroundResource(0) } } diff --git a/atox/src/main/kotlin/ui/addcontact/AddContactFragment.kt b/atox/src/main/kotlin/ui/addcontact/AddContactFragment.kt index c93ebdce..19d3a0ac 100644 --- a/atox/src/main/kotlin/ui/addcontact/AddContactFragment.kt +++ b/atox/src/main/kotlin/ui/addcontact/AddContactFragment.kt @@ -61,7 +61,7 @@ class AddContactFragment : BaseFragment(FragmentAddCo contacts = it } - toolbar.setNavigationIcon(R.drawable.back) + toolbar.setNavigationIcon(R.drawable.ic_back_white) toolbar.setNavigationOnClickListener { WindowInsetsControllerCompat(requireActivity().window, view) .hide(WindowInsetsCompat.Type.ime()) diff --git a/atox/src/main/kotlin/ui/chat/ChatFragment.kt b/atox/src/main/kotlin/ui/chat/ChatFragment.kt index 3a22a8c7..8479081b 100644 --- a/atox/src/main/kotlin/ui/chat/ChatFragment.kt +++ b/atox/src/main/kotlin/ui/chat/ChatFragment.kt @@ -23,11 +23,7 @@ import androidx.core.content.FileProvider import androidx.core.content.getSystemService import androidx.core.content.res.ResourcesCompat import androidx.core.os.bundleOf -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsAnimationCompat -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.WindowInsetsControllerCompat -import androidx.core.view.updatePadding +import androidx.core.view.* import androidx.core.widget.doAfterTextChanged import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController @@ -42,7 +38,7 @@ import ltd.evilcorp.atox.databinding.FragmentChatBinding import ltd.evilcorp.atox.requireStringArg import ltd.evilcorp.atox.truncated import ltd.evilcorp.atox.ui.BaseFragment -import ltd.evilcorp.atox.ui.colorByStatus +import ltd.evilcorp.atox.ui.colorByContactStatus import ltd.evilcorp.atox.ui.AvatarMaker import ltd.evilcorp.atox.vmFactory import ltd.evilcorp.core.vo.ConnectionStatus @@ -135,7 +131,7 @@ class ChatFragment : BaseFragment(FragmentChatBinding::infl } ) - toolbar.setNavigationIcon(R.drawable.back) + toolbar.setNavigationIcon(R.drawable.ic_back_white) toolbar.setNavigationOnClickListener { WindowInsetsControllerCompat(requireActivity().window, view).hide(WindowInsetsCompat.Type.ime()) activity?.onBackPressed() @@ -189,7 +185,7 @@ class ChatFragment : BaseFragment(FragmentChatBinding::infl else -> DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT).format(it.lastMessage) }.lowercase(Locale.getDefault()) - profileLayout.statusIndicator.setColorFilter(colorByStatus(resources, it)) + profileLayout.statusIndicator.setColorFilter(colorByContactStatus(resources, it)) AvatarMaker(it).setAvatar(profileLayout.profileImage) if (it.draftMessage.isNotEmpty() && outgoingMessage.text.isEmpty()) { diff --git a/atox/src/main/kotlin/ui/contact_profile/ContactProfileFragment.kt b/atox/src/main/kotlin/ui/contact_profile/ContactProfileFragment.kt index 04bc1a20..d957f9fe 100644 --- a/atox/src/main/kotlin/ui/contact_profile/ContactProfileFragment.kt +++ b/atox/src/main/kotlin/ui/contact_profile/ContactProfileFragment.kt @@ -15,7 +15,7 @@ import ltd.evilcorp.atox.databinding.FragmentContactProfileBinding import ltd.evilcorp.atox.requireStringArg import ltd.evilcorp.atox.ui.BaseFragment import ltd.evilcorp.atox.ui.chat.CONTACT_PUBLIC_KEY -import ltd.evilcorp.atox.ui.colorByStatus +import ltd.evilcorp.atox.ui.colorByContactStatus import ltd.evilcorp.atox.ui.AvatarMaker import ltd.evilcorp.atox.vmFactory import ltd.evilcorp.domain.tox.PublicKey @@ -31,7 +31,7 @@ class ContactProfileFragment : BaseFragment(Fragm compat } - toolbar.setNavigationIcon(R.drawable.back) + toolbar.setNavigationIcon(R.drawable.ic_back_white) toolbar.setNavigationOnClickListener { activity?.onBackPressed() } @@ -42,7 +42,7 @@ class ContactProfileFragment : BaseFragment(Fragm headerMainText.text = contact.name AvatarMaker(contact).setAvatar(profileLayout.profileImage) - profileLayout.statusIndicator.setColorFilter(colorByStatus(resources, contact)) + profileLayout.statusIndicator.setColorFilter(colorByContactStatus(resources, contact)) contactPublicKey.text = contact.publicKey contactName.text = contact.name diff --git a/atox/src/main/kotlin/ui/contactlist/ContactAdapter.kt b/atox/src/main/kotlin/ui/contactlist/ContactAdapter.kt index d5c000d4..3c4c5490 100644 --- a/atox/src/main/kotlin/ui/contactlist/ContactAdapter.kt +++ b/atox/src/main/kotlin/ui/contactlist/ContactAdapter.kt @@ -16,7 +16,7 @@ import java.text.DateFormat import ltd.evilcorp.atox.R import ltd.evilcorp.atox.databinding.ContactListViewItemBinding import ltd.evilcorp.atox.databinding.FriendRequestItemBinding -import ltd.evilcorp.atox.ui.colorByStatus +import ltd.evilcorp.atox.ui.colorByContactStatus import ltd.evilcorp.atox.ui.AvatarMaker import ltd.evilcorp.core.vo.Contact import ltd.evilcorp.core.vo.FriendRequest @@ -111,7 +111,7 @@ class ContactAdapter( vh.statusMessage.setTextColor(vh.lastMessage.currentTextColor) } } - vh.status.setColorFilter(colorByStatus(resources, this)) + vh.status.setColorFilter(colorByContactStatus(resources, this)) AvatarMaker(this).setAvatar(vh.image) vh.unreadIndicator.visibility = if (hasUnreadMessages) { View.VISIBLE diff --git a/atox/src/main/kotlin/ui/friend_request/FriendRequestFragment.kt b/atox/src/main/kotlin/ui/friend_request/FriendRequestFragment.kt index 132fe32a..b6438f38 100644 --- a/atox/src/main/kotlin/ui/friend_request/FriendRequestFragment.kt +++ b/atox/src/main/kotlin/ui/friend_request/FriendRequestFragment.kt @@ -33,7 +33,7 @@ class FriendRequestFragment : BaseFragment(Fragmen compat } - toolbar.setNavigationIcon(R.drawable.back) + toolbar.setNavigationIcon(R.drawable.ic_back_white) toolbar.setNavigationOnClickListener { activity?.onBackPressed() } diff --git a/atox/src/main/kotlin/ui/settings/SettingsFragment.kt b/atox/src/main/kotlin/ui/settings/SettingsFragment.kt index a4229676..c19971f8 100644 --- a/atox/src/main/kotlin/ui/settings/SettingsFragment.kt +++ b/atox/src/main/kotlin/ui/settings/SettingsFragment.kt @@ -96,7 +96,7 @@ class SettingsFragment : BaseFragment(FragmentSettingsB } toolbar.apply { - setNavigationIcon(R.drawable.back) + setNavigationIcon(R.drawable.ic_back_white) setNavigationOnClickListener { WindowInsetsControllerCompat(requireActivity().window, view) .hide(WindowInsetsCompat.Type.ime()) diff --git a/atox/src/main/kotlin/ui/user_profile/UserProfileFragment.kt b/atox/src/main/kotlin/ui/user_profile/UserProfileFragment.kt index 99312bf6..e4b99489 100644 --- a/atox/src/main/kotlin/ui/user_profile/UserProfileFragment.kt +++ b/atox/src/main/kotlin/ui/user_profile/UserProfileFragment.kt @@ -9,40 +9,47 @@ import android.content.ClipboardManager import android.content.Intent import android.content.res.Resources import android.graphics.Bitmap +import android.graphics.Canvas import android.graphics.Color +import android.net.Uri import android.os.Bundle -import android.text.InputFilter import android.util.TypedValue -import android.view.ContextMenu -import android.view.MenuItem import android.view.View -import android.widget.EditText import android.widget.ImageView import android.widget.Toast -import androidx.appcompat.app.AlertDialog import androidx.core.content.getSystemService -import androidx.core.content.res.ResourcesCompat -import androidx.core.graphics.scale -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.setPadding -import androidx.core.view.updatePadding +import androidx.core.view.* import androidx.fragment.app.viewModels import io.nayuki.qrcodegen.QrCode import kotlin.math.min import kotlin.math.roundToInt import ltd.evilcorp.atox.R import ltd.evilcorp.atox.databinding.FragmentUserProfileBinding -import ltd.evilcorp.atox.ui.BaseFragment -import ltd.evilcorp.atox.ui.StatusDialog import ltd.evilcorp.atox.vmFactory import ltd.evilcorp.core.vo.UserStatus +import android.util.Log +import androidx.core.content.FileProvider +import androidx.core.content.res.ResourcesCompat +import androidx.core.graphics.* +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import ltd.evilcorp.atox.BuildConfig +import ltd.evilcorp.atox.ui.* +import ltd.evilcorp.atox.ui.AvatarMaker +import ltd.evilcorp.atox.ui.colorFromStatus +import java.io.File +import java.io.FileOutputStream +import java.io.IOException + private const val TOX_MAX_NAME_LENGTH = 128 private const val TOX_MAX_STATUS_MESSAGE_LENGTH = 1007 private const val QR_CODE_TO_SCREEN_RATIO = 0.5f -private const val QR_CODE_DIALOG_PADDING = 16f // in dp +private const val QR_CODE_PADDING = 16f // in dp +private const val QR_CODE_SHARED_IMAGE_PADDING = 30f // in dp private fun dpToPx(dp: Float, res: Resources): Int = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, res.displayMetrics).toInt() @@ -51,39 +58,50 @@ class UserProfileFragment : BaseFragment(FragmentUse private val vm: UserProfileViewModel by viewModels { vmFactory } private lateinit var currentStatus: UserStatus - private fun colorFromStatus(status: UserStatus) = when (status) { - UserStatus.None -> ResourcesCompat.getColor(resources, R.color.statusAvailable, null) - UserStatus.Away -> ResourcesCompat.getColor(resources, R.color.statusAway, null) - UserStatus.Busy -> ResourcesCompat.getColor(resources, R.color.statusBusy, null) - } override fun onViewCreated(view: View, savedInstanceState: Bundle?) = binding.run { ViewCompat.setOnApplyWindowInsetsListener(view) { _, compat -> val insets = compat.getInsets(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.ime()) - profileCollapsingToolbar.updatePadding(left = insets.left, right = insets.right) - profileToolbar.updatePadding(top = insets.top) + toolbar.updatePadding(left = insets.left, top = insets.top) mainSection.updatePadding(left = insets.left, right = insets.right) compat } - profileToolbar.apply { - setNavigationOnClickListener { - activity?.onBackPressed() - } - } - vm.user.observe(viewLifecycleOwner) { user -> currentStatus = user.status userName.text = user.name userStatusMessage.text = user.statusMessage - userStatus.setColorFilter(colorFromStatus(user.status)) + profileImageLayout.statusIndicator.setColorFilter(colorFromStatus(resources, user.status)) + AvatarMaker(user).setAvatar(profileImageLayout.profileImage) } - userToxId.text = vm.toxId.string() + // Inflating views according to Day/Night theme + if (isNightMode(requireContext())) { + toolbar.setNavigationIcon(R.drawable.ic_back_white) + icEditProfile.setImageResource(R.drawable.ic_edit_white) + copyToxId.setImageResource(R.drawable.ic_copy_white) + createQrCode( + ResourcesCompat.getColor(resources, R.color.pleasantWhite, null), + Color.TRANSPARENT, + imageView = toxIdQr + ) + } else { + toolbar.setNavigationIcon(R.drawable.ic_back_black) + icEditProfile.setImageResource(R.drawable.ic_edit_black) + copyToxId.setImageResource(R.drawable.ic_copy_black) + createQrCode(Color.BLACK, Color.TRANSPARENT, imageView = toxIdQr) + } + setImageButtonRippleDayNight(requireContext(), copyToxId) - // TODO(robinlinden): This should open a nice dialog where you show the QR and have both share and copy buttons. - profileShareId.setOnClickListener { + toolbar.setNavigationOnClickListener { + WindowInsetsControllerCompat(requireActivity().window, view) + .hide(WindowInsetsCompat.Type.ime()) + activity?.onBackPressed() + } + + userToxId.text = vm.toxId.string() + userToxId.setOnClickListener { val shareIntent = Intent().apply { action = Intent.ACTION_SEND type = "text/plain" @@ -91,9 +109,28 @@ class UserProfileFragment : BaseFragment(FragmentUse } startActivity(Intent.createChooser(shareIntent, getString(R.string.tox_id_share))) } - registerForContextMenu(profileShareId) - profileOptions.profileChangeNickname.setOnClickListener { + copyToxId.setOnClickListener { + val clipboard = requireActivity().getSystemService()!! + clipboard.setPrimaryClip(ClipData.newPlainText(getText(R.string.tox_id), vm.toxId.string())) + Toast.makeText(requireContext(), getText(R.string.copied), Toast.LENGTH_SHORT).show() + } + + toxIdQr.setOnClickListener { + vm.viewModelScope.launch { + val qrImageUri = getQrForSharing("tox_id_qr_code") + val shareIntent = Intent().apply { + action = Intent.ACTION_SEND + clipData = ClipData.newRawUri(null, qrImageUri) + type = "image/png" + putExtra(Intent.EXTRA_STREAM, qrImageUri) + flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + } + startActivity(Intent.createChooser(shareIntent, getString(R.string.tox_id_share))) + } + } + + /*profileOptions.profileChangeNickname.setOnClickListener { val nameEdit = EditText(requireContext()).apply { text.append(binding.userName.text) filters = arrayOf(InputFilter.LengthFilter(TOX_MAX_NAME_LENGTH)) @@ -127,61 +164,96 @@ class UserProfileFragment : BaseFragment(FragmentUse profileOptions.profileChangeStatus.setOnClickListener { StatusDialog(requireContext(), currentStatus) { status -> vm.setStatus(status) }.show() - } - - // TODO(robinlinden): Remove hack. It's used to make sure we can scroll to the settings - // further down when in landscape orientation. This is only needed if the view is recreated - // while we're on this screen as Android changes the size of the contents of the NestedScrollView - // when that happens. - if (savedInstanceState != null) { - needsHacks.updatePadding(bottom = (150 * resources.displayMetrics.density).toInt()) - } + }*/ } - override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) = binding.run { - super.onCreateContextMenu(menu, v, menuInfo) - when (v.id) { - R.id.profile_share_id -> requireActivity().menuInflater.inflate( - R.menu.user_profile_share_id_context_menu, - menu - ) - } - } - override fun onContextItemSelected(item: MenuItem): Boolean = binding.run { - return when (item.itemId) { - R.id.copy -> { - val clipboard = requireActivity().getSystemService()!! - clipboard.setPrimaryClip(ClipData.newPlainText(getText(R.string.tox_id), vm.toxId.string())) - Toast.makeText(requireContext(), getText(R.string.copied), Toast.LENGTH_SHORT).show() - true - } - R.id.qr -> { - createQrCodeDialog().show() - true - } - else -> super.onContextItemSelected(item) - } + override fun onDestroyView() = binding.run { + super.onDestroyView() + unbindDrawables(icEditProfile) + unbindDrawables(copyToxId) } - private fun createQrCodeDialog(): AlertDialog { + + /** + * Function will create a QR code for the Tox ID and assign it to the image view if specified. + * @param qrCodeColor The color of the QR code. + * @param backgroundColor The color of the background. + * @param paddingDp The padding to add after each side in the QR code bitmap. Will be painted with backgroundColor. + * @param imageView The image view for whom to assign the QR code bitmap. Can be null. + * @return The QR code bitmap with the specified padding. + */ + private fun createQrCode( + qrCodeColor: Int = Color.BLACK, + backgroundColor: Int = Color.WHITE, + paddingDp: Float = QR_CODE_PADDING, + imageView: ImageView? = null, + ) : Bitmap { + // Creating the QR bitmap val qrData = QrCode.encodeText("tox:%s".format(vm.toxId.string()), QrCode.Ecc.LOW) - var bmp: Bitmap = Bitmap.createBitmap(qrData.size, qrData.size, Bitmap.Config.RGB_565) + var bmpQr: Bitmap = Bitmap.createBitmap(qrData.size, qrData.size, Bitmap.Config.ARGB_8888) + bmpQr.setHasAlpha(true) for (x in 0 until qrData.size) { for (y in 0 until qrData.size) { - bmp.setPixel(x, y, if (qrData.getModule(x, y)) Color.BLACK else Color.WHITE) + bmpQr.setPixel(x, y, if (qrData.getModule(x, y)) qrCodeColor else backgroundColor) } } + // Scaling the QR bitmap to be half of the screen's width val metrics = resources.displayMetrics val size = (min(metrics.widthPixels, metrics.heightPixels) * QR_CODE_TO_SCREEN_RATIO).roundToInt() - bmp = bmp.scale(size, size, false) - val qrCode = ImageView(requireContext()).apply { - setPadding(dpToPx(QR_CODE_DIALOG_PADDING, resources)) - setImageBitmap(bmp) + bmpQr = bmpQr.scale(size, size, false) + + // Adding a padding to the QR bitmap + val paddingPx = dpToPx(paddingDp, resources) + val bmpQrWithPadding = + Bitmap.createBitmap(bmpQr.width + 2 * paddingPx, bmpQr.height + 2 * paddingPx, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bmpQrWithPadding) + canvas.drawARGB(backgroundColor.alpha, backgroundColor.red, backgroundColor.green, backgroundColor.blue) + canvas.drawBitmap(bmpQr, paddingPx.toFloat(), paddingPx.toFloat(), null) + + imageView?.apply { + setPadding(paddingPx) + setImageBitmap(bmpQr) + } + + return bmpQrWithPadding + } + + + /** + * Function will save the image with the input bitmap and name to the cache directory as a png format. + * Then it will return the Uri of the image. + * @param image The bitmap of the image. + * @param name Image file name. + * @return The Uri of the image or null. + */ + private fun saveImageForSharing(image: Bitmap, name: String): Uri? { + val imagesFolder = File(requireActivity().cacheDir, "images") + var uri: Uri? = null + try { + imagesFolder.mkdirs() + val file = File(imagesFolder, "$name.png") + val stream = FileOutputStream(file) + image.compress(Bitmap.CompressFormat.PNG, 90, stream) + stream.flush() + stream.close() + uri = FileProvider.getUriForFile(requireActivity(), "${BuildConfig.APPLICATION_ID}.fileprovider", file) + } catch (e: IOException) { + Log.d("corp.atox.debug", "IOException while trying to write file for sharing: " + e.message) + } + return uri + } + + + /** + * Function will run in a different thread, create a new QR code for sharing and return the Uri. + * @param name The image name. + * @return The Uri for the QR code. + */ + private suspend fun getQrForSharing(name: String) : Uri? { + return withContext(Dispatchers.IO) { + val bmp = createQrCode(paddingDp = QR_CODE_SHARED_IMAGE_PADDING) + saveImageForSharing(bmp, name) } - return AlertDialog.Builder(requireContext()) - .setTitle(R.string.tox_id) - .setView(qrCode) - .create() } } diff --git a/atox/src/main/res/drawable/attach_file.xml b/atox/src/main/res/drawable/ic_attach_file.xml similarity index 100% rename from atox/src/main/res/drawable/attach_file.xml rename to atox/src/main/res/drawable/ic_attach_file.xml diff --git a/atox/src/main/res/drawable/back.xml b/atox/src/main/res/drawable/ic_back_black.xml similarity index 89% rename from atox/src/main/res/drawable/back.xml rename to atox/src/main/res/drawable/ic_back_black.xml index 9f690964..0ee09c33 100644 --- a/atox/src/main/res/drawable/back.xml +++ b/atox/src/main/res/drawable/ic_back_black.xml @@ -4,6 +4,6 @@ android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0"> - diff --git a/atox/src/main/res/drawable/ic_back_white.xml b/atox/src/main/res/drawable/ic_back_white.xml new file mode 100644 index 00000000..c6cb4d30 --- /dev/null +++ b/atox/src/main/res/drawable/ic_back_white.xml @@ -0,0 +1,9 @@ + + + + diff --git a/atox/src/main/res/drawable/ic_copy_black.xml b/atox/src/main/res/drawable/ic_copy_black.xml new file mode 100644 index 00000000..2aa2065e --- /dev/null +++ b/atox/src/main/res/drawable/ic_copy_black.xml @@ -0,0 +1,9 @@ + + + diff --git a/atox/src/main/res/drawable/ic_copy_white.xml b/atox/src/main/res/drawable/ic_copy_white.xml new file mode 100644 index 00000000..18297bd4 --- /dev/null +++ b/atox/src/main/res/drawable/ic_copy_white.xml @@ -0,0 +1,5 @@ + + + diff --git a/atox/src/main/res/drawable/ic_edit_black.xml b/atox/src/main/res/drawable/ic_edit_black.xml new file mode 100644 index 00000000..d5e54700 --- /dev/null +++ b/atox/src/main/res/drawable/ic_edit_black.xml @@ -0,0 +1,9 @@ + + + diff --git a/atox/src/main/res/drawable/ic_edit_white.xml b/atox/src/main/res/drawable/ic_edit_white.xml new file mode 100644 index 00000000..e260ac76 --- /dev/null +++ b/atox/src/main/res/drawable/ic_edit_white.xml @@ -0,0 +1,5 @@ + + + diff --git a/atox/src/main/res/drawable/send.xml b/atox/src/main/res/drawable/ic_send.xml similarity index 100% rename from atox/src/main/res/drawable/send.xml rename to atox/src/main/res/drawable/ic_send.xml diff --git a/atox/src/main/res/drawable/side_nav_bar_background.xml b/atox/src/main/res/drawable/side_nav_bar_background.xml index 467ca417..9be4b75d 100644 --- a/atox/src/main/res/drawable/side_nav_bar_background.xml +++ b/atox/src/main/res/drawable/side_nav_bar_background.xml @@ -3,7 +3,7 @@ diff --git a/atox/src/main/res/layout/dialog_receive_share.xml b/atox/src/main/res/layout/dialog_receive_share.xml index 34980c82..698909f4 100644 --- a/atox/src/main/res/layout/dialog_receive_share.xml +++ b/atox/src/main/res/layout/dialog_receive_share.xml @@ -27,7 +27,7 @@ android:fontFamily="sans-serif-light" android:gravity="center" android:text="@string/receive_share_share_to" - android:textColor="@color/textWhiteColor" + android:textColor="@android:color/white" android:textSize="20sp" /> @@ -91,7 +91,7 @@ android:layout_gravity="center" android:fontFamily="sans-serif-light" android:text="@string/status_away" - android:textColor="@color/textWhiteColor" + android:textColor="@android:color/white" android:textSize="16sp" /> @@ -123,7 +123,7 @@ android:layout_gravity="center" android:fontFamily="sans-serif-light" android:text="@string/status_busy" - android:textColor="@color/textWhiteColor" + android:textColor="@android:color/white" android:textSize="16sp" /> @@ -158,7 +158,7 @@ diff --git a/atox/src/main/res/layout/fragment_add_contact.xml b/atox/src/main/res/layout/fragment_add_contact.xml index 348f1fd8..31597305 100644 --- a/atox/src/main/res/layout/fragment_add_contact.xml +++ b/atox/src/main/res/layout/fragment_add_contact.xml @@ -9,7 +9,7 @@ diff --git a/atox/src/main/res/layout/fragment_chat.xml b/atox/src/main/res/layout/fragment_chat.xml index 904fda03..209c3320 100644 --- a/atox/src/main/res/layout/fragment_chat.xml +++ b/atox/src/main/res/layout/fragment_chat.xml @@ -7,7 +7,7 @@ @@ -88,6 +88,6 @@ android:background="@android:color/transparent" android:contentDescription="@string/attach_file" android:paddingHorizontal="4dp" - android:src="@drawable/attach_file"/> + android:src="@drawable/ic_attach_file"/> diff --git a/atox/src/main/res/layout/fragment_contact_list.xml b/atox/src/main/res/layout/fragment_contact_list.xml index 06f36e8a..d1fbb997 100644 --- a/atox/src/main/res/layout/fragment_contact_list.xml +++ b/atox/src/main/res/layout/fragment_contact_list.xml @@ -12,12 +12,12 @@ + android:theme="@style/Theme.aTox.AppBarOverlay"> + android:background="?attr/colorSurface" + app:popupTheme="@style/Theme.aTox.PopupOverlay"/> diff --git a/atox/src/main/res/layout/fragment_profile.xml b/atox/src/main/res/layout/fragment_profile.xml index 5643fdde..52e9187f 100644 --- a/atox/src/main/res/layout/fragment_profile.xml +++ b/atox/src/main/res/layout/fragment_profile.xml @@ -8,7 +8,7 @@ diff --git a/atox/src/main/res/layout/fragment_user_profile.xml b/atox/src/main/res/layout/fragment_user_profile.xml index 603d1860..7be8d919 100644 --- a/atox/src/main/res/layout/fragment_user_profile.xml +++ b/atox/src/main/res/layout/fragment_user_profile.xml @@ -5,136 +5,169 @@ android:layout_width="match_parent" android:layout_height="match_parent" tools:context="ltd.evilcorp.atox.ui.user_profile.UserProfileFragment"> + + + - + + + + + + + + - + + - + + + + - - - - - - - - - + android:layout_marginStart="25dp" + android:layout_toEndOf="@id/profileImageLayout" + android:singleLine="true" + tools:text="Name goes here" /> + - - - - - - + + + + + + + - + + + + - + android:layout_alignParentStart="true" + android:layout_marginStart="25dp" + android:layout_marginTop="25dp" + android:layout_marginEnd="10dp" + android:layout_marginBottom="25dp" + android:layout_toStartOf="@id/copy_tox_id"> + - - - - - - - - + android:id="@+id/user_tox_id" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="16dp" + android:textSize="14sp" + tools:text="TOX ID GOES HERE" /> + + + + + + + + + + + + + + + + + + + + diff --git a/atox/src/main/res/layout/profile_image_layout.xml b/atox/src/main/res/layout/profile_image_layout.xml index 9005096d..2b22349c 100644 --- a/atox/src/main/res/layout/profile_image_layout.xml +++ b/atox/src/main/res/layout/profile_image_layout.xml @@ -7,16 +7,16 @@ android:layout_height="wrap_content"> - - - - - - - - - - - - - diff --git a/atox/src/main/res/menu/user_profile_share_id_context_menu.xml b/atox/src/main/res/menu/user_profile_share_id_context_menu.xml deleted file mode 100644 index ac108283..00000000 --- a/atox/src/main/res/menu/user_profile_share_id_context_menu.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/atox/src/main/res/values-night-v21/styles.xml b/atox/src/main/res/values-night-v21/styles.xml index 70747779..d1e85dfa 100644 --- a/atox/src/main/res/values-night-v21/styles.xml +++ b/atox/src/main/res/values-night-v21/styles.xml @@ -1,11 +1,19 @@ - + + + + + diff --git a/atox/src/main/res/values-night-v23/styles.xml b/atox/src/main/res/values-night-v23/styles.xml new file mode 100644 index 00000000..d1e85dfa --- /dev/null +++ b/atox/src/main/res/values-night-v23/styles.xml @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/atox/src/main/res/values-night-v29/styles.xml b/atox/src/main/res/values-night-v29/styles.xml deleted file mode 100644 index fe32258a..00000000 --- a/atox/src/main/res/values-night-v29/styles.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - diff --git a/atox/src/main/res/values-v21/styles.xml b/atox/src/main/res/values-v21/styles.xml index f51e3dc4..0c06baf0 100644 --- a/atox/src/main/res/values-v21/styles.xml +++ b/atox/src/main/res/values-v21/styles.xml @@ -1,11 +1,19 @@ - + + + + + diff --git a/atox/src/main/res/values-v23/styles.xml b/atox/src/main/res/values-v23/styles.xml new file mode 100644 index 00000000..394a4e24 --- /dev/null +++ b/atox/src/main/res/values-v23/styles.xml @@ -0,0 +1,20 @@ + + + + + + + + + diff --git a/atox/src/main/res/values-v29/styles.xml b/atox/src/main/res/values-v29/styles.xml deleted file mode 100644 index fe32258a..00000000 --- a/atox/src/main/res/values-v29/styles.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - diff --git a/atox/src/main/res/values/colors.xml b/atox/src/main/res/values/colors.xml index e0e60755..2dcff4b5 100644 --- a/atox/src/main/res/values/colors.xml +++ b/atox/src/main/res/values/colors.xml @@ -1,11 +1,12 @@ - #1976D2 - #1565C0 - #0D47A1 - #D81B60 + #73a0f4 + #0053e1 + @color/colorPrimaryLight #555 + #f2f5ed + @color/colorGray @android:color/holo_red_dark @android:color/holo_orange_light @@ -13,8 +14,6 @@ #E0E0E0 - #FFFFFF - #546E7A #ECEFF1 #3E4651 diff --git a/atox/src/main/res/values/strings.xml b/atox/src/main/res/values/strings.xml index 882165ac..bce97e10 100644 --- a/atox/src/main/res/values/strings.xml +++ b/atox/src/main/res/values/strings.xml @@ -177,4 +177,10 @@ This **only** blocks screenshots on **your** device, and provides no protection against your contacts taking screenshots or otherwise saving your conversations Return to chat Toggle speakerphone + Edit profile icon + Share this Tox ID with others who you want them to send you a friendship request. + Just click on it to share, or click the copy icon. + Or ask them to scan in the app this QR code. + QR code of Tox ID + Copy Tox ID button \ No newline at end of file diff --git a/atox/src/main/res/values/styles.xml b/atox/src/main/res/values/styles.xml index c8c41c42..38cc8a09 100644 --- a/atox/src/main/res/values/styles.xml +++ b/atox/src/main/res/values/styles.xml @@ -1,11 +1,13 @@ - - - diff --git a/atox/src/main/res/xml/file_paths.xml b/atox/src/main/res/xml/file_paths.xml index 46567504..a3bfdc98 100644 --- a/atox/src/main/res/xml/file_paths.xml +++ b/atox/src/main/res/xml/file_paths.xml @@ -1,4 +1,6 @@ + + From 8220a029e14c817ee1f70185bf7f3f481ac4dd8c Mon Sep 17 00:00:00 2001 From: roihershberg Date: Fri, 17 Dec 2021 01:43:02 +0200 Subject: [PATCH 03/11] Added edit profile screen + related code cleanup Added an edit profile screen for editing the user profile. The screen has a dropdown for changing the status and a nice dialog for changing the name and the status message. Also a lot of related code clean up. --- atox/src/main/kotlin/di/ViewModelModule.kt | 13 + atox/src/main/kotlin/ui/AvatarMaker.kt | 24 +- atox/src/main/kotlin/ui/Util.kt | 28 +-- .../kotlin/ui/contactlist/ContactAdapter.kt | 2 +- .../EditTextValueDialog.kt | 128 ++++++++++ .../EditTextValueDialogViewModel.kt | 17 ++ .../EditUserProfileFragment.kt | 137 +++++++++++ .../EditUserProfileViewModel.kt | 26 ++ .../edit_user_profile/StatusArrayAdapter.kt | 49 ++++ .../ui/user_profile/UserProfileFragment.kt | 67 +----- atox/src/main/res/anim/slide_in_left.xml | 6 + atox/src/main/res/anim/slide_in_right.xml | 2 +- atox/src/main/res/anim/slide_out_left.xml | 6 + atox/src/main/res/anim/slide_out_right.xml | 2 +- .../main/res/color/box_stroke_color_day.xml | 14 ++ .../main/res/color/box_stroke_color_night.xml | 14 ++ .../main/res/color/hint_text_color_day.xml | 11 + .../main/res/color/hint_text_color_night.xml | 11 + .../res/color/status_available_color_list.xml | 4 + .../main/res/color/status_away_color_list.xml | 4 + .../main/res/color/status_busy_color_list.xml | 4 + .../res/color/trailing_icon_color_day.xml | 5 + .../res/color/trailing_icon_color_night.xml | 5 + atox/src/main/res/drawable/ic_available.xml | 8 + atox/src/main/res/drawable/ic_away.xml | 8 + atox/src/main/res/drawable/ic_busy.xml | 8 + .../{ic_copy_black.xml => ic_copy.xml} | 2 +- atox/src/main/res/drawable/ic_copy_white.xml | 5 - .../{ic_edit_black.xml => ic_edit.xml} | 2 +- atox/src/main/res/drawable/ic_edit_white.xml | 5 - atox/src/main/res/drawable/ic_person.xml | 9 + atox/src/main/res/drawable/ic_status.xml | 15 ++ .../main/res/drawable/ic_status_message.xml | 9 + .../res/drawable/side_nav_bar_background.xml | 2 +- atox/src/main/res/layout/dialog_status.xml | 2 +- atox/src/main/res/layout/edit_status_item.xml | 23 ++ .../res/layout/edit_text_value_dialog.xml | 56 +++++ .../res/layout/fragment_edit_user_profile.xml | 222 ++++++++++++++++++ .../src/main/res/layout/fragment_settings.xml | 2 +- .../main/res/layout/fragment_user_profile.xml | 19 +- atox/src/main/res/navigation/nav_graph.xml | 15 +- atox/src/main/res/values-ar/strings.xml | 5 - atox/src/main/res/values-bs/strings.xml | 5 - atox/src/main/res/values-de/strings.xml | 5 - atox/src/main/res/values-el/strings.xml | 5 - atox/src/main/res/values-es/strings.xml | 5 - atox/src/main/res/values-et/strings.xml | 5 - atox/src/main/res/values-fa/strings.xml | 5 - atox/src/main/res/values-fr/strings.xml | 5 - atox/src/main/res/values-hr/strings.xml | 5 - atox/src/main/res/values-hu/strings.xml | 5 - atox/src/main/res/values-hy/strings.xml | 5 - atox/src/main/res/values-it/strings.xml | 5 - atox/src/main/res/values-lt/strings.xml | 5 - atox/src/main/res/values-nb-rNO/strings.xml | 5 - atox/src/main/res/values-night-v21/styles.xml | 19 -- atox/src/main/res/values-night-v23/styles.xml | 19 -- atox/src/main/res/values-night/styles.xml | 16 ++ atox/src/main/res/values-pl/strings.xml | 5 - atox/src/main/res/values-pt-rBR/strings.xml | 5 - atox/src/main/res/values-pt/strings.xml | 5 - atox/src/main/res/values-ro/strings.xml | 5 - atox/src/main/res/values-ru/strings.xml | 5 - atox/src/main/res/values-sk/strings.xml | 5 - atox/src/main/res/values-sv/strings.xml | 5 - atox/src/main/res/values-tr/strings.xml | 5 - atox/src/main/res/values-uk/strings.xml | 5 - atox/src/main/res/values-v21/styles.xml | 19 -- atox/src/main/res/values-v23/styles.xml | 14 +- atox/src/main/res/values-v27/styles.xml | 26 ++ atox/src/main/res/values-zh-rCN/strings.xml | 5 - atox/src/main/res/values/colors.xml | 3 +- atox/src/main/res/values/strings.xml | 21 +- atox/src/main/res/values/styles.xml | 33 ++- 74 files changed, 974 insertions(+), 307 deletions(-) create mode 100644 atox/src/main/kotlin/ui/edit_text_value_dialog/EditTextValueDialog.kt create mode 100644 atox/src/main/kotlin/ui/edit_text_value_dialog/EditTextValueDialogViewModel.kt create mode 100644 atox/src/main/kotlin/ui/edit_user_profile/EditUserProfileFragment.kt create mode 100644 atox/src/main/kotlin/ui/edit_user_profile/EditUserProfileViewModel.kt create mode 100644 atox/src/main/kotlin/ui/edit_user_profile/StatusArrayAdapter.kt create mode 100644 atox/src/main/res/anim/slide_in_left.xml create mode 100644 atox/src/main/res/anim/slide_out_left.xml create mode 100644 atox/src/main/res/color/box_stroke_color_day.xml create mode 100644 atox/src/main/res/color/box_stroke_color_night.xml create mode 100644 atox/src/main/res/color/hint_text_color_day.xml create mode 100644 atox/src/main/res/color/hint_text_color_night.xml create mode 100644 atox/src/main/res/color/status_available_color_list.xml create mode 100644 atox/src/main/res/color/status_away_color_list.xml create mode 100644 atox/src/main/res/color/status_busy_color_list.xml create mode 100644 atox/src/main/res/color/trailing_icon_color_day.xml create mode 100644 atox/src/main/res/color/trailing_icon_color_night.xml create mode 100644 atox/src/main/res/drawable/ic_available.xml create mode 100644 atox/src/main/res/drawable/ic_away.xml create mode 100644 atox/src/main/res/drawable/ic_busy.xml rename atox/src/main/res/drawable/{ic_copy_black.xml => ic_copy.xml} (89%) delete mode 100644 atox/src/main/res/drawable/ic_copy_white.xml rename atox/src/main/res/drawable/{ic_edit_black.xml => ic_edit.xml} (89%) delete mode 100644 atox/src/main/res/drawable/ic_edit_white.xml create mode 100644 atox/src/main/res/drawable/ic_person.xml create mode 100644 atox/src/main/res/drawable/ic_status.xml create mode 100644 atox/src/main/res/drawable/ic_status_message.xml create mode 100644 atox/src/main/res/layout/edit_status_item.xml create mode 100644 atox/src/main/res/layout/edit_text_value_dialog.xml create mode 100644 atox/src/main/res/layout/fragment_edit_user_profile.xml delete mode 100644 atox/src/main/res/values-night-v21/styles.xml delete mode 100644 atox/src/main/res/values-night-v23/styles.xml create mode 100644 atox/src/main/res/values-night/styles.xml delete mode 100644 atox/src/main/res/values-v21/styles.xml create mode 100644 atox/src/main/res/values-v27/styles.xml diff --git a/atox/src/main/kotlin/di/ViewModelModule.kt b/atox/src/main/kotlin/di/ViewModelModule.kt index 94088ad5..2778b85a 100644 --- a/atox/src/main/kotlin/di/ViewModelModule.kt +++ b/atox/src/main/kotlin/di/ViewModelModule.kt @@ -16,6 +16,9 @@ import ltd.evilcorp.atox.ui.chat.ChatViewModel import ltd.evilcorp.atox.ui.contact_profile.ContactProfileViewModel import ltd.evilcorp.atox.ui.contactlist.ContactListViewModel import ltd.evilcorp.atox.ui.create_profile.CreateProfileViewModel +import ltd.evilcorp.atox.ui.edit_text_value_dialog.EditTextValueDialog +import ltd.evilcorp.atox.ui.edit_text_value_dialog.EditTextValueDialogViewModel +import ltd.evilcorp.atox.ui.edit_user_profile.EditUserProfileViewModel import ltd.evilcorp.atox.ui.friend_request.FriendRequestViewModel import ltd.evilcorp.atox.ui.settings.SettingsViewModel import ltd.evilcorp.atox.ui.user_profile.UserProfileViewModel @@ -77,4 +80,14 @@ abstract class ViewModelModule { @IntoMap @ViewModelKey(UserProfileViewModel::class) abstract fun bindUserProfileViewModel(vm: UserProfileViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(EditUserProfileViewModel::class) + abstract fun bindEditUserProfileViewModel(vm: EditUserProfileViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(EditTextValueDialogViewModel::class) + abstract fun bindEditTextValueDialogViewModel(vm: EditTextValueDialogViewModel): ViewModel } diff --git a/atox/src/main/kotlin/ui/AvatarMaker.kt b/atox/src/main/kotlin/ui/AvatarMaker.kt index b2abda80..2979ccc1 100644 --- a/atox/src/main/kotlin/ui/AvatarMaker.kt +++ b/atox/src/main/kotlin/ui/AvatarMaker.kt @@ -13,6 +13,12 @@ import ltd.evilcorp.core.vo.User import kotlin.math.abs +internal enum class SizeUnit { + DP, + PX, +} + + /** * Class for creating an avatar for user or contact and setting it in the ImageView */ @@ -57,20 +63,30 @@ internal class AvatarMaker { * otherwise a new avatar image will be created based on the initials of the name * and the public key for the background color. * @param imageView The image view for whom to set the avatar image. - * @param sizeDp The size of the avatar image in dp units. + * @param size The size of the avatar image in the units specified in sizeUnit (default: DP units). + * @param sizeUnit The size unit of size parameter. */ - fun setAvatar(imageView: ImageView, sizeDp: Int = DEFAULT_AVATAR_SIZE_DP) = + fun setAvatar(imageView: ImageView, size: Int = DEFAULT_AVATAR_SIZE_DP, sizeUnit: SizeUnit = SizeUnit.DP) = if (avatarUri.isNotEmpty()) { imageView.setImageURI(Uri.parse(avatarUri)) } else { - val side = (sizeDp * imageView.resources.displayMetrics.density).toInt() + val side: Int + val textScale: Float + + if (sizeUnit == SizeUnit.DP) { + side = (size * imageView.resources.displayMetrics.density).toInt() + textScale = size.toFloat() / DEFAULT_AVATAR_SIZE_DP + } else { + side = size + textScale = size.toFloat() / dpToPx(DEFAULT_AVATAR_SIZE_DP.toFloat(), imageView.resources) + } + val bitmap = Bitmap.createBitmap(side, side, Bitmap.Config.ARGB_8888) val canvas = Canvas(bitmap) val rect = RectF(0f, 0f, bitmap.width.toFloat(), bitmap.height.toFloat()) val colors = imageView.resources.getIntArray(R.array.contactBackgrounds) val backgroundPaint = Paint().apply { color = colors[abs(publicKey.hashCode()).rem(colors.size)] } - val textScale = sizeDp.toFloat() / DEFAULT_AVATAR_SIZE_DP val textPaint = Paint().apply { color = Color.WHITE textSize = imageView.resources.getDimension(R.dimen.contact_avatar_placeholder_text) * textScale diff --git a/atox/src/main/kotlin/ui/Util.kt b/atox/src/main/kotlin/ui/Util.kt index 49581918..adce26de 100644 --- a/atox/src/main/kotlin/ui/Util.kt +++ b/atox/src/main/kotlin/ui/Util.kt @@ -12,8 +12,7 @@ import android.graphics.Color import android.graphics.drawable.GradientDrawable import android.graphics.drawable.RippleDrawable import android.os.Build -import android.view.View -import android.view.ViewGroup +import android.util.TypedValue import android.widget.ImageButton import androidx.core.content.res.ResourcesCompat import ltd.evilcorp.atox.R @@ -51,6 +50,15 @@ internal fun colorFromStatus(resources: Resources, status: UserStatus) = when (s } +/** + * Function will convert dp (Density Pixels) units to px (Pixels) units + * @param dp The dp units. + * @return The px units as Int. + */ +internal fun dpToPx(dp: Float, res: Resources): Int = + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, res.displayMetrics).toInt() + + /** * Function will return whether or not night mode is set. * @param context The related context. @@ -95,19 +103,3 @@ internal fun setImageButtonRipple(imageButton: ImageButton, colorInt: Int) { imageButton.background = ripple } } - - -/** - * Function will release the resources of a view, hopefully prevent memory leaks. - * @param view The view for whom to release the resources. - */ -internal fun unbindDrawables(view: View) { - if (view.background != null) { - view.background.callback = null - } - if (view is ViewGroup) { - for (i in 0 until view.childCount) unbindDrawables(view.getChildAt(i)) - view.removeAllViews() - view.setBackgroundResource(0) - } -} diff --git a/atox/src/main/kotlin/ui/contactlist/ContactAdapter.kt b/atox/src/main/kotlin/ui/contactlist/ContactAdapter.kt index 3c4c5490..96fe7179 100644 --- a/atox/src/main/kotlin/ui/contactlist/ContactAdapter.kt +++ b/atox/src/main/kotlin/ui/contactlist/ContactAdapter.kt @@ -103,7 +103,7 @@ class ContactAdapter( draftMessage.isNotEmpty() -> { vh.statusMessage.text = resources.getString(R.string.draft_message, draftMessage) vh.statusMessage.setTextColor( - ResourcesCompat.getColor(resources, R.color.colorAccent, null) + ResourcesCompat.getColor(resources, R.color.colorSecondary, null) ) } else -> { diff --git a/atox/src/main/kotlin/ui/edit_text_value_dialog/EditTextValueDialog.kt b/atox/src/main/kotlin/ui/edit_text_value_dialog/EditTextValueDialog.kt new file mode 100644 index 00000000..39aa99f1 --- /dev/null +++ b/atox/src/main/kotlin/ui/edit_text_value_dialog/EditTextValueDialog.kt @@ -0,0 +1,128 @@ +// SPDX-FileCopyrightText: 2020 aTox contributors +// +// SPDX-License-Identifier: GPL-3.0-only + +package ltd.evilcorp.atox.ui.edit_text_value_dialog + +import android.os.* +import android.text.InputFilter +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.EditText +import androidx.fragment.app.viewModels +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import ltd.evilcorp.atox.databinding.EditTextValueDialogBinding +import ltd.evilcorp.atox.vmFactory + + +class EditTextValueDialog() : BottomSheetDialogFragment() { + + private val vm: EditTextValueDialogViewModel by viewModels { vmFactory } + private var _binding: EditTextValueDialogBinding? = null + private val binding get() = _binding!! + + private var title: String? = null + private var hint: String? = null + private var defaultValue: String? = null + private var singleLine: Boolean = true + private var filters: Array? = null + private lateinit var setTextValueBlock: (String) -> Unit + + constructor( + title: String, + hint: String, + defaultValue: String? = null, + singleLine: Boolean = true, + filters: Array? = null, + setTextValueBlock: (String) -> Unit + ) : this() { + this.title = title + this.hint = hint + this.defaultValue = defaultValue + this.singleLine = singleLine + this.filters = filters + this.setTextValueBlock = setTextValueBlock + } + + companion object { + const val TAG = "EditTextValueDialog" + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + _binding = EditTextValueDialogBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onDestroyView() { + _binding = null + super.onDestroyView() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) = binding.run { + super.onViewCreated(view, savedInstanceState) + + val editText: EditText? = textField.editText + + // Assigning values from constructor to view model + title?.run { + vm.title = this + hint?.run { vm.hint = this } + defaultValue?.run { + vm.defaultValue = this + vm.selectionStart = length + vm.selectionEnd = length + } + vm.singleLine = singleLine + filters?.run { vm.filters = this } + setTextValueBlock.run { vm.setTextValueBlock = this } + } + + // Assigning values to the views according to the given parameters + titleTextView.text = vm.title + textField.hint = vm.hint + vm.defaultValue?.run { + editText?.setText(this) + } + editText?.isSingleLine = vm.singleLine + vm.filters?.run { + editText?.filters = this + + // Displaying the max length as a counter + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + try { + (this.first { it is InputFilter.LengthFilter } as InputFilter.LengthFilter).run { + textField.isCounterEnabled = true + textField.counterMaxLength = max + } + } catch (e: NoSuchElementException) { + } + } + } + vm.selectionStart?.let { selectionStart -> + vm.selectionEnd?.let { selectionEnd -> + editText?.setSelection(selectionStart, selectionEnd) + } + } + + cancel.setOnClickListener { dismiss() } + save.setOnClickListener { + vm.setTextValueBlock(textField.editText?.text.toString()) + dismiss() + } + } + + override fun onResume() = binding.run { + textField.editText?.requestFocus() + super.onResume() + } + + override fun onSaveInstanceState(outState: Bundle) = binding.run { + val editText: EditText? = textField.editText + + vm.defaultValue = editText?.text.toString() + vm.selectionStart = editText?.selectionStart + vm.selectionEnd = editText?.selectionEnd + super.onSaveInstanceState(outState) + } +} diff --git a/atox/src/main/kotlin/ui/edit_text_value_dialog/EditTextValueDialogViewModel.kt b/atox/src/main/kotlin/ui/edit_text_value_dialog/EditTextValueDialogViewModel.kt new file mode 100644 index 00000000..9185891c --- /dev/null +++ b/atox/src/main/kotlin/ui/edit_text_value_dialog/EditTextValueDialogViewModel.kt @@ -0,0 +1,17 @@ +package ltd.evilcorp.atox.ui.edit_text_value_dialog + +import android.text.InputFilter +import androidx.lifecycle.ViewModel +import javax.inject.Inject + +class EditTextValueDialogViewModel @Inject constructor() : ViewModel() { + var title: String? = null + var hint: String? = null + var defaultValue: String? = null + var singleLine: Boolean = true + var filters: Array? = null + lateinit var setTextValueBlock: (String) -> Unit + + var selectionStart: Int? = null + var selectionEnd: Int? = null +} diff --git a/atox/src/main/kotlin/ui/edit_user_profile/EditUserProfileFragment.kt b/atox/src/main/kotlin/ui/edit_user_profile/EditUserProfileFragment.kt new file mode 100644 index 00000000..91d76964 --- /dev/null +++ b/atox/src/main/kotlin/ui/edit_user_profile/EditUserProfileFragment.kt @@ -0,0 +1,137 @@ +package ltd.evilcorp.atox.ui.edit_user_profile + +import android.os.Bundle +import android.text.InputFilter +import android.view.View +import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat +import androidx.core.view.updatePadding +import androidx.core.widget.doOnTextChanged +import androidx.fragment.app.viewModels +import ltd.evilcorp.atox.R +import ltd.evilcorp.atox.databinding.FragmentEditUserProfileBinding +import ltd.evilcorp.atox.ui.* +import ltd.evilcorp.atox.ui.AvatarMaker +import ltd.evilcorp.atox.ui.SizeUnit +import ltd.evilcorp.atox.ui.edit_text_value_dialog.EditTextValueDialog +import ltd.evilcorp.atox.ui.isNightMode +import ltd.evilcorp.atox.vmFactory +import ltd.evilcorp.core.vo.UserStatus +import kotlin.math.min + +private const val TOX_MAX_NAME_LENGTH = 128 +private const val TOX_MAX_STATUS_MESSAGE_LENGTH = 1007 + +private const val AVATAR_IMAGE_TO_SCREEN_RATIO = 1f/3 + +class EditUserProfileFragment : BaseFragment(FragmentEditUserProfileBinding::inflate) { + private val vm: EditUserProfileViewModel by viewModels { vmFactory } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) = binding.run { + super.onViewCreated(view, savedInstanceState) + + ViewCompat.setOnApplyWindowInsetsListener(view) { _, compat -> + val insets = compat.getInsets(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.ime()) + toolbar.updatePadding(left = insets.left, top = insets.top) + mainSection.updatePadding(left = insets.left, right = insets.right) + compat + } + + // Inflating views according to Day/Night theme + if (isNightMode(requireContext())) { + toolbar.setNavigationIcon(R.drawable.ic_back_white) + + ContextCompat.getColorStateList(requireContext(), R.color.box_stroke_color_night)?.run { + editStatus.setBoxStrokeColorStateList(this) + } + editStatus.defaultHintTextColor = ContextCompat.getColorStateList(requireContext(), R.color.hint_text_color_night) + editStatus.setEndIconTintList(ContextCompat.getColorStateList(requireContext(), R.color.trailing_icon_color_night)) + } else { + toolbar.setNavigationIcon(R.drawable.ic_back_black) + } + + toolbar.setNavigationOnClickListener { + WindowInsetsControllerCompat(requireActivity().window, view) + .hide(WindowInsetsCompat.Type.ime()) + activity?.onBackPressed() + } + + // Setting the adapter for edit status + val statusList = resources.getStringArray(R.array.status_list) + val adapter = StatusArrayAdapter(requireContext(), R.layout.edit_status_item, R.id.item_text, statusList) + editStatusText.setAdapter(adapter) + + // Setting the icon and the status according to the chosen status + editStatusText.doOnTextChanged { text, _, _, _ -> + when (text.toString()) { + getString(R.string.status_available) -> { + editStatus.setStartIconTintList( + ResourcesCompat.getColorStateList(resources, R.color.status_available_color_list, null) + ) + vm.setStatus(UserStatus.None) + } + getString(R.string.status_away) -> { + editStatus.setStartIconTintList( + ResourcesCompat.getColorStateList(resources, R.color.status_away_color_list, null) + ) + vm.setStatus(UserStatus.Away) + } + getString(R.string.status_busy) -> { + editStatus.setStartIconTintList( + ResourcesCompat.getColorStateList(resources, R.color.status_busy_color_list, null) + ) + vm.setStatus(UserStatus.Busy) + } + } + } + + // Getting the avatar image side value according to the screen resolution + val metrics = resources.displayMetrics + val side = (min(metrics.widthPixels, metrics.heightPixels) * AVATAR_IMAGE_TO_SCREEN_RATIO).toInt() + + vm.user.observe(viewLifecycleOwner) { user -> + if (vm.statusModifiedFromDropdown) { + vm.statusModifiedFromDropdown = false + } else { + AvatarMaker(user).setAvatar(avatarImage, side, SizeUnit.PX) + userName.text = user.name + editStatusText.setText( + when (user.status) { + UserStatus.None -> getString(R.string.status_available) + UserStatus.Away -> getString(R.string.status_away) + UserStatus.Busy -> getString(R.string.status_busy) + }, + false + ) + statusMessage.text = user.statusMessage + } + } + + editName.setOnClickListener { + EditTextValueDialog( + title = getString(R.string.edit_name), + hint = getString(R.string.name), + defaultValue = userName.text.toString(), + filters = arrayOf(InputFilter.LengthFilter(TOX_MAX_NAME_LENGTH)) + ) { + vm.setName(it) + }.show(requireActivity().supportFragmentManager, EditTextValueDialog.TAG) + } + + editStatusMessage.setOnClickListener { + EditTextValueDialog( + title = getString(R.string.edit_status_message), + hint = getString(R.string.status_message), + defaultValue = statusMessage.text.toString(), + singleLine = false, + filters = arrayOf(InputFilter.LengthFilter(TOX_MAX_STATUS_MESSAGE_LENGTH)) + ) { + vm.setStatusMessage(it) + }.show(requireActivity().supportFragmentManager, EditTextValueDialog.TAG) + } + + } +} diff --git a/atox/src/main/kotlin/ui/edit_user_profile/EditUserProfileViewModel.kt b/atox/src/main/kotlin/ui/edit_user_profile/EditUserProfileViewModel.kt new file mode 100644 index 00000000..6b35a0a5 --- /dev/null +++ b/atox/src/main/kotlin/ui/edit_user_profile/EditUserProfileViewModel.kt @@ -0,0 +1,26 @@ +package ltd.evilcorp.atox.ui.edit_user_profile + +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.asLiveData +import ltd.evilcorp.core.vo.User +import ltd.evilcorp.core.vo.UserStatus +import ltd.evilcorp.domain.feature.UserManager +import ltd.evilcorp.domain.tox.Tox +import javax.inject.Inject + +class EditUserProfileViewModel @Inject constructor( + private val userManager: UserManager, + private val tox: Tox +) : ViewModel() { + val publicKey by lazy { tox.publicKey } + val user: LiveData = userManager.get(publicKey).asLiveData() + var statusModifiedFromDropdown: Boolean = false + + fun setName(name: String) = userManager.setName(name) + fun setStatusMessage(statusMessage: String) = userManager.setStatusMessage(statusMessage) + fun setStatus(status: UserStatus) { + statusModifiedFromDropdown = true + userManager.setStatus(status) + } +} diff --git a/atox/src/main/kotlin/ui/edit_user_profile/StatusArrayAdapter.kt b/atox/src/main/kotlin/ui/edit_user_profile/StatusArrayAdapter.kt new file mode 100644 index 00000000..5319524e --- /dev/null +++ b/atox/src/main/kotlin/ui/edit_user_profile/StatusArrayAdapter.kt @@ -0,0 +1,49 @@ +package ltd.evilcorp.atox.ui.edit_user_profile + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.Filter +import android.widget.ImageView +import ltd.evilcorp.atox.R + +class StatusArrayAdapter( + context: Context, + resource: Int, + textViewResourceId: Int, + private val strings: Array, +) : ArrayAdapter(context, resource, textViewResourceId, strings) { + + private val statusFilter = StatusFilter() // This filter doesn't filter :) + + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val view = super.getView(position, convertView, parent) + val imageView: ImageView = view.findViewById(R.id.ic_status_indicator) + + when (getItem(position)) { + context.getString(R.string.status_available) -> imageView.setImageResource(R.drawable.ic_available) + context.getString(R.string.status_away) -> imageView.setImageResource(R.drawable.ic_away) + context.getString(R.string.status_busy) -> imageView.setImageResource(R.drawable.ic_busy) + } + + return view + } + + override fun getFilter(): Filter { + return statusFilter + } + + inner class StatusFilter : Filter() { + override fun performFiltering(prefix: CharSequence): FilterResults { + val results = FilterResults() + results.values = strings; + results.count = strings.size; + + return results; + } + + override fun publishResults(constraint: CharSequence, results: FilterResults) = Unit + } + +} diff --git a/atox/src/main/kotlin/ui/user_profile/UserProfileFragment.kt b/atox/src/main/kotlin/ui/user_profile/UserProfileFragment.kt index e4b99489..65475ae6 100644 --- a/atox/src/main/kotlin/ui/user_profile/UserProfileFragment.kt +++ b/atox/src/main/kotlin/ui/user_profile/UserProfileFragment.kt @@ -7,13 +7,11 @@ package ltd.evilcorp.atox.ui.user_profile import android.content.ClipData import android.content.ClipboardManager import android.content.Intent -import android.content.res.Resources import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Color import android.net.Uri import android.os.Bundle -import android.util.TypedValue import android.view.View import android.widget.ImageView import android.widget.Toast @@ -26,12 +24,13 @@ import kotlin.math.roundToInt import ltd.evilcorp.atox.R import ltd.evilcorp.atox.databinding.FragmentUserProfileBinding import ltd.evilcorp.atox.vmFactory -import ltd.evilcorp.core.vo.UserStatus import android.util.Log +import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import androidx.core.content.res.ResourcesCompat import androidx.core.graphics.* import androidx.lifecycle.viewModelScope +import androidx.navigation.fragment.findNavController import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -44,19 +43,12 @@ import java.io.FileOutputStream import java.io.IOException -private const val TOX_MAX_NAME_LENGTH = 128 -private const val TOX_MAX_STATUS_MESSAGE_LENGTH = 1007 - private const val QR_CODE_TO_SCREEN_RATIO = 0.5f private const val QR_CODE_PADDING = 16f // in dp private const val QR_CODE_SHARED_IMAGE_PADDING = 30f // in dp -private fun dpToPx(dp: Float, res: Resources): Int = - TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, res.displayMetrics).toInt() - class UserProfileFragment : BaseFragment(FragmentUserProfileBinding::inflate) { private val vm: UserProfileViewModel by viewModels { vmFactory } - private lateinit var currentStatus: UserStatus override fun onViewCreated(view: View, savedInstanceState: Bundle?) = binding.run { @@ -68,8 +60,6 @@ class UserProfileFragment : BaseFragment(FragmentUse } vm.user.observe(viewLifecycleOwner) { user -> - currentStatus = user.status - userName.text = user.name userStatusMessage.text = user.statusMessage profileImageLayout.statusIndicator.setColorFilter(colorFromStatus(resources, user.status)) @@ -79,17 +69,13 @@ class UserProfileFragment : BaseFragment(FragmentUse // Inflating views according to Day/Night theme if (isNightMode(requireContext())) { toolbar.setNavigationIcon(R.drawable.ic_back_white) - icEditProfile.setImageResource(R.drawable.ic_edit_white) - copyToxId.setImageResource(R.drawable.ic_copy_white) createQrCode( - ResourcesCompat.getColor(resources, R.color.pleasantWhite, null), + ContextCompat.getColor(requireContext(), R.color.pleasantWhite), Color.TRANSPARENT, imageView = toxIdQr ) } else { toolbar.setNavigationIcon(R.drawable.ic_back_black) - icEditProfile.setImageResource(R.drawable.ic_edit_black) - copyToxId.setImageResource(R.drawable.ic_copy_black) createQrCode(Color.BLACK, Color.TRANSPARENT, imageView = toxIdQr) } setImageButtonRippleDayNight(requireContext(), copyToxId) @@ -100,6 +86,10 @@ class UserProfileFragment : BaseFragment(FragmentUse activity?.onBackPressed() } + editProfile.setOnClickListener { + findNavController().navigate(R.id.action_userProfileFragment_to_editUserProfileFragment) + } + userToxId.text = vm.toxId.string() userToxId.setOnClickListener { val shareIntent = Intent().apply { @@ -129,49 +119,6 @@ class UserProfileFragment : BaseFragment(FragmentUse startActivity(Intent.createChooser(shareIntent, getString(R.string.tox_id_share))) } } - - /*profileOptions.profileChangeNickname.setOnClickListener { - val nameEdit = EditText(requireContext()).apply { - text.append(binding.userName.text) - filters = arrayOf(InputFilter.LengthFilter(TOX_MAX_NAME_LENGTH)) - setSingleLine() - } - AlertDialog.Builder(requireContext()) - .setTitle(R.string.name) - .setView(nameEdit) - .setPositiveButton(R.string.update) { _, _ -> - vm.setName(nameEdit.text.toString()) - } - .setNegativeButton(R.string.cancel) { _, _ -> } - .show() - } - - profileOptions.profileChangeStatusText.setOnClickListener { - val statusMessageEdit = - EditText(requireContext()).apply { - text.append(binding.userStatusMessage.text) - filters = arrayOf(InputFilter.LengthFilter(TOX_MAX_STATUS_MESSAGE_LENGTH)) - } - AlertDialog.Builder(requireContext()) - .setTitle(R.string.status_message) - .setView(statusMessageEdit) - .setPositiveButton(R.string.update) { _, _ -> - vm.setStatusMessage(statusMessageEdit.text.toString()) - } - .setNegativeButton(R.string.cancel) { _, _ -> } - .show() - } - - profileOptions.profileChangeStatus.setOnClickListener { - StatusDialog(requireContext(), currentStatus) { status -> vm.setStatus(status) }.show() - }*/ - } - - - override fun onDestroyView() = binding.run { - super.onDestroyView() - unbindDrawables(icEditProfile) - unbindDrawables(copyToxId) } diff --git a/atox/src/main/res/anim/slide_in_left.xml b/atox/src/main/res/anim/slide_in_left.xml new file mode 100644 index 00000000..7bba0912 --- /dev/null +++ b/atox/src/main/res/anim/slide_in_left.xml @@ -0,0 +1,6 @@ + + diff --git a/atox/src/main/res/anim/slide_in_right.xml b/atox/src/main/res/anim/slide_in_right.xml index b2769790..1871adf3 100644 --- a/atox/src/main/res/anim/slide_in_right.xml +++ b/atox/src/main/res/anim/slide_in_right.xml @@ -2,5 +2,5 @@ diff --git a/atox/src/main/res/anim/slide_out_left.xml b/atox/src/main/res/anim/slide_out_left.xml new file mode 100644 index 00000000..e480fd59 --- /dev/null +++ b/atox/src/main/res/anim/slide_out_left.xml @@ -0,0 +1,6 @@ + + diff --git a/atox/src/main/res/anim/slide_out_right.xml b/atox/src/main/res/anim/slide_out_right.xml index c970a130..28e919a3 100644 --- a/atox/src/main/res/anim/slide_out_right.xml +++ b/atox/src/main/res/anim/slide_out_right.xml @@ -2,5 +2,5 @@ diff --git a/atox/src/main/res/color/box_stroke_color_day.xml b/atox/src/main/res/color/box_stroke_color_day.xml new file mode 100644 index 00000000..28cadab8 --- /dev/null +++ b/atox/src/main/res/color/box_stroke_color_day.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/atox/src/main/res/color/box_stroke_color_night.xml b/atox/src/main/res/color/box_stroke_color_night.xml new file mode 100644 index 00000000..4a82eab6 --- /dev/null +++ b/atox/src/main/res/color/box_stroke_color_night.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/atox/src/main/res/color/hint_text_color_day.xml b/atox/src/main/res/color/hint_text_color_day.xml new file mode 100644 index 00000000..a8549898 --- /dev/null +++ b/atox/src/main/res/color/hint_text_color_day.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/atox/src/main/res/color/hint_text_color_night.xml b/atox/src/main/res/color/hint_text_color_night.xml new file mode 100644 index 00000000..c32a268a --- /dev/null +++ b/atox/src/main/res/color/hint_text_color_night.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/atox/src/main/res/color/status_available_color_list.xml b/atox/src/main/res/color/status_available_color_list.xml new file mode 100644 index 00000000..dba5b8da --- /dev/null +++ b/atox/src/main/res/color/status_available_color_list.xml @@ -0,0 +1,4 @@ + + + + diff --git a/atox/src/main/res/color/status_away_color_list.xml b/atox/src/main/res/color/status_away_color_list.xml new file mode 100644 index 00000000..50d1dbae --- /dev/null +++ b/atox/src/main/res/color/status_away_color_list.xml @@ -0,0 +1,4 @@ + + + + diff --git a/atox/src/main/res/color/status_busy_color_list.xml b/atox/src/main/res/color/status_busy_color_list.xml new file mode 100644 index 00000000..826ef5d0 --- /dev/null +++ b/atox/src/main/res/color/status_busy_color_list.xml @@ -0,0 +1,4 @@ + + + + diff --git a/atox/src/main/res/color/trailing_icon_color_day.xml b/atox/src/main/res/color/trailing_icon_color_day.xml new file mode 100644 index 00000000..9f631fb1 --- /dev/null +++ b/atox/src/main/res/color/trailing_icon_color_day.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/atox/src/main/res/color/trailing_icon_color_night.xml b/atox/src/main/res/color/trailing_icon_color_night.xml new file mode 100644 index 00000000..cbae2526 --- /dev/null +++ b/atox/src/main/res/color/trailing_icon_color_night.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/atox/src/main/res/drawable/ic_available.xml b/atox/src/main/res/drawable/ic_available.xml new file mode 100644 index 00000000..3b0240d3 --- /dev/null +++ b/atox/src/main/res/drawable/ic_available.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/atox/src/main/res/drawable/ic_away.xml b/atox/src/main/res/drawable/ic_away.xml new file mode 100644 index 00000000..d6e632a3 --- /dev/null +++ b/atox/src/main/res/drawable/ic_away.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/atox/src/main/res/drawable/ic_busy.xml b/atox/src/main/res/drawable/ic_busy.xml new file mode 100644 index 00000000..8fddfd38 --- /dev/null +++ b/atox/src/main/res/drawable/ic_busy.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/atox/src/main/res/drawable/ic_copy_black.xml b/atox/src/main/res/drawable/ic_copy.xml similarity index 89% rename from atox/src/main/res/drawable/ic_copy_black.xml rename to atox/src/main/res/drawable/ic_copy.xml index 2aa2065e..3ede6153 100644 --- a/atox/src/main/res/drawable/ic_copy_black.xml +++ b/atox/src/main/res/drawable/ic_copy.xml @@ -4,6 +4,6 @@ android:viewportWidth="24" android:viewportHeight="24"> diff --git a/atox/src/main/res/drawable/ic_copy_white.xml b/atox/src/main/res/drawable/ic_copy_white.xml deleted file mode 100644 index 18297bd4..00000000 --- a/atox/src/main/res/drawable/ic_copy_white.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/atox/src/main/res/drawable/ic_edit_black.xml b/atox/src/main/res/drawable/ic_edit.xml similarity index 89% rename from atox/src/main/res/drawable/ic_edit_black.xml rename to atox/src/main/res/drawable/ic_edit.xml index d5e54700..be60d088 100644 --- a/atox/src/main/res/drawable/ic_edit_black.xml +++ b/atox/src/main/res/drawable/ic_edit.xml @@ -4,6 +4,6 @@ android:viewportWidth="24" android:viewportHeight="24"> diff --git a/atox/src/main/res/drawable/ic_edit_white.xml b/atox/src/main/res/drawable/ic_edit_white.xml deleted file mode 100644 index e260ac76..00000000 --- a/atox/src/main/res/drawable/ic_edit_white.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/atox/src/main/res/drawable/ic_person.xml b/atox/src/main/res/drawable/ic_person.xml new file mode 100644 index 00000000..66a93840 --- /dev/null +++ b/atox/src/main/res/drawable/ic_person.xml @@ -0,0 +1,9 @@ + + + diff --git a/atox/src/main/res/drawable/ic_status.xml b/atox/src/main/res/drawable/ic_status.xml new file mode 100644 index 00000000..1abe09a4 --- /dev/null +++ b/atox/src/main/res/drawable/ic_status.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/atox/src/main/res/drawable/ic_status_message.xml b/atox/src/main/res/drawable/ic_status_message.xml new file mode 100644 index 00000000..81da7238 --- /dev/null +++ b/atox/src/main/res/drawable/ic_status_message.xml @@ -0,0 +1,9 @@ + + + diff --git a/atox/src/main/res/drawable/side_nav_bar_background.xml b/atox/src/main/res/drawable/side_nav_bar_background.xml index 9be4b75d..853f574a 100644 --- a/atox/src/main/res/drawable/side_nav_bar_background.xml +++ b/atox/src/main/res/drawable/side_nav_bar_background.xml @@ -4,6 +4,6 @@ android:angle="135" android:centerColor="@color/colorPrimary" android:endColor="@color/colorPrimary" - android:startColor="@color/colorPrimaryLight" + android:startColor="@color/colorSecondary" android:type="linear"/> diff --git a/atox/src/main/res/layout/dialog_status.xml b/atox/src/main/res/layout/dialog_status.xml index ad9a6e17..f28503b6 100644 --- a/atox/src/main/res/layout/dialog_status.xml +++ b/atox/src/main/res/layout/dialog_status.xml @@ -151,7 +151,7 @@ android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:background="@drawable/circle" - android:backgroundTint="@color/colorAccent" + android:backgroundTint="@color/colorSecondary" android:baselineAligned="false" android:elevation="4dp" android:focusable="false"> diff --git a/atox/src/main/res/layout/edit_status_item.xml b/atox/src/main/res/layout/edit_status_item.xml new file mode 100644 index 00000000..1d0c4d5f --- /dev/null +++ b/atox/src/main/res/layout/edit_status_item.xml @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/atox/src/main/res/layout/edit_text_value_dialog.xml b/atox/src/main/res/layout/edit_text_value_dialog.xml new file mode 100644 index 00000000..2a41c67d --- /dev/null +++ b/atox/src/main/res/layout/edit_text_value_dialog.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + +