diff --git a/FEATURES.md b/FEATURES.md index 211cd1cc..98ad3591 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -218,6 +218,7 @@ most things are toggleable in `Settings → Inugram`, with sensible opinionated - sticker creator output sent as photo when high-quality default is on - recyclerlistview double-tap requires same view - dialogs list pull-to-reveal-archive glitches +- standalone TTF/OTF fonts with inflated vertical metrics don't add bogus top/bottom padding - shared media player visual glitches - shared media pager: fling mid-animation to chain tabs or reverse (was ignored until settled); at the edge tab the fling falls through to swipe-to-close - attach panel: better perf, safe close before fully open diff --git a/patches/bugfix/proxy-list-md3-sections.patch b/patches/bugfix/proxy-list-md3-sections.patch new file mode 100644 index 00000000..01104200 --- /dev/null +++ b/patches/bugfix/proxy-list-md3-sections.patch @@ -0,0 +1,41 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Andrew +Date: Thu, 16 Jul 2026 12:32:04 +0300 +Subject: Fix MD3 sections style not applying in Proxy Settings list + +--- + .../src/main/java/org/telegram/ui/ProxyListActivity.java | 5 ----- + 1 file changed, 5 deletions(-) + +diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ProxyListActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ProxyListActivity.java +index 0000000..0000000 100644 +--- a/TMessagesProj/src/main/java/org/telegram/ui/ProxyListActivity.java ++++ b/TMessagesProj/src/main/java/org/telegram/ui/ProxyListActivity.java +@@ -1008,27 +1008,22 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente + break; + case VIEW_TYPE_TEXT_SETTING: + view = new TextSettingsCell(mContext); +- view.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + break; + case VIEW_TYPE_HEADER: + view = new HeaderCell(mContext); +- view.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + break; + case VIEW_TYPE_TEXT_CHECK: + view = new TextCheckCell(mContext); +- view.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + break; + case VIEW_TYPE_INFO: + view = new TextInfoPrivacyCell(mContext); + break; + case VIEW_TYPE_SLIDE_CHOOSER: + view = new SlideChooseView(mContext); +- view.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + break; + case VIEW_TYPE_PROXY_DETAIL: + default: + view = new TextDetailProxyCell(mContext); +- view.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + break; + } + view.setLayoutParams(new RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)); diff --git a/series b/series index be1e02ca..cd3980e1 100644 --- a/series +++ b/series @@ -219,3 +219,4 @@ bugfix/catch-shared-media-swipe.patch bugfix/recyclerlist-ripple.patch bugfix/popup-drag-select-high-ids.patch feature/web-preview-spoiler.patch +bugfix/proxy-list-md3-sections.patch diff --git a/src/kotlin/helpers/dialogs/DrawerHelper.kt b/src/kotlin/helpers/dialogs/DrawerHelper.kt index 9e7dac24..2bc324d3 100644 --- a/src/kotlin/helpers/dialogs/DrawerHelper.kt +++ b/src/kotlin/helpers/dialogs/DrawerHelper.kt @@ -17,6 +17,7 @@ import desu.inugram.helpers.update.UpdateHelper import desu.inugram.ui.drawer.DrawerAddCell import desu.inugram.ui.drawer.DrawerLayoutAdapter import desu.inugram.ui.drawer.DrawerProfileCell +import desu.inugram.ui.drawer.DrawerProxyCell import desu.inugram.ui.drawer.DrawerSwipeController import desu.inugram.ui.drawer.DrawerUserCell import desu.inugram.ui.drawer.SideMenultItemAnimator @@ -26,6 +27,7 @@ import org.telegram.messenger.ApplicationLoader import org.telegram.messenger.FileLoader import org.telegram.messenger.ImageLoader import org.telegram.messenger.LocaleController.getString +import org.telegram.tgnet.ConnectionsManager import org.telegram.messenger.MessagesController import org.telegram.messenger.NotificationCenter import org.telegram.messenger.R @@ -49,6 +51,7 @@ import org.telegram.ui.LaunchActivity import org.telegram.ui.LoginActivity import org.telegram.ui.MainTabsActivity import org.telegram.ui.ProfileActivity +import org.telegram.ui.ProxyListActivity import org.telegram.ui.SettingsActivity import org.telegram.ui.UpdateLayoutWrapper @@ -59,6 +62,7 @@ object DrawerHelper { private var sideMenu: RecyclerListView? = null private var sideMenuContainer: FrameLayout? = null private var themeObserver: NotificationCenter.NotificationCenterDelegate? = null + private var proxyObserver: NotificationCenter.NotificationCenterDelegate? = null private var updateLayout: IUpdateLayout? = null private var updateObserver: NotificationCenter.NotificationCenterDelegate? = null private var updateObserverAccount: Int = -1 @@ -120,7 +124,7 @@ object DrawerHelper { val sm = RecyclerListView(context) sm.layoutManager = LinearLayoutManager(context) val itemAnimator = SideMenultItemAnimator(sm) - val newAdapter = DrawerLayoutAdapter(context, itemAnimator, drawerLayoutContainer) + val newAdapter = DrawerLayoutAdapter(context, itemAnimator, drawerLayoutContainer, ::applyProxyEnabled) adapter = newAdapter sideMenu = sm sm.setItemAnimator(itemAnimator) @@ -157,6 +161,7 @@ object DrawerHelper { controller.setAllowOpenDrawer(true, false) installThemeObserver() + installProxyObserver() installUpdateLayout(context as? Activity, container, sm) } @@ -348,6 +353,34 @@ object DrawerHelper { NotificationCenter.getGlobalInstance().addObserver(obs, NotificationCenter.reloadInterface) } + private fun installProxyObserver() { + if (proxyObserver != null) return + val obs = NotificationCenter.NotificationCenterDelegate { id, _, _ -> + if (id == NotificationCenter.proxySettingsChanged) { + adapter?.notifyDataSetChanged() + } + } + proxyObserver = obs + NotificationCenter.getGlobalInstance().addObserver(obs, NotificationCenter.proxySettingsChanged) + } + + private fun applyProxyEnabled(enabled: Boolean) { + val proxy = if (enabled) SharedConfig.currentProxy else null + MessagesController.getGlobalMainSettings().edit() + .putBoolean("proxy_enabled", enabled && proxy != null) + .apply() + if (proxy != null) { + ConnectionsManager.setProxySettings( + true, proxy.address, proxy.port, + proxy.username, proxy.password, proxy.secret + ) + } else { + ConnectionsManager.setProxySettings(false, "", 0, "", "", "") + } + NotificationCenter.getGlobalInstance() + .postNotificationName(NotificationCenter.proxySettingsChanged) + } + private fun refreshTheme() { sideMenuContainer?.setBackgroundColor(Theme.getColor(Theme.key_chats_menuBackground)) sideMenu?.let { applySideMenuColors(it) } @@ -453,6 +486,11 @@ object DrawerHelper { close() } + ITEM_PROXY -> { + nav.presentFragment(ProxyListActivity()) + close() + } + else -> close() } } @@ -465,6 +503,7 @@ object DrawerHelper { private const val ITEM_CALLS = 10 private const val ITEM_SAVED_MESSAGES = 11 private const val ITEM_SETTINGS = 8 + private const val ITEM_PROXY = DrawerLayoutAdapter.ITEM_PROXY @JvmStatic fun notifyDataChanged() { diff --git a/src/kotlin/helpers/font/FontLibrary.kt b/src/kotlin/helpers/font/FontLibrary.kt index 9f9164a7..0c62962b 100644 --- a/src/kotlin/helpers/font/FontLibrary.kt +++ b/src/kotlin/helpers/font/FontLibrary.kt @@ -7,6 +7,7 @@ import android.graphics.fonts.FontStyle import android.graphics.fonts.SystemFonts import android.net.Uri import android.os.Build +import android.util.Log import androidx.annotation.RequiresApi import desu.inugram.helpers.font.FontLibrary.buildEditorRoster import desu.inugram.helpers.font.SfntParser.Script @@ -19,6 +20,7 @@ import org.telegram.messenger.Utilities import org.telegram.ui.Components.Paint.PaintTypeface import java.io.File import java.io.FileOutputStream +import java.io.RandomAccessFile /** * Owns the editor **font roster** and the storage behind it: imported font families, discovered device @@ -355,6 +357,208 @@ object FontLibrary { File(sub, MANIFEST).writeText(root.toString()) } + /** + * Some Google Fonts downloads (notably Google Sans variable) carry a reasonable line box but a wildly + * oversized safety bbox/win box. Android can let those outer boxes leak into TextView top/bottom metrics, + * which shifts Telegram text. Clamp only the outer boxes to a padded line box; keep hhea/typo metrics. + */ + private fun normalizeVerticalMetrics(file: File) { + try { + RandomAccessFile(file, "rw").use { raf -> + when (val magic = raf.readInt()) { + 0x74746366 -> Unit // TTC checkSumAdjustment is per-face; skip rather than risk corrupting it. + + 0x00010000, 0x4F54544F, 0x74727565 -> { + if (normalizeFaceVerticalMetrics(raf, 0)) { + Log.d(TAG, "normalizeVerticalMetrics: normalized ${file.name}") + } + } + + else -> Log.d(TAG, "normalizeVerticalMetrics: unknown magic 0x${Integer.toHexString(magic)} in ${file.name}") + } + } + } catch (e: Throwable) { + FileLog.e("$TAG: normalizeVerticalMetrics: failed for ${file.name}", e) + } + } + + private fun normalizeFaceVerticalMetrics(raf: RandomAccessFile, sfntOffset: Int): Boolean { + raf.seek(sfntOffset.toLong()) + val sfntVer = raf.readInt() + if (sfntVer != 0x00010000 && sfntVer != 0x4F54544F && sfntVer != 0x74727565) return false + val numTables = raf.readShort().toInt() and 0xffff + raf.skipBytes(6) + + var os2: SfntTable? = null + var head: SfntTable? = null + var maxp: SfntTable? = null + var loca: SfntTable? = null + var glyf: SfntTable? = null + for (i in 0 until numTables) { + val recordOffset = raf.filePointer + val tag = raf.readInt() + raf.readInt() + val off = raf.readInt() + val length = raf.readInt() + when (tag) { + 0x4F532F32 -> os2 = SfntTable(recordOffset, off, length) // OS/2 + 0x68656164 -> head = SfntTable(recordOffset, off, length) // head + 0x6D617870 -> maxp = SfntTable(recordOffset, off, length) // maxp + 0x6C6F6361 -> loca = SfntTable(recordOffset, off, length) // loca + 0x676C7966 -> glyf = SfntTable(recordOffset, off, length) // glyf + } + } + val os2Table = os2 ?: return false + + raf.seek(os2Table.offset.toLong() + 68) + val typoAsc = raf.readShort().toInt() + val typoDesc = raf.readShort().toInt() + val typoGap = raf.readShort().toInt() + raf.seek(os2Table.offset.toLong() + 74) + val winAsc = raf.readShort().toInt() and 0xffff + val winDesc = raf.readShort().toInt() and 0xffff + val headTable = head + var headYMin = 0 + var headYMax = 0 + if (headTable != null) { + raf.seek(headTable.offset.toLong() + 38) + headYMin = raf.readShort().toInt() + raf.skipBytes(2) + headYMax = raf.readShort().toInt() + } + + val typoDescAbs = -typoDesc + val typoTotal = typoAsc + typoDescAbs + typoGap + val winTotal = winAsc + winDesc + if (typoAsc <= 0 || typoDescAbs <= 0 || typoTotal <= 0) return false + + val shouldNormalizeWinBox = winTotal > typoTotal * 3 / 2 || winDesc > typoDescAbs * 2 + val headDescAbs = -headYMin + val shouldNormalizeHeadBox = headTable != null && (headDescAbs > typoDescAbs * 2 || headYMax > typoAsc * 3 / 2) + if (!shouldNormalizeWinBox && !shouldNormalizeHeadBox) return false + + val glyphExtents = glyphYExtents(raf, headTable, maxp, loca, glyf) + val minAsc = glyphExtents?.yMax ?: headYMax.coerceAtLeast(0) + val minDesc = -(glyphExtents?.yMin ?: headYMin.coerceAtMost(0)) + val targetDesc = (typoDescAbs + typoTotal / 16).coerceAtMost(typoDescAbs * 2) + val targetAsc = (typoAsc + typoTotal / 10).coerceAtMost(typoAsc * 3 / 2) + val safeTargetDesc = targetDesc.coerceAtLeast(minDesc) + val safeTargetAsc = targetAsc.coerceAtLeast(minAsc) + var changed = false + + if (shouldNormalizeHeadBox && headTable != null && (safeTargetDesc != headDescAbs || safeTargetAsc != headYMax)) { + raf.seek(headTable.offset.toLong() + 38) + raf.writeShort((-safeTargetDesc).coerceIn(Short.MIN_VALUE.toInt(), -1)) + raf.skipBytes(2) + raf.writeShort(safeTargetAsc.coerceIn(1, 0xffff)) + changed = true + } + + val winTargetAsc = safeTargetAsc.coerceAtMost(winAsc) + val winTargetDesc = safeTargetDesc.coerceAtMost(winDesc) + if (shouldNormalizeWinBox && (winTargetAsc != winAsc || winTargetDesc != winDesc)) { + raf.seek(os2Table.offset.toLong() + 74) + raf.writeShort(winTargetAsc.coerceIn(1, 0xffff)) + raf.writeShort(winTargetDesc.coerceIn(1, 0xffff)) + updateTableChecksum(raf, os2Table) + changed = true + } + if (changed) headTable?.let { updateCheckSumAdjustment(raf, it) } + return changed + } + + private data class SfntTable(val recordOffset: Long, val offset: Int, val length: Int) + private data class GlyphYExtents(val yMin: Int, val yMax: Int) + + private fun glyphYExtents( + raf: RandomAccessFile, + head: SfntTable?, + maxp: SfntTable?, + loca: SfntTable?, + glyf: SfntTable?, + ): GlyphYExtents? { + if (head == null || maxp == null || loca == null || glyf == null) return null + return try { + raf.seek(head.offset.toLong() + 50) + val longLoca = raf.readShort().toInt() != 0 + raf.seek(maxp.offset.toLong() + 4) + val glyphCount = raf.readShort().toInt() and 0xffff + if (glyphCount <= 0) return null + + fun locaOffset(index: Int): Int { + raf.seek(loca.offset.toLong() + if (longLoca) index * 4L else index * 2L) + return if (longLoca) raf.readInt() else (raf.readShort().toInt() and 0xffff) * 2 + } + + var yMin = Int.MAX_VALUE + var yMax = Int.MIN_VALUE + for (i in 0 until glyphCount) { + val start = locaOffset(i) + val end = locaOffset(i + 1) + if (end <= start || start < 0 || end > glyf.length) continue + raf.seek(glyf.offset.toLong() + start + 4) + val glyphYMin = raf.readShort().toInt() + raf.skipBytes(2) + val glyphYMax = raf.readShort().toInt() + if (glyphYMin < yMin) yMin = glyphYMin + if (glyphYMax > yMax) yMax = glyphYMax + } + if (yMin == Int.MAX_VALUE || yMax == Int.MIN_VALUE) null else GlyphYExtents(yMin, yMax) + } catch (_: Throwable) { + null + } + } + + private fun updateTableChecksum(raf: RandomAccessFile, table: SfntTable) { + val checksum = tableChecksum(raf, table.offset.toLong(), table.length) + raf.seek(table.recordOffset + 4) + raf.writeInt(checksum) + } + + private fun updateCheckSumAdjustment(raf: RandomAccessFile, head: SfntTable) { + val adjustmentOffset = head.offset.toLong() + 8 + raf.seek(adjustmentOffset) + raf.writeInt(0) + val headChecksum = tableChecksum(raf, head.offset.toLong(), head.length) + raf.seek(head.recordOffset + 4) + raf.writeInt(headChecksum) + + val fileSum = tableChecksum(raf, 0, raf.length().coerceAtMost(Int.MAX_VALUE.toLong()).toInt()).toLong() and 0xffffffffL + raf.seek(adjustmentOffset) + raf.writeInt((0xB1B0AFBAL - fileSum).toInt()) + } + + private fun tableChecksum(raf: RandomAccessFile, offset: Long, length: Int): Int { + var sum = 0L + val buffer = ByteArray(8192) + var remaining = length + raf.seek(offset) + while (remaining > 0) { + val read = minOf(buffer.size, remaining) + raf.readFully(buffer, 0, read) + var pos = 0 + while (pos + 4 <= read) { + sum = (sum + ((buffer[pos].toLong() and 0xffL) shl 24) + + ((buffer[pos + 1].toLong() and 0xffL) shl 16) + + ((buffer[pos + 2].toLong() and 0xffL) shl 8) + + (buffer[pos + 3].toLong() and 0xffL)) and 0xffffffffL + pos += 4 + } + if (pos < read) { + var word = 0L + var shift = 24 + while (pos < read) { + word = word or ((buffer[pos].toLong() and 0xffL) shl shift) + shift -= 8 + pos++ + } + sum = (sum + word) and 0xffffffffL + } + remaining -= read + } + return sum.toInt() + } + /** Returns (roster entries or null if absent, hidden entry set). */ private fun readIndex(): Pair?, Set> { val dir = rootDir ?: return null to emptySet() @@ -623,6 +827,7 @@ object FontLibrary { tmp.delete() return StageResult.UNPARSEABLE } + normalizeVerticalMetrics(tmp) if (!isLoadableBySystem(tmp)) { tmp.delete() return StageResult.UNSUPPORTED diff --git a/src/kotlin/ui/drawer/DrawerLayoutAdapter.kt b/src/kotlin/ui/drawer/DrawerLayoutAdapter.kt index a6138364..7e4a1072 100644 --- a/src/kotlin/ui/drawer/DrawerLayoutAdapter.kt +++ b/src/kotlin/ui/drawer/DrawerLayoutAdapter.kt @@ -16,6 +16,7 @@ import org.telegram.messenger.UserConfig import org.telegram.tgnet.TLRPC import org.telegram.ui.ActionBar.DrawerLayoutContainer import org.telegram.ui.ActionBar.Theme +import org.telegram.messenger.SharedConfig import org.telegram.ui.Cells.DividerCell import org.telegram.ui.Cells.EmptyCell import org.telegram.ui.Components.RecyclerListView @@ -24,6 +25,7 @@ class DrawerLayoutAdapter( private val mContext: Context, private val itemAnimator: SideMenultItemAnimator, private val mDrawerLayoutContainer: DrawerLayoutContainer, + private val onProxySwitchToggled: ((Boolean) -> Unit)? = null, ) : RecyclerListView.SelectionAdapter() { private val items = ArrayList(11) @@ -82,7 +84,7 @@ class DrawerLayoutAdapter( override fun isEnabled(holder: RecyclerView.ViewHolder): Boolean { val t = holder.itemViewType - return t == 3 || t == 4 || t == 5 || t == 6 + return t == 3 || t == 4 || t == 5 || t == 6 || t == 7 } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { @@ -92,6 +94,7 @@ class DrawerLayoutAdapter( 3 -> DrawerActionCell(mContext) 4 -> DrawerUserCell(mContext) 5 -> DrawerAddCell(mContext) + 7 -> DrawerProxyCell(mContext) else -> EmptyCell(mContext, AndroidUtilities.dp(8f)) } view.layoutParams = RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) @@ -121,6 +124,18 @@ class DrawerLayoutAdapter( val cell = holder.itemView as DrawerUserCell cell.setAccount(accountNumbers[position - 2]) } + + 7 -> { + val cell = holder.itemView as DrawerProxyCell + var pos = position - 2 + if (accountsShown) pos -= getAccountRowsCount() + val item = items[pos] + if (item != null) cell.bind(item.text ?: "", item.icon) + val hasProxies = SharedConfig.proxyList.isNotEmpty() + cell.setSwitchVisible(hasProxies) + cell.setChecked(SharedConfig.isProxyEnabled()) + cell.onSwitchToggled = onProxySwitchToggled + } } } @@ -138,7 +153,9 @@ class DrawerLayoutAdapter( } idx -= getAccountRowsCount() } - if (idx < 0 || idx >= items.size || items[idx] == null) return 2 + if (idx < 0 || idx >= items.size) return 2 + val id = items[idx]?.id ?: return 2 + if (id == ITEM_PROXY) return 7 return 3 } @@ -200,9 +217,14 @@ class DrawerLayoutAdapter( items.add(Item(6, LocaleController.getString(R.string.Contacts), R.drawable.msg_contacts)) items.add(Item(10, LocaleController.getString(R.string.Calls), R.drawable.msg_calls)) items.add(Item(11, LocaleController.getString(R.string.SavedMessages), R.drawable.msg_saved)) + items.add(Item(ITEM_PROXY, LocaleController.getString(R.string.ProxySettings), R.drawable.outline_shield_check)) items.add(Item(8, LocaleController.getString(R.string.Settings), R.drawable.msg_settings_old)) } + companion object { + const val ITEM_PROXY = 9 + } + class Item private constructor( val id: Int, val text: CharSequence?, diff --git a/src/kotlin/ui/drawer/DrawerProxyCell.kt b/src/kotlin/ui/drawer/DrawerProxyCell.kt new file mode 100644 index 00000000..d393550f --- /dev/null +++ b/src/kotlin/ui/drawer/DrawerProxyCell.kt @@ -0,0 +1,124 @@ +package desu.inugram.ui.drawer + +import android.content.Context +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.util.TypedValue +import android.view.Gravity +import android.view.MotionEvent +import android.view.ViewConfiguration +import android.widget.FrameLayout +import android.widget.TextView +import org.telegram.messenger.AndroidUtilities +import org.telegram.messenger.LocaleController +import org.telegram.ui.ActionBar.Theme +import org.telegram.ui.Components.BackupImageView +import org.telegram.ui.Components.LayoutHelper +import org.telegram.ui.Components.Switch +import kotlin.math.abs + +class DrawerProxyCell(context: Context) : FrameLayout(context) { + + private val imageView: BackupImageView + private val textView: TextView + private val checkBox: Switch + + private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop + private val isRTL get() = LocaleController.isRTL + + var onSwitchToggled: ((Boolean) -> Unit)? = null + + init { + imageView = BackupImageView(context) + imageView.setColorFilter(PorterDuffColorFilter(Theme.getColor(Theme.key_chats_menuItemIcon), PorterDuff.Mode.SRC_IN)) + + textView = TextView(context) + textView.setTextColor(Theme.getColor(Theme.key_chats_menuItemText)) + textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15f) + textView.typeface = AndroidUtilities.bold() + textView.gravity = Gravity.CENTER_VERTICAL or (if (isRTL) Gravity.RIGHT else Gravity.LEFT) + + checkBox = Switch(context) + checkBox.setColors(Theme.key_switchTrack, Theme.key_switchTrackChecked, Theme.key_chats_menuBackground, Theme.key_chats_menuBackground) + checkBox.isClickable = false + checkBox.isFocusable = false + + val startGravity = if (isRTL) Gravity.RIGHT else Gravity.LEFT + val endGravity = if (isRTL) Gravity.LEFT else Gravity.RIGHT + addView(imageView, LayoutHelper.createFrame(24, 24f, startGravity or Gravity.TOP, if (isRTL) 0f else 19f, 12f, if (isRTL) 19f else 0f, 0f)) + addView(textView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT.toFloat(), startGravity or Gravity.TOP, if (isRTL) 70f else 72f, 0f, if (isRTL) 72f else 70f, 0f)) + addView(checkBox, LayoutHelper.createFrame(37, 24f, endGravity or Gravity.CENTER_VERTICAL, if (isRTL) 22f else 0f, 0f, if (isRTL) 0f else 22f, 0f)) + + // Switch draws a larger thumb/ripple than its measured bounds — allow it to overdraw. + setClipChildren(false) + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + textView.setTextColor(Theme.getColor(Theme.key_chats_menuItemText)) + imageView.setColorFilter(PorterDuffColorFilter(Theme.getColor(Theme.key_chats_menuItemIcon), PorterDuff.Mode.SRC_IN)) + checkBox.setColors(Theme.key_switchTrack, Theme.key_switchTrackChecked, Theme.key_chats_menuBackground, Theme.key_chats_menuBackground) + } + + private var switchDownX = -1f + private var inSwitchZone = false + + private fun isInSwitchZone(x: Float): Boolean { + if (checkBox.visibility != VISIBLE) return false + // hit zone: from the switch edge (with extra padding) to the near edge of the cell + return if (isRTL) x <= checkBox.right + AndroidUtilities.dp(12f) + else x >= checkBox.left - AndroidUtilities.dp(12f) + } + + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { + if (ev.action == MotionEvent.ACTION_DOWN) { + inSwitchZone = isInSwitchZone(ev.x) + if (inSwitchZone) { + parent?.requestDisallowInterceptTouchEvent(true) + } + } + return inSwitchZone + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + if (!inSwitchZone) return super.onTouchEvent(event) + when (event.action) { + MotionEvent.ACTION_DOWN -> { + switchDownX = event.x + } + MotionEvent.ACTION_UP -> { + if (abs(event.x - switchDownX) < touchSlop) { + val newState = !checkBox.isChecked + checkBox.setChecked(newState, true) + onSwitchToggled?.invoke(newState) + } + inSwitchZone = false + } + MotionEvent.ACTION_CANCEL -> { + inSwitchZone = false + } + // ACTION_MOVE: consumed silently to prevent the row click from firing mid-drag + } + return true + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure( + MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(48f), MeasureSpec.EXACTLY) + ) + } + + fun bind(text: CharSequence, iconRes: Int) { + textView.text = text + imageView.setImageResource(iconRes) + } + + fun setChecked(checked: Boolean) { + checkBox.setChecked(checked, isAttachedToWindow) + } + + fun setSwitchVisible(visible: Boolean) { + checkBox.visibility = if (visible) VISIBLE else GONE + } +} diff --git a/src/kotlin/ui/settings/fonts/FontsSettingsActivity.kt b/src/kotlin/ui/settings/fonts/FontsSettingsActivity.kt index 0c670914..6b39cdf2 100644 --- a/src/kotlin/ui/settings/fonts/FontsSettingsActivity.kt +++ b/src/kotlin/ui/settings/fonts/FontsSettingsActivity.kt @@ -4,9 +4,16 @@ import android.annotation.SuppressLint import android.app.Activity import android.content.Context import android.content.Intent +import android.graphics.Canvas +import android.graphics.LinearGradient +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.RectF +import android.graphics.Shader import android.graphics.Typeface import android.net.Uri import android.os.Build +import android.os.SystemClock import android.view.MotionEvent import android.view.View import android.widget.FrameLayout @@ -44,7 +51,7 @@ class FontsSettingsActivity : SettingsPageActivity(), NotificationCenter.Notific override fun getTitle(): CharSequence = LocaleController.getString(R.string.InuFonts) override fun didReceivedNotification(id: Int, account: Int, vararg args: Any?) { - if (id == NotificationCenter.customTypefacesLoaded) listView?.adapter?.update(true) + if (id == NotificationCenter.customTypefacesLoaded && context != null) listView?.adapter?.update(true) } override fun onFragmentDestroy() { @@ -71,6 +78,7 @@ class FontsSettingsActivity : SettingsPageActivity(), NotificationCenter.Notific @SuppressLint("ClickableViewAccessibility") override fun fillItems(items: ArrayList, adapter: UniversalAdapter) { + val ctx = context ?: return items.add( UItem.asButton( BUTTON_APP_FONT, LocaleController.getString(R.string.InuAppFont), when (val mode = FontConfig.FONT.value) { @@ -99,7 +107,7 @@ class FontsSettingsActivity : SettingsPageActivity(), NotificationCenter.Notific for (font in FontLibrary.getCachedRoster()) { val token = font.token() val row = rows.getOrPut(token) { - FontRow(context).also { r -> + FontRow(ctx).also { r -> r.setOnReorderTouchListener { _, event -> if (event.actionMasked == MotionEvent.ACTION_DOWN) { val holder = listView.findContainingViewHolder(r) @@ -117,9 +125,15 @@ class FontsSettingsActivity : SettingsPageActivity(), NotificationCenter.Notific }) } adapter.reorderSectionEnd() + for (i in 0 until importPlaceholderCount) { + items.add(UItem.asCustom(FontImportSkeletonRow(ctx), 58).apply { id = BUTTON_IMPORT_PROGRESS xor i }) + } items.add(UItem.asShadow(LocaleController.getString(R.string.InuFontsInfo))) - items.add(UItem.asButton(BUTTON_ADD, R.drawable.msg_add, LocaleController.getString(R.string.InuFontAdd))) + items.add( + UItem.asButton(BUTTON_ADD, R.drawable.msg_add, LocaleController.getString(R.string.InuFontAdd)) + .setEnabled(importPlaceholderCount == 0) + ) items.add(UItem.asButton(BUTTON_RESET, R.drawable.msg_reset, LocaleController.getString(R.string.InuFontResetOrder))) items.add(UItem.asShadow(null)) } @@ -127,7 +141,7 @@ class FontsSettingsActivity : SettingsPageActivity(), NotificationCenter.Notific override fun onClick(item: UItem, view: View, position: Int, x: Float, y: Float) { when (item.id) { BUTTON_APP_FONT -> presentFragment(FontStackActivity()) - BUTTON_ADD -> launchFontPicker() + BUTTON_ADD -> if (importPlaceholderCount == 0) launchFontPicker() BUTTON_RESET -> { FontLibrary.resetOrder() FontLibrary.invalidateEditorRoster() @@ -258,13 +272,30 @@ class FontsSettingsActivity : SettingsPageActivity(), NotificationCenter.Notific if (uris.isEmpty()) return val ctx = parentActivity ?: context ?: return FileLog.d("InuFonts: onActivityResultFragment: picked ${uris.size} uris") + setImportPlaceholderCount(uris.size) + BulletinFactory.of(this).createSimpleBulletin( + R.raw.chats_infotip, + LocaleController.getString(R.string.InuFontImporting) + ).show() Utilities.globalQueue.postRunnable { - val result = FontLibrary.importFromUris(ctx, uris) + val result = try { + FontLibrary.importFromUris(ctx, uris) + } catch (e: Throwable) { + FileLog.e("InuFonts: importFromUris failed", e) + null + } AndroidUtilities.runOnUIThread { - if (result.addedFaces > 0) { - FontLibrary.invalidateEditorRoster() + val added = (result?.addedFaces ?: 0) > 0 + setImportPlaceholderCount(0, notifyOthers = result == null || !added) + if (added) FontLibrary.invalidateEditorRoster() + if (context == null) return@runOnUIThread + if (added) { listView.adapter.update(true) - if (result.rejectedBySystem > 0) { + BulletinFactory.of(this).createSimpleBulletin( + R.raw.contact_check, + LocaleController.getString(R.string.InuFontInstalled) + ).show() + if ((result?.rejectedBySystem ?: 0) > 0) { BulletinFactory.of(this).createErrorBulletin( LocaleController.getString(R.string.InuFontUnsupported) ).show() @@ -272,7 +303,7 @@ class FontsSettingsActivity : SettingsPageActivity(), NotificationCenter.Notific } else { BulletinFactory.of(this).createErrorBulletin( LocaleController.getString( - if (result.rejectedBySystem > 0) R.string.InuFontUnsupported else R.string.InuFontImportFailed + if ((result?.rejectedBySystem ?: 0) > 0) R.string.InuFontUnsupported else R.string.InuFontImportFailed ) ).show() } @@ -280,11 +311,21 @@ class FontsSettingsActivity : SettingsPageActivity(), NotificationCenter.Notific } } + private fun setImportPlaceholderCount(value: Int, notifyOthers: Boolean = false) { + importPlaceholderCount = value.coerceAtLeast(0) + if (context != null) listView?.adapter?.update(true) + if (notifyOthers) NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.customTypefacesLoaded) + } + companion object { + @Volatile + private var importPlaceholderCount = 0 + private val BUTTON_APP_FONT = InuUtils.generateId() private val BUTTON_INCLUDE_SYSTEM = InuUtils.generateId() private val BUTTON_ADD = InuUtils.generateId() private val BUTTON_RESET = InuUtils.generateId() + private val BUTTON_IMPORT_PROGRESS = InuUtils.generateId() private const val REQ_PICK_FONT = 31010 @JvmField @@ -340,4 +381,66 @@ class FontsSettingsActivity : SettingsPageActivity(), NotificationCenter.Notific fun isInEye(x: Float): Boolean = x >= eye.left && x <= eye.right } -} \ No newline at end of file + private class FontImportSkeletonRow(context: Context) : View(context) { + private val paint = Paint(Paint.ANTI_ALIAS_FLAG) + private val rect = RectF() + private val matrix = Matrix() + private var gradient: LinearGradient? = null + private var gradientWidth = 0 + private var lastUpdate = 0L + private var shimmerOffset = 0f + + init { + setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)) + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + val viewWidth = width + if (viewWidth <= 0 || height <= 0) return + + ensureGradient(viewWidth) + val now = SystemClock.elapsedRealtime() + val dt = if (lastUpdate == 0L) 16L else (now - lastUpdate).coerceIn(0L, 32L) + lastUpdate = now + shimmerOffset += dt * viewWidth / 900f + if (shimmerOffset > viewWidth + gradientWidth) shimmerOffset = -gradientWidth.toFloat() + matrix.setTranslate(shimmerOffset, 0f) + gradient?.setLocalMatrix(matrix) + + val rtl = LocaleController.isRTL + val start = AndroidUtilities.dp(64f).toFloat() + val end = viewWidth - AndroidUtilities.dp(64f).toFloat() + val titleWidth = AndroidUtilities.dp(164f).toFloat().coerceAtMost(end - start) + val tagWidth = AndroidUtilities.dp(76f).toFloat().coerceAtMost(end - start) + val titleLeft = if (rtl) end - titleWidth else start + val tagLeft = if (rtl) end - tagWidth else start + + drawBar(canvas, titleLeft, AndroidUtilities.dp(11f).toFloat(), titleWidth, AndroidUtilities.dp(14f).toFloat(), 6f) + drawBar(canvas, tagLeft, AndroidUtilities.dp(32f).toFloat(), tagWidth, AndroidUtilities.dp(15f).toFloat(), 7.5f) + postInvalidateOnAnimation() + } + + private fun ensureGradient(width: Int) { + if (gradient != null && gradientWidth == width) return + gradientWidth = width + val base = Theme.getColor(Theme.key_windowBackgroundWhiteGrayText) + val c0 = Theme.multAlpha(base, if (Theme.isCurrentThemeDark()) 0.18f else 0.10f) + val c1 = Theme.multAlpha(base, if (Theme.isCurrentThemeDark()) 0.30f else 0.18f) + gradient = LinearGradient( + -width.toFloat(), 0f, 0f, 0f, + intArrayOf(c0, c1, c0), + floatArrayOf(0f, 0.55f, 1f), + Shader.TileMode.CLAMP, + ) + paint.shader = gradient + } + + private fun drawBar(canvas: Canvas, left: Float, top: Float, width: Float, height: Float, radiusDp: Float) { + rect.set(left, top, left + width, top + height) + val radius = AndroidUtilities.dp(radiusDp).toFloat() + canvas.drawRoundRect(rect, radius, radius, paint) + } + } + +} diff --git a/src/res/values-ja/strings_inu.xml b/src/res/values-ja/strings_inu.xml index 6565a804..f01f4d21 100644 --- a/src/res/values-ja/strings_inu.xml +++ b/src/res/values-ja/strings_inu.xml @@ -186,6 +186,7 @@ システム フォントをインポートできませんでした。TTF/OTF/TTC ファイルか確認してください。 Android がこのフォントを読み込めないため、正しく表示されません。 + フォントをインポート中… インストール 再インストール %1$s に追加 diff --git a/src/res/values-ru/strings_inu.xml b/src/res/values-ru/strings_inu.xml index eb658f99..351bd496 100644 --- a/src/res/values-ru/strings_inu.xml +++ b/src/res/values-ru/strings_inu.xml @@ -174,6 +174,7 @@ Системный Не удалось импортировать шрифт. Убедитесь, что это файл TTF/OTF/TTC. Android не может загрузить этот шрифт, поэтому он будет отображаться некорректно. + Импортируем шрифт… Шрифты Шрифт приложения Шрифт, используемый по всему интерфейсу приложения. @@ -680,4 +681,4 @@ Запрашивать биометрическое подтверждение перед выходом из аккаунта. Разрешить PIN/графический ключ устройства Использовать PIN, графический ключ или пароль устройства, если биометрия не настроена. - \ No newline at end of file + diff --git a/src/res/values-zh-rCN/strings_inu.xml b/src/res/values-zh-rCN/strings_inu.xml index f0fc71c7..29734c8a 100644 --- a/src/res/values-zh-rCN/strings_inu.xml +++ b/src/res/values-zh-rCN/strings_inu.xml @@ -185,6 +185,7 @@ 系统 无法导入字体。请确认是 TTF/OTF/TTC 文件。 Android 无法加载此字体,因此无法正确显示。 + 正在导入字体… 安装 重新安装 添加到 %1$s diff --git a/src/res/values/strings_inu.xml b/src/res/values/strings_inu.xml index 434403c3..4e9947e1 100644 --- a/src/res/values/strings_inu.xml +++ b/src/res/values/strings_inu.xml @@ -250,6 +250,7 @@ System Could not import font. Make sure it is a TTF/OTF/TTC file. Android can\'t load this font, so it would not display correctly. + Importing font… Install Reinstall Add to %1$s