From f4291c93305edbb9e55d7a1d49e727353ac223a0 Mon Sep 17 00:00:00 2001 From: jiangyuyi Date: Sat, 10 Oct 2026 16:42:02 +0800 Subject: [PATCH] Add QR code sharing for URL profiles --- .../github/kr328/clash/ProfilesActivity.kt | 4 +- design/build.gradle.kts | 1 + .../kr328/clash/design/ProfilesDesign.kt | 57 ++++++++++++++++++- .../res/layout/dialog_profile_qr_code.xml | 17 ++++++ .../main/res/layout/dialog_profiles_menu.xml | 11 +++- design/src/main/res/values-ja-rJP/strings.xml | 2 + design/src/main/res/values-ko-rKR/strings.xml | 2 + design/src/main/res/values-ru/strings.xml | 2 + design/src/main/res/values-vi/strings.xml | 2 + design/src/main/res/values-zh-rHK/strings.xml | 2 + design/src/main/res/values-zh-rTW/strings.xml | 2 + design/src/main/res/values-zh/strings.xml | 2 + design/src/main/res/values/strings.xml | 2 + gradle/libs.versions.toml | 2 + 14 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 design/src/main/res/layout/dialog_profile_qr_code.xml diff --git a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt index fe570680b7..13d2eedd9a 100644 --- a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt @@ -75,6 +75,8 @@ class ProfilesActivity : BaseActivity() { startActivity(PropertiesActivity::class.intent.setUUID(uuid)) } + is ProfilesDesign.Request.ShareQrCode -> + design.showProfileQrCode(it.profile) } } if (activityStarted) { @@ -124,4 +126,4 @@ class ProfilesActivity : BaseActivity() { } } } -} \ No newline at end of file +} diff --git a/design/build.gradle.kts b/design/build.gradle.kts index 6e4421e65a..6f0c4c141a 100644 --- a/design/build.gradle.kts +++ b/design/build.gradle.kts @@ -18,4 +18,5 @@ dependencies { implementation(libs.androidx.fragment) implementation(libs.androidx.viewpager) implementation(libs.google.material) + implementation(libs.zxing.core) } diff --git a/design/src/main/java/com/github/kr328/clash/design/ProfilesDesign.kt b/design/src/main/java/com/github/kr328/clash/design/ProfilesDesign.kt index 7d850af374..66e67e00ec 100644 --- a/design/src/main/java/com/github/kr328/clash/design/ProfilesDesign.kt +++ b/design/src/main/java/com/github/kr328/clash/design/ProfilesDesign.kt @@ -2,6 +2,8 @@ package com.github.kr328.clash.design import android.app.Dialog import android.content.Context +import android.graphics.Bitmap +import android.graphics.Color import android.view.View import android.view.ViewGroup import android.view.animation.Animation @@ -9,12 +11,20 @@ import android.view.animation.AnimationUtils import com.github.kr328.clash.design.adapter.ProfileAdapter import com.github.kr328.clash.design.databinding.DesignProfilesBinding import com.github.kr328.clash.design.databinding.DialogProfilesMenuBinding +import com.github.kr328.clash.design.databinding.DialogProfileQrCodeBinding import com.github.kr328.clash.design.dialog.AppBottomSheetDialog import com.github.kr328.clash.design.ui.ToastDuration import com.github.kr328.clash.design.util.* import com.github.kr328.clash.service.model.Profile +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.zxing.BarcodeFormat +import com.google.zxing.EncodeHintType +import com.google.zxing.WriterException +import com.google.zxing.qrcode.QRCodeWriter import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext +import kotlin.coroutines.resume class ProfilesDesign(context: Context) : Design(context) { sealed class Request { @@ -24,6 +34,7 @@ class ProfilesDesign(context: Context) : Design(context) data class Update(val profile: Profile) : Request() data class Edit(val profile: Profile) : Request() data class Duplicate(val profile: Profile) : Request() + data class ShareQrCode(val profile: Profile) : Request() data class Delete(val profile: Profile) : Request() } @@ -135,6 +146,50 @@ class ProfilesDesign(context: Context) : Design(context) dialog.dismiss() } + fun requestShareQrCode(dialog: Dialog, profile: Profile) { + requests.trySend(Request.ShareQrCode(profile)) + + dialog.dismiss() + } + + suspend fun showProfileQrCode(profile: Profile) { + if (profile.type != Profile.Type.Url || profile.source.isBlank()) return + + val bitmap = try { + withContext(Dispatchers.Default) { + val matrix = QRCodeWriter().encode( + profile.source, BarcodeFormat.QR_CODE, 768, 768, + mapOf(EncodeHintType.CHARACTER_SET to "UTF-8") + ) + val pixels = IntArray(matrix.width * matrix.height) { index -> + if (matrix[index % matrix.width, index / matrix.width]) Color.BLACK else Color.WHITE + } + Bitmap.createBitmap(pixels, matrix.width, matrix.height, Bitmap.Config.ARGB_8888) + } + } catch (_: WriterException) { + showToast(R.string.qr_code_generation_failed, ToastDuration.Long) + return + } + + withContext(Dispatchers.Main) { + val binding = DialogProfileQrCodeBinding.inflate(context.layoutInflater) + binding.qrCode.setImageBitmap(bitmap) + + suspendCancellableCoroutine { continuation -> + val dialog = MaterialAlertDialogBuilder(context) + .setTitle(R.string.share_qr_code) + .setView(binding.root) + .setPositiveButton(R.string.close) { _, _ -> } + .setOnDismissListener { + if (continuation.isActive) continuation.resume(Unit) + } + .show() + + continuation.invokeOnCancellation { dialog.dismiss() } + } + } + } + private fun changeUpdateAllButtonStatus() { if (allUpdating) { binding.updateView.startAnimation(rotateAnimation) @@ -142,4 +197,4 @@ class ProfilesDesign(context: Context) : Design(context) binding.updateView.clearAnimation() } } -} \ No newline at end of file +} diff --git a/design/src/main/res/layout/dialog_profile_qr_code.xml b/design/src/main/res/layout/dialog_profile_qr_code.xml new file mode 100644 index 0000000000..62fbc80c9f --- /dev/null +++ b/design/src/main/res/layout/dialog_profile_qr_code.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/design/src/main/res/layout/dialog_profiles_menu.xml b/design/src/main/res/layout/dialog_profiles_menu.xml index e47d56f020..3568fa76d4 100644 --- a/design/src/main/res/layout/dialog_profiles_menu.xml +++ b/design/src/main/res/layout/dialog_profiles_menu.xml @@ -47,6 +47,15 @@ app:icon="@drawable/ic_baseline_content_copy" app:text="@string/duplicate" /> + + - \ No newline at end of file + diff --git a/design/src/main/res/values-ja-rJP/strings.xml b/design/src/main/res/values-ja-rJP/strings.xml index e38b8bf030..359bde73b0 100644 --- a/design/src/main/res/values-ja-rJP/strings.xml +++ b/design/src/main/res/values-ja-rJP/strings.xml @@ -66,6 +66,8 @@ インポート プロパティ 複製 + QRコードで共有 + このリンクのQRコードを生成できません。 プロファイルを有効にする前に保存する必要があります 保存せずに閉じる すべての変更は失われます diff --git a/design/src/main/res/values-ko-rKR/strings.xml b/design/src/main/res/values-ko-rKR/strings.xml index bc22550af8..132f264aee 100644 --- a/design/src/main/res/values-ko-rKR/strings.xml +++ b/design/src/main/res/values-ko-rKR/strings.xml @@ -66,6 +66,8 @@ 가져오기 속성 복제 + QR 코드로 공유 + 이 링크의 QR 코드를 생성할 수 없습니다. 활성화하기 전에 구성 파일을 저장해야합니다. 저장없이 종료 모든 변경 사항이 저장되지 않습니다. diff --git a/design/src/main/res/values-ru/strings.xml b/design/src/main/res/values-ru/strings.xml index 4f680de2c5..196cf995a6 100644 --- a/design/src/main/res/values-ru/strings.xml +++ b/design/src/main/res/values-ru/strings.xml @@ -76,6 +76,8 @@ Импорт Свойства Создать копию + Поделиться QR-кодом + Не удалось создать QR-код для этой ссылки. Профиль должен быть сохранён перед активацией Выйти без сохранения diff --git a/design/src/main/res/values-vi/strings.xml b/design/src/main/res/values-vi/strings.xml index c39742cf6c..2122523830 100644 --- a/design/src/main/res/values-vi/strings.xml +++ b/design/src/main/res/values-vi/strings.xml @@ -90,6 +90,8 @@ Quyên góp Chưa sửa đổi Tạo bản sao + Chia sẻ mã QR + Không thể tạo mã QR cho liên kết này. Sửa Trống Tên trống diff --git a/design/src/main/res/values-zh-rHK/strings.xml b/design/src/main/res/values-zh-rHK/strings.xml index a63dff70a4..d0c8209653 100644 --- a/design/src/main/res/values-zh-rHK/strings.xml +++ b/design/src/main/res/values-zh-rHK/strings.xml @@ -25,6 +25,8 @@ DNS 劫持 處理所有 DNS 數據包 複製 + 二維碼分享 + 無法為此連結產生二維碼。 編輯 空名稱 退出而不保存 diff --git a/design/src/main/res/values-zh-rTW/strings.xml b/design/src/main/res/values-zh-rTW/strings.xml index 4bc185b77d..0be9bb2118 100644 --- a/design/src/main/res/values-zh-rTW/strings.xml +++ b/design/src/main/res/values-zh-rTW/strings.xml @@ -25,6 +25,8 @@ DNS 攔截攻擊 處理所有 DNS 封包 複製 + 分享 QR 碼 + 無法為此連結產生 QR 碼。 編輯 空白名稱 退出而不儲存 diff --git a/design/src/main/res/values-zh/strings.xml b/design/src/main/res/values-zh/strings.xml index be62f50f54..ee7daf75e1 100644 --- a/design/src/main/res/values-zh/strings.xml +++ b/design/src/main/res/values-zh/strings.xml @@ -25,6 +25,8 @@ DNS 劫持 处理所有 DNS 数据包 复制 + 二维码分享 + 无法为此链接生成二维码。 编辑 空名称 退出而不保存 diff --git a/design/src/main/res/values/strings.xml b/design/src/main/res/values/strings.xml index 4e43d3fd26..a2f951e679 100644 --- a/design/src/main/res/values/strings.xml +++ b/design/src/main/res/values/strings.xml @@ -84,6 +84,8 @@ Import Properties Duplicate + Share QR code + Unable to generate a QR code for this link. Profile needs to be saved before activation Exit without Save diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3190850eff..a36849569d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,6 +17,7 @@ kaidl = "1.15" room = "2.4.2" multiprocess = "1.0.0" quickie = "1.11.0" +zxing = "3.5.3" androidx-activity-ktx = "1.9.0" [libraries] @@ -42,6 +43,7 @@ kaidl-compiler = { module = "com.github.kr328.kaidl:kaidl", version.ref = "kaidl kaidl-runtime = { module = "com.github.kr328.kaidl:kaidl-runtime", version.ref = "kaidl" } rikkax-multiprocess = { module = "dev.rikka.rikkax.preference:multiprocess", version.ref = "multiprocess" } quickie-bundled = { group = "io.github.g00fy2.quickie", name = "quickie-bundled", version.ref = "quickie" } +zxing-core = { module = "com.google.zxing:core", version.ref = "zxing" } androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "androidx-activity-ktx" } [plugins]