V1.3.6: 「调音」与「扫码点歌」弹窗分辨率自适应
问题: 两个弹窗都用固定 dp 尺寸 + WRAP_CONTENT 高度, 在矮屏(720p@1.5≈480dp 高、 1080p@2.0≈960dp 等)上会被屏幕裁掉, 底部内容(频段滑杆/二维码说明)看不到。 修复: - 新增通用 helper fitDialogToScreen(dialog, scroll, content, preferredWidthDp): 宽度 = min(首选, 屏幕宽*0.94); 高度不再用 WRAP_CONTENT, 而是把内容所在的 ScrollView 限制为 屏幕高*0.92 - 弹窗标题/按钮框架高度。先按最终宽度量一次内容原始高度, 放得下就紧凑显示, 放不下就在弹窗内滚动 —— 任何分辨率下都不会被裁切。 - 调音弹窗: 按屏幕高度算出内容预算, 把剩余空间给 EQ 频段区并夹在 112~216dp, 同时矮屏(高度<620dp)压缩刻度区宽度/标签/预设行高度; 输出自适应日志便于现场核对。 - 扫码点歌弹窗: 二维码边长按 屏幕高*0.42 / 屏宽*0.62 自适应并夹在 160~220(矮屏)/280dp, 位图按最终显示尺寸生成保证清晰; 文案宽度改为 MATCH_PARENT, 内容包进 ScrollView。 - 声音设置弹窗同样接入该 helper(同一类裁切风险)。 版本号 1.3.5 -> 1.3.6(versionCode 11 -> 12)。按约定本次不发布 Release, 仅本地打包测试。
This commit is contained in:
@@ -108,7 +108,7 @@ $BT/apksigner verify 目标名.apk
|
|||||||
|
|
||||||
### 版本号
|
### 版本号
|
||||||
|
|
||||||
- `app/build.gradle`:`versionName "1.3.5"` / `versionCode 11`
|
- `app/build.gradle`:`versionName "1.3.6"` / `versionCode 12`
|
||||||
- App 内显示统一用纯版本号(不加 `(versionCode)` 括号)
|
- App 内显示统一用纯版本号(不加 `(versionCode)` 括号)
|
||||||
|
|
||||||
## 三、关键实现约束(避免踩坑)
|
## 三、关键实现约束(避免踩坑)
|
||||||
@@ -179,4 +179,11 @@ $BT/apksigner verify 目标名.apk
|
|||||||
`next()` 每次都重新比对曲库,**自动吸纳新增歌曲**、剔除已删除歌曲;一轮播完自动开新一轮。
|
`next()` 每次都重新比对曲库,**自动吸纳新增歌曲**、剔除已删除歌曲;一轮播完自动开新一轮。
|
||||||
**不显示**(界面不展示)、**不关联已点清单**(`orderQueue` 完全独立,随机歌曲不入队)、
|
**不显示**(界面不展示)、**不关联已点清单**(`orderQueue` 完全独立,随机歌曲不入队)、
|
||||||
**App 退出即清理**(`onDestroy` 里 `isFinishing` 时 `clear()`,不落盘)
|
**App 退出即清理**(`onDestroy` 里 `isFinishing` 时 `clear()`,不落盘)
|
||||||
|
- **弹窗分辨率自适应**:TV/投影/盒子的分辨率与 density 差异很大(1280x720、1920x1080,
|
||||||
|
density 1.0~2.5),固定 dp 尺寸在矮屏上会被屏幕裁切。统一用 `fitDialogToScreen(dialog, scroll,
|
||||||
|
content, preferredWidthDp, …)`:宽度取 `min(preferredWidthDp, 屏幕宽*0.94)`,高度把内容所在的
|
||||||
|
ScrollView 限制为 `屏幕高*0.92 - 弹窗标题/按钮框架(chromeDp)` —— **不要再用 `WRAP_CONTENT` 作为
|
||||||
|
弹窗高度**。内容高度由「屏幕预算 - 固定区高度」反推(调音弹窗的 EQ 频段区就是这样自适应),
|
||||||
|
预算不足时弹窗内滚动而不是被裁掉。已应用:调音弹窗、扫码点歌弹窗、声音设置弹窗。
|
||||||
|
新增/改动弹窗时请复用该 helper,别再写死 `setLayout(w, WRAP_CONTENT)`
|
||||||
- **仓库可见性**:Gitea 仓库保持 **public**(若后续把 App 更新/热更新端点迁到 Gitea,非 public 会导致 403)
|
- **仓库可见性**:Gitea 仓库保持 **public**(若后续把 App 更新/热更新端点迁到 Gitea,非 public 会导致 403)
|
||||||
|
|||||||
+2
-2
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId "com.local.ktv"
|
applicationId "com.local.ktv"
|
||||||
minSdk 21
|
minSdk 21
|
||||||
targetSdk 25
|
targetSdk 25
|
||||||
versionCode 11
|
versionCode 12
|
||||||
versionName "1.3.5"
|
versionName "1.3.6"
|
||||||
ndk {
|
ndk {
|
||||||
// 双 ABI 打包:兼容 64 位(Exynos 1580 等 arm64-v8a)与 32 位设备(极米 Play 超越版
|
// 双 ABI 打包:兼容 64 位(Exynos 1580 等 arm64-v8a)与 32 位设备(极米 Play 超越版
|
||||||
// 的 Amlogic 950X2/S905X2 采用 32 位 Android 用户态,必须包含 armeabi-v7a,否则
|
// 的 Amlogic 950X2/S905X2 采用 32 位 Android 用户态,必须包含 armeabi-v7a,否则
|
||||||
|
|||||||
@@ -1763,6 +1763,56 @@ class MainActivity : AppCompatActivity() {
|
|||||||
afterShow: (() -> Unit)? = null,
|
afterShow: (() -> Unit)? = null,
|
||||||
): AlertDialog = create().showForTv(preferredButton, afterShow)
|
): AlertDialog = create().showForTv(preferredButton, afterShow)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 弹窗分辨率自适应(在 [showForTv] 的 afterShow 里调用)。
|
||||||
|
*
|
||||||
|
* TV / 投影 / 盒子的分辨率与 density 差异很大(1280x720、1920x1080,density 1.0~2.0),
|
||||||
|
* 固定 dp 尺寸在矮屏上会被屏幕裁掉。这里统一按当前屏幕可用区域收敛:
|
||||||
|
* - 宽度取 `min(preferredWidthDp, 屏幕宽 * widthRatio)`;
|
||||||
|
* - 高度不交给 `WRAP_CONTENT`,而是把内容所在的 [scroll] 高度限制为
|
||||||
|
* `屏幕高 * heightRatio - 弹窗标题/按钮区(chromeDp)`,内容更高时在弹窗内滚动。
|
||||||
|
*
|
||||||
|
* 这样内容放得下时窗口紧凑,放不下时滚动,任何分辨率下都不会被裁切。
|
||||||
|
*/
|
||||||
|
private fun fitDialogToScreen(
|
||||||
|
dialog: AlertDialog,
|
||||||
|
scroll: View,
|
||||||
|
content: View,
|
||||||
|
preferredWidthDp: Int,
|
||||||
|
widthRatio: Float = 0.94f,
|
||||||
|
heightRatio: Float = 0.92f,
|
||||||
|
chromeDp: Int = 150,
|
||||||
|
) {
|
||||||
|
val metrics = resources.displayMetrics
|
||||||
|
val availableWidth = (metrics.widthPixels * widthRatio).toInt()
|
||||||
|
val availableHeight = (metrics.heightPixels * heightRatio).toInt()
|
||||||
|
val width = if (preferredWidthDp > 0) {
|
||||||
|
min(dp(preferredWidthDp), availableWidth)
|
||||||
|
} else {
|
||||||
|
availableWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先按最终宽度量一次内容的原始高度:放得下就紧凑,放不下才滚动。
|
||||||
|
content.measure(
|
||||||
|
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.AT_MOST),
|
||||||
|
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
|
||||||
|
)
|
||||||
|
val maxContentHeight = (availableHeight - dp(chromeDp)).coerceAtLeast(dp(120))
|
||||||
|
val contentHeight = content.measuredHeight.coerceAtMost(maxContentHeight)
|
||||||
|
val params = scroll.layoutParams
|
||||||
|
if (params != null) {
|
||||||
|
params.width = ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
params.height = contentHeight
|
||||||
|
scroll.layoutParams = params
|
||||||
|
} else {
|
||||||
|
scroll.layoutParams = android.widget.FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
contentHeight,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
dialog.window?.setLayout(width, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||||
|
}
|
||||||
|
|
||||||
private fun AlertDialog.restoreSourceFocusOnDismiss(): AlertDialog {
|
private fun AlertDialog.restoreSourceFocusOnDismiss(): AlertDialog {
|
||||||
val sourcePage = currentPage
|
val sourcePage = currentPage
|
||||||
val sourceView = this@MainActivity.window.decorView.findFocus()
|
val sourceView = this@MainActivity.window.decorView.findFocus()
|
||||||
@@ -2506,6 +2556,29 @@ class MainActivity : AppCompatActivity() {
|
|||||||
val working = KtvEqualizer.levelsSnapshot().let { saved ->
|
val working = KtvEqualizer.levelsSnapshot().let { saved ->
|
||||||
FloatArray(caps.bandCount) { saved.getOrElse(it) { 0f } }
|
FloatArray(caps.bandCount) { saved.getOrElse(it) { 0f } }
|
||||||
}
|
}
|
||||||
|
// 分辨率自适应:按屏幕高度算出「弹窗内容能占多少」,把剩余空间留给 EQ 频段区,
|
||||||
|
// 矮屏自动压缩频段区高度(而不是把弹窗顶出屏幕),再叠加 fitDialogToScreen 的
|
||||||
|
// 高度上限兜底 —— 任何分辨率下都不会被裁切。
|
||||||
|
val metrics = resources.displayMetrics
|
||||||
|
val screenHeightDp = metrics.heightPixels / metrics.density
|
||||||
|
val compact = screenHeightDp < 620f
|
||||||
|
val chromeDp = if (compact) 132 else 152
|
||||||
|
val contentBudgetDp = (screenHeightDp * 0.92f - chromeDp).coerceAtLeast(240f)
|
||||||
|
// 固定区:面板内边距 30 + 标题 42 + 音量标题 28 + 音量杆 + 开关(含间距) 50 + 预设(含间距)
|
||||||
|
val fixedHeightDp = 30f + 42f + 28f +
|
||||||
|
(if (compact) 38 else 44) + 50f +
|
||||||
|
(if (compact) 44 else 52) + 10f
|
||||||
|
val bandAreaDp = (contentBudgetDp - fixedHeightDp).coerceIn(112f, 216f)
|
||||||
|
val bandAreaHeight = dp(bandAreaDp.toInt())
|
||||||
|
val barColumnWidth = dp(if (compact) 38 else 46)
|
||||||
|
val barCaptionHeight = dp(if (compact) 18 else 20)
|
||||||
|
val volumeRowHeight = dp(if (compact) 38 else 44)
|
||||||
|
val presetRowHeight = dp(if (compact) 44 else 52)
|
||||||
|
Log.i(
|
||||||
|
TAG,
|
||||||
|
"tuning dialog adaptive: screen=${screenHeightDp}dp compact=$compact " +
|
||||||
|
"budget=${contentBudgetDp}dp bandArea=${bandAreaDp}dp",
|
||||||
|
)
|
||||||
|
|
||||||
val panel = LinearLayout(this).apply {
|
val panel = LinearLayout(this).apply {
|
||||||
orientation = LinearLayout.VERTICAL
|
orientation = LinearLayout.VERTICAL
|
||||||
@@ -2576,7 +2649,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
panel.addView(volumeBar, LinearLayout.LayoutParams(-1, dp(44)))
|
panel.addView(volumeBar, LinearLayout.LayoutParams(-1, volumeRowHeight))
|
||||||
|
|
||||||
// ── 2. 均衡器开关(默认关闭)──
|
// ── 2. 均衡器开关(默认关闭)──
|
||||||
lateinit var eqSwitch: TextView
|
lateinit var eqSwitch: TextView
|
||||||
@@ -2611,7 +2684,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
orientation = LinearLayout.HORIZONTAL
|
orientation = LinearLayout.HORIZONTAL
|
||||||
gravity = Gravity.CENTER
|
gravity = Gravity.CENTER
|
||||||
}
|
}
|
||||||
eqArea.addView(presetRow, LinearLayout.LayoutParams(-1, dp(52)).apply { topMargin = dp(10) })
|
eqArea.addView(presetRow, LinearLayout.LayoutParams(-1, presetRowHeight).apply { topMargin = dp(10) })
|
||||||
|
|
||||||
// ── 4. EQ 频段:竖向控制杆 ──
|
// ── 4. EQ 频段:竖向控制杆 ──
|
||||||
val bars = ArrayList<VerticalEqBar>(caps.bandCount)
|
val bars = ArrayList<VerticalEqBar>(caps.bandCount)
|
||||||
@@ -2628,7 +2701,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
orientation = LinearLayout.HORIZONTAL
|
orientation = LinearLayout.HORIZONTAL
|
||||||
gravity = Gravity.CENTER
|
gravity = Gravity.CENTER
|
||||||
}
|
}
|
||||||
eqArea.addView(barRow, LinearLayout.LayoutParams(-1, dp(216)).apply { topMargin = dp(6) })
|
eqArea.addView(barRow, LinearLayout.LayoutParams(-1, bandAreaHeight).apply { topMargin = dp(6) })
|
||||||
caps.frequencies.forEachIndexed { index, _ ->
|
caps.frequencies.forEachIndexed { index, _ ->
|
||||||
val column = LinearLayout(this).apply {
|
val column = LinearLayout(this).apply {
|
||||||
orientation = LinearLayout.VERTICAL
|
orientation = LinearLayout.VERTICAL
|
||||||
@@ -2640,7 +2713,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
Color.argb(170, 255, 255, 255),
|
Color.argb(170, 255, 255, 255),
|
||||||
).apply { gravity = Gravity.CENTER }
|
).apply { gravity = Gravity.CENTER }
|
||||||
valueViews.add(value)
|
valueViews.add(value)
|
||||||
column.addView(value, LinearLayout.LayoutParams(-1, dp(20)))
|
column.addView(value, LinearLayout.LayoutParams(-1, barCaptionHeight))
|
||||||
val bar = VerticalEqBar(this).apply {
|
val bar = VerticalEqBar(this).apply {
|
||||||
setRange(caps.minDb, caps.maxDb, eqSliderSteps)
|
setRange(caps.minDb, caps.maxDb, eqSliderSteps)
|
||||||
setLevel(working.getOrElse(index) { 0f })
|
setLevel(working.getOrElse(index) { 0f })
|
||||||
@@ -2660,12 +2733,12 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
bars.add(bar)
|
bars.add(bar)
|
||||||
column.addView(bar, LinearLayout.LayoutParams(dp(46), 0, 1f))
|
column.addView(bar, LinearLayout.LayoutParams(barColumnWidth, 0, 1f))
|
||||||
column.addView(
|
column.addView(
|
||||||
label(bandLabels.getOrElse(index) { "${index + 1}" } + "Hz", 12, Color.WHITE).apply {
|
label(bandLabels.getOrElse(index) { "${index + 1}" } + "Hz", 12, Color.WHITE).apply {
|
||||||
gravity = Gravity.CENTER
|
gravity = Gravity.CENTER
|
||||||
},
|
},
|
||||||
LinearLayout.LayoutParams(-1, dp(20)),
|
LinearLayout.LayoutParams(-1, barCaptionHeight),
|
||||||
)
|
)
|
||||||
barRow.addView(column, LinearLayout.LayoutParams(0, -1, 1f))
|
barRow.addView(column, LinearLayout.LayoutParams(0, -1, 1f))
|
||||||
}
|
}
|
||||||
@@ -2713,9 +2786,15 @@ class MainActivity : AppCompatActivity() {
|
|||||||
setBackgroundDrawableResource(android.R.color.transparent)
|
setBackgroundDrawableResource(android.R.color.transparent)
|
||||||
attributes = attributes.apply { dimAmount = 0.72f }
|
attributes = attributes.apply { dimAmount = 0.72f }
|
||||||
addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
||||||
val available = (resources.displayMetrics.widthPixels * 0.94f).toInt()
|
|
||||||
setLayout(min(dp(760), available), ViewGroup.LayoutParams.WRAP_CONTENT)
|
|
||||||
}
|
}
|
||||||
|
// 分辨率自适应:宽度上限 + 内容区高度上限(超出时弹窗内滚动,绝不裁切)。
|
||||||
|
fitDialogToScreen(
|
||||||
|
dialog = dialog,
|
||||||
|
scroll = scroll,
|
||||||
|
content = panel,
|
||||||
|
preferredWidthDp = 760,
|
||||||
|
chromeDp = if (compact) 128 else 150,
|
||||||
|
)
|
||||||
volumeBar.requestFocus()
|
volumeBar.requestFocus()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -3025,34 +3104,54 @@ class MainActivity : AppCompatActivity() {
|
|||||||
.create().showForTv(DialogInterface.BUTTON_POSITIVE)
|
.create().showForTv(DialogInterface.BUTTON_POSITIVE)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// 分辨率自适应:二维码与文案高度按当前屏幕收敛,矮屏压缩、大屏放大(夹在 160~280dp)。
|
||||||
|
val metrics = resources.displayMetrics
|
||||||
|
val screenWidthDp = metrics.widthPixels / metrics.density
|
||||||
|
val screenHeightDp = metrics.heightPixels / metrics.density
|
||||||
|
val compact = screenHeightDp < 620f
|
||||||
|
val qrSide = min(
|
||||||
|
min((screenHeightDp * 0.42f).toInt(), (screenWidthDp * 0.62f).toInt()),
|
||||||
|
if (compact) 220 else 280,
|
||||||
|
).coerceAtLeast(160)
|
||||||
val box = LinearLayout(this)
|
val box = LinearLayout(this)
|
||||||
box.setOrientation(LinearLayout.VERTICAL)
|
box.setOrientation(LinearLayout.VERTICAL)
|
||||||
box.setGravity(Gravity.CENTER_HORIZONTAL)
|
box.setGravity(Gravity.CENTER_HORIZONTAL)
|
||||||
box.setPadding(dp(24), dp(18), dp(24), dp(12))
|
box.setPadding(dp(20), dp(12), dp(20), dp(8))
|
||||||
val qr = ImageView(this)
|
val qr = ImageView(this)
|
||||||
qr.setBackgroundColor(Color.WHITE)
|
qr.setBackgroundColor(Color.WHITE)
|
||||||
qr.setImageBitmap(qrBitmap(url, dp(260)))
|
// 位图按最终显示尺寸生成,保证任何分辨率下二维码都清晰可扫。
|
||||||
box.addView(qr, LinearLayout.LayoutParams(dp(280), dp(280)))
|
qr.setImageBitmap(qrBitmap(url, dp(qrSide)))
|
||||||
val tip = label("手机连接同一局域网后扫码点歌", 18, Color.WHITE)
|
box.addView(qr, LinearLayout.LayoutParams(dp(qrSide), dp(qrSide)))
|
||||||
|
val tip = label("手机连接同一局域网后扫码点歌", if (compact) 16 else 18, Color.WHITE)
|
||||||
tip.gravity = Gravity.CENTER
|
tip.gravity = Gravity.CENTER
|
||||||
box.addView(tip, LinearLayout.LayoutParams(dp(380), dp(54)))
|
box.addView(tip, LinearLayout.LayoutParams(-1, dp(if (compact) 42 else 54)))
|
||||||
// 显示当前服务的 ip:端口 地址(去掉路径,便于对照路由器/局域网排查)。
|
// 显示当前服务的 ip:端口 地址(去掉路径,便于对照路由器/局域网排查)。
|
||||||
val serverHost = remoteServer.address().orEmpty()
|
val serverHost = remoteServer.address().orEmpty()
|
||||||
val serverPort = remoteServer.port()
|
val serverPort = remoteServer.port()
|
||||||
if (serverHost.isNotEmpty() && serverPort > 0) {
|
if (serverHost.isNotEmpty() && serverPort > 0) {
|
||||||
val addressLine = label("$serverHost:$serverPort", 16, GOLD)
|
val addressLine = label("$serverHost:$serverPort", 16, GOLD)
|
||||||
addressLine.gravity = Gravity.CENTER
|
addressLine.gravity = Gravity.CENTER
|
||||||
box.addView(addressLine, LinearLayout.LayoutParams(dp(380), dp(34)))
|
box.addView(addressLine, LinearLayout.LayoutParams(-1, dp(32)))
|
||||||
}
|
}
|
||||||
// 提示:扫码连上后播放界面右上角二维码会自动隐藏。
|
// 提示:扫码连上后播放界面右上角二维码会自动隐藏。
|
||||||
val hint = label("扫码点歌后自动隐藏播放界面二维码", 14, Color.rgb(193, 193, 193))
|
val hint = label("扫码点歌后自动隐藏播放界面二维码", 14, Color.rgb(193, 193, 193))
|
||||||
hint.gravity = Gravity.CENTER
|
hint.gravity = Gravity.CENTER
|
||||||
box.addView(hint, LinearLayout.LayoutParams(dp(420), dp(30)))
|
box.addView(hint, LinearLayout.LayoutParams(-1, dp(if (compact) 26 else 30)))
|
||||||
AlertDialog.Builder(this)
|
val scroll = android.widget.ScrollView(this).apply { addView(box) }
|
||||||
|
val dialog = AlertDialog.Builder(this)
|
||||||
.setTitle("扫码点歌")
|
.setTitle("扫码点歌")
|
||||||
.setView(box)
|
.setView(scroll)
|
||||||
.setPositiveButton("确定", null)
|
.setPositiveButton("确定", null)
|
||||||
.create().showForTv(DialogInterface.BUTTON_POSITIVE)
|
.create()
|
||||||
|
dialog.showForTv(DialogInterface.BUTTON_POSITIVE) {
|
||||||
|
fitDialogToScreen(
|
||||||
|
dialog = dialog,
|
||||||
|
scroll = scroll,
|
||||||
|
content = box,
|
||||||
|
preferredWidthDp = 460,
|
||||||
|
chromeDp = if (compact) 128 else 150,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restartRemoteServer() {
|
private fun restartRemoteServer() {
|
||||||
@@ -8921,8 +9020,14 @@ class MainActivity : AppCompatActivity() {
|
|||||||
saveState()
|
saveState()
|
||||||
}.create()
|
}.create()
|
||||||
dialog.showForTv(DialogInterface.BUTTON_NEGATIVE) {
|
dialog.showForTv(DialogInterface.BUTTON_NEGATIVE) {
|
||||||
val available = (resources.displayMetrics.widthPixels * 0.92f).toInt()
|
// 同样的分辨率自适应:内容过高时在弹窗内滚动而不是被裁切。
|
||||||
dialog.window?.setLayout(min(dp(790), available), ViewGroup.LayoutParams.WRAP_CONTENT)
|
fitDialogToScreen(
|
||||||
|
dialog = dialog,
|
||||||
|
scroll = scroll,
|
||||||
|
content = body,
|
||||||
|
preferredWidthDp = 790,
|
||||||
|
widthRatio = 0.92f,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user