diff --git a/TMessagesProj/build.gradle b/TMessagesProj/build.gradle index ee67982c..e7dbe87e 100644 --- a/TMessagesProj/build.gradle +++ b/TMessagesProj/build.gradle @@ -148,7 +148,7 @@ android { arguments '-DANDROID_STL=c++_static', '-DANDROID_PLATFORM=android-23', '-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON', '-DCMAKE_BUILD_TYPE=Release' abiFilters "arm64-v8a" System.getenv("PATH").split(File.pathSeparator).any { path -> - var file = Paths.get("${path}${File.separator}ccache${if (OperatingSystem.current().windows) ".exe" else ""}").toFile() + var file = Paths.get("${path.trim()}${File.separator}ccache${if (OperatingSystem.current().windows) ".exe" else ""}").toFile() if (file.exists()) { println("Using ccache ${file.getAbsolutePath()}") arguments += "-DANDROID_CCACHE=${file.getAbsolutePath()}" diff --git a/TMessagesProj/src/main/assets/fonts/custom/arimo.ttf b/TMessagesProj/src/main/assets/fonts/custom/arimo.ttf new file mode 100644 index 00000000..428b8cb4 Binary files /dev/null and b/TMessagesProj/src/main/assets/fonts/custom/arimo.ttf differ diff --git a/TMessagesProj/src/main/assets/fonts/custom/notosans.ttf b/TMessagesProj/src/main/assets/fonts/custom/notosans.ttf new file mode 100644 index 00000000..75575046 Binary files /dev/null and b/TMessagesProj/src/main/assets/fonts/custom/notosans.ttf differ diff --git a/TMessagesProj/src/main/assets/fonts/custom/nunito.ttf b/TMessagesProj/src/main/assets/fonts/custom/nunito.ttf new file mode 100644 index 00000000..2ec1f4b0 Binary files /dev/null and b/TMessagesProj/src/main/assets/fonts/custom/nunito.ttf differ diff --git a/TMessagesProj/src/main/assets/fonts/custom/opensans.ttf b/TMessagesProj/src/main/assets/fonts/custom/opensans.ttf new file mode 100644 index 00000000..9db85693 Binary files /dev/null and b/TMessagesProj/src/main/assets/fonts/custom/opensans.ttf differ diff --git a/TMessagesProj/src/main/assets/fonts/custom/raleway.ttf b/TMessagesProj/src/main/assets/fonts/custom/raleway.ttf new file mode 100644 index 00000000..d81d42f5 Binary files /dev/null and b/TMessagesProj/src/main/assets/fonts/custom/raleway.ttf differ diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/AndroidUtilities.java b/TMessagesProj/src/main/java/org/telegram/messenger/AndroidUtilities.java index a6a4bbeb..c477f326 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/AndroidUtilities.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/AndroidUtilities.java @@ -265,7 +265,11 @@ public class AndroidUtilities { public static Typeface bold() { if (mediumTypeface == null) { - if (SharedConfig.useSystemBoldFont && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + // Check if a custom font is selected via FontHelper + Typeface customTypeface = tw.nekomimi.nekogram.helpers.FontHelper.getMediumTypeface(); + if (customTypeface != null) { + mediumTypeface = customTypeface; + } else if (SharedConfig.useSystemBoldFont && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { mediumTypeface = Typeface.create(null, 500, false); } else { mediumTypeface = getTypeface(TYPEFACE_ROBOTO_MEDIUM); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/ApplicationLoader.java b/TMessagesProj/src/main/java/org/telegram/messenger/ApplicationLoader.java index 5ed85b6c..284ca521 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/ApplicationLoader.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/ApplicationLoader.java @@ -254,6 +254,7 @@ public class ApplicationLoader extends Application { } SharedConfig.loadConfig(); + tw.nekomimi.nekogram.helpers.FontHelper.init(); SharedPrefsHelper.init(applicationContext); for (int a = 0; a < UserConfig.MAX_ACCOUNT_COUNT; a++) { //TODO improve account UserConfig.getInstance(a).loadConfig(); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/SharedConfig.java b/TMessagesProj/src/main/java/org/telegram/messenger/SharedConfig.java index 068ce502..25c17d4d 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/SharedConfig.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/SharedConfig.java @@ -401,6 +401,8 @@ public class SharedConfig { public boolean builtin = false; /** Display name for a built-in proxy (e.g. "LTE 1", "WiFi 2"). */ public String builtinName = ""; + /** True if this server is only available to sponsors. */ + public boolean sponsorOnly = false; public int vlessLocalPort = 10808; public long proxyCheckPingId; @@ -1600,6 +1602,33 @@ public class SharedConfig { } } + /** + * Reload built-in proxies after a remote refresh. + * Removes old built-in entries and re-inserts from the updated cache. + * Should be called on the UI thread (or with subsequent UI update). + */ + public static void reloadBuiltinProxies() { + // Remove old built-in entries + for (java.util.Iterator it = proxyList.iterator(); it.hasNext(); ) { + if (it.next().builtin) it.remove(); + } + // Re-insert updated built-ins at the top + java.util.List builtins = XrayController.createBuiltinProxies(); + for (int i = builtins.size() - 1; i >= 0; i--) { + proxyList.add(0, builtins.get(i)); + } + // If current proxy was built-in, find its replacement by localPort + if (currentProxy != null && currentProxy.builtin) { + int port = currentProxy.vlessLocalPort; + ProxyInfo replacement = null; + for (ProxyInfo p : proxyList) { + if (p.builtin && p.vlessLocalPort == port) { replacement = p; break; } + } + if (replacement == null && !proxyList.isEmpty()) replacement = proxyList.get(0); + currentProxy = replacement; + } + } + public static void saveProxyList() { // Exclude builtin proxy from serialization — it's always injected at load time List infoToSerialize = new ArrayList<>(); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/XrayController.java b/TMessagesProj/src/main/java/org/telegram/messenger/XrayController.java index 485dfdb6..c44abf2e 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/XrayController.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/XrayController.java @@ -11,13 +11,20 @@ import android.net.NetworkRequest; import android.os.Build; import android.util.Log; +import org.json.JSONArray; import org.json.JSONObject; import org.telegram.tgnet.ConnectionsManager; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; import java.net.InetSocketAddress; import java.net.Socket; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; /** * Controls the embedded Xray (VLESS+Reality) instance. @@ -35,7 +42,7 @@ public class XrayController { /** Build all configured built-in proxies, in declaration order. */ public static java.util.List createBuiltinProxies() { java.util.List list = new java.util.ArrayList<>(); - for (XrayServers.Server s : XrayServers.SERVERS) { + for (XrayServers.Server s : allEffectiveServers()) { SharedConfig.ProxyInfo p = fromServer(s); if (p != null) list.add(p); } @@ -44,7 +51,7 @@ public class XrayController { /** Build the built-in proxy assigned to the given local SOCKS5 port. */ public static SharedConfig.ProxyInfo createBuiltinProxyByPort(int localPort) { - for (XrayServers.Server s : XrayServers.SERVERS) { + for (XrayServers.Server s : allEffectiveServers()) { if (s.localPort == localPort) { return fromServer(s); } @@ -54,7 +61,7 @@ public class XrayController { /** Build the first configured built-in proxy, or null if none. */ public static SharedConfig.ProxyInfo createBuiltinProxy() { - for (XrayServers.Server s : XrayServers.SERVERS) { + for (XrayServers.Server s : allEffectiveServers()) { SharedConfig.ProxyInfo p = fromServer(s); if (p != null) return p; } @@ -89,7 +96,7 @@ public class XrayController { /** First built-in proxy that matches the current network, or null. */ public static SharedConfig.ProxyInfo createBuiltinProxyForCurrentNetwork() { SharedConfig.ProxyInfo fallback = null; - for (XrayServers.Server s : XrayServers.SERVERS) { + for (XrayServers.Server s : allEffectiveServers()) { SharedConfig.ProxyInfo p = fromServer(s); if (p == null) continue; if (fallback == null) fallback = p; @@ -121,7 +128,7 @@ public class XrayController { /** All built-in proxies that match the current network, in declaration order. */ public static java.util.List builtinProxiesForCurrentNetwork() { java.util.List list = new java.util.ArrayList<>(); - for (XrayServers.Server s : XrayServers.SERVERS) { + for (XrayServers.Server s : allEffectiveServers()) { SharedConfig.ProxyInfo p = fromServer(s); if (p != null && matchesCurrentNetwork(p)) list.add(p); } @@ -155,8 +162,9 @@ public class XrayController { s.address, s.port, s.uuid, s.publicKey, s.shortId, s.fingerprint, s.sni, s.localPort, s.network, s.serviceName); - p.builtin = true; - p.builtinName = s.name != null ? s.name : ""; + p.builtin = true; + p.builtinName = s.name != null ? s.name : ""; + p.sponsorOnly = s.sponsorOnly; return p; } @@ -173,6 +181,147 @@ public class XrayController { } } + // ─── Dynamic server list ─────────────────────────────────────────────────── + private static final String PREFS_SERVERS = "xray_servers"; + private static final String PREFS_KEY_LTE = "cached_lte"; + private static final String PREFS_KEY_WIFI = "cached_wifi"; + private static final long CACHE_TTL_MS = 24 * 60 * 60 * 1000L; // 24h + + /** Cached dynamic server list; null means "not yet loaded or fallback mode". */ + private static volatile List sCachedLte = null; + private static volatile List sCachedWifi = null; + + /** + * Returns the effective server list for the given category. + * If a dynamic list has been loaded it is returned; otherwise falls back + * to the hard-coded {@link XrayServers#SERVERS} array. + */ + private static List effectiveServers(String category) { + List dyn = "lte".equals(category) ? sCachedLte : sCachedWifi; + if (dyn != null && !dyn.isEmpty()) return dyn; + // fallback: filter built-in list by category + List list = new ArrayList<>(); + for (XrayServers.Server s : XrayServers.SERVERS) { + String name = s.name != null ? s.name.toLowerCase(java.util.Locale.ROOT) : ""; + boolean isWifi = name.contains("wifi") || name.contains("wi-fi"); + boolean isLte = name.contains("lte") || name.contains("mobile"); + if ("wifi".equals(category) ? isWifi : isLte) list.add(s); + } + if (list.isEmpty()) { + // if no match, return everything + for (XrayServers.Server s : XrayServers.SERVERS) list.add(s); + } + return list; + } + + /** All effective servers (both categories), same order as API returns. */ + private static List allEffectiveServers() { + if (sCachedLte != null && sCachedWifi != null + && (!sCachedLte.isEmpty() || !sCachedWifi.isEmpty())) { + List all = new ArrayList<>(sCachedLte); + all.addAll(sCachedWifi); + return all; + } + List list = new ArrayList<>(); + for (XrayServers.Server s : XrayServers.SERVERS) list.add(s); + return list; + } + + /** + * Fetch the server list from the FoxCloud API on a background thread. + * On success updates {@link #sCachedLte} / {@link #sCachedWifi} and + * (optionally) calls {@code onDone} on the same background thread. + * Falls back silently to hard-coded servers on any error. + * + * @param ctx application context (may be null — skips cache persist) + * @param onDone callback invoked when done (success or failure); may be null + */ + public static void refreshServers(Context ctx, Runnable onDone) { + new Thread(() -> { + boolean ok = doFetchServers(ctx); + Log.d(TAG, "refreshServers: " + (ok ? "success" : "failed — using built-in fallback")); + if (onDone != null) onDone.run(); + }, "xray-refresh-servers").start(); + } + + private static boolean doFetchServers(Context ctx) { + try { + String apiUrl = XrayServers.API_BASE_URL + "/api/servers?token=" + + XrayServers.API_TOKEN; + URL url = new URL(apiUrl); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setConnectTimeout(10_000); + conn.setReadTimeout(10_000); + conn.setRequestProperty("Accept", "application/json"); + // Pass Telegram ID so the backend can return sponsor-only servers + try { + org.telegram.messenger.UserConfig uc = + org.telegram.messenger.UserConfig.getInstance( + org.telegram.messenger.UserConfig.selectedAccount); + if (uc != null && uc.getCurrentUser() != null) { + conn.setRequestProperty("X-Tg-Id", String.valueOf(uc.getCurrentUser().id)); + } + } catch (Exception ignored) {} + + int code = conn.getResponseCode(); + if (code != 200) { + Log.w(TAG, "doFetchServers: HTTP " + code); + return false; + } + + StringBuilder sb = new StringBuilder(); + try (BufferedReader br = new BufferedReader( + new InputStreamReader(conn.getInputStream(), "UTF-8"))) { + String line; + while ((line = br.readLine()) != null) sb.append(line); + } + + JSONObject root = new JSONObject(sb.toString()); + if (!root.optBoolean("success", false)) return false; + + sCachedLte = parseServerArray(root.optJSONArray("lte"), "lte"); + sCachedWifi = parseServerArray(root.optJSONArray("wifi"), "wifi"); + + Log.d(TAG, "doFetchServers: lte=" + sCachedLte.size() + + " wifi=" + sCachedWifi.size()); + return true; + } catch (Exception e) { + Log.e(TAG, "doFetchServers exception", e); + return false; + } + } + + private static List parseServerArray(JSONArray arr, String defaultCat) { + List list = new ArrayList<>(); + if (arr == null) return list; + for (int i = 0; i < arr.length(); i++) { + try { + JSONObject o = arr.getJSONObject(i); + String name = o.optString("name", defaultCat.toUpperCase() + " " + (i + 1)); + String address = o.optString("address", ""); + int port = o.optInt ("port", 443); + String uuid = o.optString("uuid", ""); + String publicKey = o.optString("publicKey", ""); + String shortId = o.optString("shortId", ""); + String fingerprint = o.optString("fingerprint", "chrome"); + String sni = o.optString("sni", address); + String network = o.optString("network", "tcp"); + String serviceName = o.optString("serviceName", ""); + int localPort = o.optInt ("localPort", 10808 + i); + boolean sponsorOnly = o.optBoolean("sponsorOnly", false); + if (address.isEmpty() || uuid.isEmpty()) continue; + list.add(new XrayServers.Server(name, address, port, uuid, + publicKey, shortId, fingerprint, sni, + network, serviceName, localPort, sponsorOnly)); + } catch (Exception e) { + Log.w(TAG, "parseServerArray: skip entry " + i, e); + } + } + return list; + } + // ────────────────────────────────────────────────────────────────────────── + public static void init(Context ctx) { sAppContext = ctx.getApplicationContext(); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarMenuItem.java b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarMenuItem.java index ea98902d..80d14fa3 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarMenuItem.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarMenuItem.java @@ -263,7 +263,7 @@ public class ActionBarMenuItem extends FrameLayout { iconView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); addView(iconView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); if (iconColor != 0) { - iconView.setColorFilter(new PorterDuffColorFilter(iconColor, PorterDuff.Mode.MULTIPLY)); + iconView.setColorFilter(new PorterDuffColorFilter(iconColor, PorterDuff.Mode.SRC_IN)); } } } @@ -380,13 +380,13 @@ public class ActionBarMenuItem extends FrameLayout { public void setIconColor(int color) { if (iconView != null) { - iconView.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)); + iconView.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)); } if (textView != null) { textView.setTextColor(color); } if (clearButton != null) { - clearButton.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)); + clearButton.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)); } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarMenuSubItem.java b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarMenuSubItem.java index 3e6b5927..7c75bba4 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarMenuSubItem.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarMenuSubItem.java @@ -82,7 +82,7 @@ public class ActionBarMenuSubItem extends FrameLayout { textColor = getThemedColor(Theme.key_actionBarDefaultSubmenuItem); iconColor = getThemedColor(Theme.key_actionBarDefaultSubmenuItemIcon); - iconColorMode = PorterDuff.Mode.MULTIPLY; + iconColorMode = PorterDuff.Mode.SRC_IN; selectorColor = getThemedColor(Theme.key_dialogButtonSelector); updateBackground(); @@ -90,7 +90,7 @@ public class ActionBarMenuSubItem extends FrameLayout { imageView = new RLottieImageView(context); imageView.setScaleType(ImageView.ScaleType.CENTER); - imageView.setColorFilter(new PorterDuffColorFilter(iconColor, PorterDuff.Mode.MULTIPLY)); + imageView.setColorFilter(new PorterDuffColorFilter(iconColor, PorterDuff.Mode.SRC_IN)); addView(imageView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, 40, Gravity.CENTER_VERTICAL | (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT))); textView = new AnimatedEmojiSpan.TextViewEmojis(context); @@ -169,7 +169,7 @@ public class ActionBarMenuSubItem extends FrameLayout { if (rightIcon == null) { rightIcon = new ImageView(getContext()); rightIcon.setScaleType(ImageView.ScaleType.CENTER); - rightIcon.setColorFilter(iconColor, PorterDuff.Mode.MULTIPLY); + rightIcon.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN); if (LocaleController.isRTL) { rightIcon.setScaleX(-1); } @@ -275,7 +275,7 @@ public class ActionBarMenuSubItem extends FrameLayout { } public void setIconColor(int iconColor) { - setIconColor(iconColor, PorterDuff.Mode.MULTIPLY); + setIconColor(iconColor, PorterDuff.Mode.SRC_IN); } public void setIconColor(int iconColor, PorterDuff.Mode mode) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/AlertDialog.java b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/AlertDialog.java index b37258bf..99d4002f 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/AlertDialog.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/AlertDialog.java @@ -250,7 +250,7 @@ public class AlertDialog extends Dialog implements Drawable.Callback, Notificati imageView = new ImageView(context); imageView.setScaleType(ImageView.ScaleType.CENTER); - imageView.setColorFilter(new PorterDuffColorFilter(getThemedColor(Theme.key_dialogIcon), PorterDuff.Mode.MULTIPLY)); + imageView.setColorFilter(new PorterDuffColorFilter(getThemedColor(Theme.key_dialogIcon), PorterDuff.Mode.SRC_IN)); addView(imageView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, 40, Gravity.CENTER_VERTICAL | (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT))); textView = new TextView(context); @@ -1607,7 +1607,7 @@ public class AlertDialog extends Dialog implements Drawable.Callback, Notificati } AlertDialogCell cell = itemViews.get(item); cell.textView.setTextColor(color); - cell.imageView.setColorFilter(new PorterDuffColorFilter(icon, PorterDuff.Mode.MULTIPLY)); + cell.imageView.setColorFilter(new PorterDuffColorFilter(icon, PorterDuff.Mode.SRC_IN)); } public int getItemsCount() { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/BottomSheet.java b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/BottomSheet.java index f3c0f9bf..8e02eb97 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/BottomSheet.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/BottomSheet.java @@ -1040,7 +1040,7 @@ public class BottomSheet extends Dialog implements BaseFragment.AttachedSheet { imageView = new ImageView(context); imageView.setScaleType(ImageView.ScaleType.CENTER); - imageView.setColorFilter(new PorterDuffColorFilter(getThemedColor(Theme.key_dialogIcon), PorterDuff.Mode.MULTIPLY)); + imageView.setColorFilter(new PorterDuffColorFilter(getThemedColor(Theme.key_dialogIcon), PorterDuff.Mode.SRC_IN)); addView(imageView, LayoutHelper.createFrame(56, 48, Gravity.CENTER_VERTICAL | (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT))); imageView2 = new ImageView(context); @@ -1087,7 +1087,7 @@ public class BottomSheet extends Dialog implements BaseFragment.AttachedSheet { } public void setIconColor(int color) { - imageView.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)); + imageView.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)); } public void setGravity(int gravity) { @@ -1834,7 +1834,7 @@ public class BottomSheet extends Dialog implements BaseFragment.AttachedSheet { } BottomSheetCell cell = itemViews.get(item); cell.textView.setTextColor(color); - cell.imageView.setColorFilter(new PorterDuffColorFilter(icon, PorterDuff.Mode.MULTIPLY)); + cell.imageView.setColorFilter(new PorterDuffColorFilter(icon, PorterDuff.Mode.SRC_IN)); } public ArrayList getItemViews() { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/SimpleTextView.java b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/SimpleTextView.java index 0f2b5307..c8020694 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/SimpleTextView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/SimpleTextView.java @@ -85,6 +85,7 @@ public class SimpleTextView extends View implements Drawable.Callback { private float scrollingOffset; private long lastUpdateTime; private int currentScrollDelay; + private int scrollDelayMs = SCROLL_DELAY_MS; private Paint fadePaint; private Paint fadePaintBack; private Paint fadeEllpsizePaint; @@ -233,6 +234,14 @@ public class SimpleTextView extends View implements Drawable.Callback { checkUi_layerType(); } + /** + * Pause (in ms) the marquee holds at the start of the text before it begins scrolling, + * and again after each full loop. Defaults to {@link #SCROLL_DELAY_MS}. + */ + public void setScrollDelayMs(int value) { + scrollDelayMs = value; + } + public void setEllipsizeByGradient(boolean value) { setEllipsizeByGradient(value, null); } @@ -338,8 +347,27 @@ public class SimpleTextView extends View implements Drawable.Callback { textHeight = layout.getLineBottom(0); } + int rightDrawableWidthForFit = 0; + if (rightDrawableInside) { + if (rightDrawable != null && !rightDrawableOutside) { + rightDrawableWidthForFit += (int) (rightDrawable.getIntrinsicWidth() * rightDrawableScale); + } + if (rightDrawable2 != null && !rightDrawableOutside) { + rightDrawableWidthForFit += (int) (rightDrawable2.getIntrinsicWidth() * rightDrawableScale); + } + } + // Same overflow test that drives the marquee (see textDoesNotFit below). + final boolean overflowsForScroll = textWidth + rightDrawableWidthForFit > (width - paddingRight); + if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.CENTER_HORIZONTAL) { - offsetX = (width - textWidth) / 2 - (int) layout.getLineLeft(0); + // When the text fits, center it. When it overflows and marquee scrolling is on, + // start from the left edge so the scroll animation reveals the text from its + // beginning instead of starting already shifted by a few pixels/characters. + if (scrollNonFitText && overflowsForScroll) { + offsetX = -(int) layout.getLineLeft(0); + } else { + offsetX = (width - textWidth) / 2 - (int) layout.getLineLeft(0); + } } else if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.LEFT) { if (firstLineLayout != null) { offsetX = -(int) firstLineLayout.getLineLeft(0); @@ -356,16 +384,7 @@ public class SimpleTextView extends View implements Drawable.Callback { offsetX = -dp(8); } offsetX += getPaddingLeft(); - int rightDrawableWidth = 0; - if (rightDrawableInside) { - if (rightDrawable != null && !rightDrawableOutside) { - rightDrawableWidth += (int) (rightDrawable.getIntrinsicWidth() * rightDrawableScale); - } - if (rightDrawable2 != null && !rightDrawableOutside) { - rightDrawableWidth += (int) (rightDrawable2.getIntrinsicWidth() * rightDrawableScale); - } - } - textDoesNotFit = textWidth + rightDrawableWidth > (width - paddingRight); + textDoesNotFit = overflowsForScroll; checkUi_layerType(); if (fullLayout != null && fullLayoutAdditionalWidth > 0) { @@ -511,7 +530,7 @@ public class SimpleTextView extends View implements Drawable.Callback { if (lastWidth != AndroidUtilities.displaySize.x) { lastWidth = AndroidUtilities.displaySize.x; scrollingOffset = 0; - currentScrollDelay = SCROLL_DELAY_MS; + currentScrollDelay = scrollDelayMs; checkUi_layerType(); } createLayout(width - getPaddingLeft() - getPaddingRight() - minusWidth - (leftDrawableOutside && leftDrawable != null ? leftDrawable.getIntrinsicWidth() + drawablePadding : 0) - (rightDrawableOutside && rightDrawable != null ? rightDrawable.getIntrinsicWidth() + drawablePadding : 0) - (rightDrawableOutside && rightDrawable2 != null ? rightDrawable2.getIntrinsicWidth() + drawablePadding : 0)); @@ -703,7 +722,7 @@ public class SimpleTextView extends View implements Drawable.Callback { return false; } text = value; - currentScrollDelay = SCROLL_DELAY_MS; + currentScrollDelay = scrollDelayMs; recreateLayoutMaybe(); return true; } @@ -1125,6 +1144,12 @@ public class SimpleTextView extends View implements Drawable.Callback { } if (rightDrawable != null && rightDrawableOutside) { int x = Math.min(textOffsetX + textWidth + drawablePadding + (scrollingOffset == 0 ? -nextScrollX : (int) -scrollingOffset) + nextScrollX, getMaxTextWidth() - paddingRight + drawablePadding); + // When the text is centered/right-aligned, offsetX shifts the text horizontally. + // The outside drawable must follow the text, otherwise it overlaps the centered title. + if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.CENTER_HORIZONTAL || + (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.RIGHT) { + x += offsetX; + } int dw = (int) (rightDrawable.getIntrinsicWidth() * rightDrawableScale); int dh = (int) (rightDrawable.getIntrinsicHeight() * rightDrawableScale); int y; @@ -1143,6 +1168,11 @@ public class SimpleTextView extends View implements Drawable.Callback { textOffsetX + textWidth + drawablePadding + (scrollingOffset == 0 ? -nextScrollX : (int) -scrollingOffset) + nextScrollX, getMaxTextWidth() - paddingRight + drawablePadding ); + // Follow the centered/right-aligned text (see rightDrawable above). + if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.CENTER_HORIZONTAL || + (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.RIGHT) { + x += offsetX; + } if (rightDrawable != null) { x += (int) (rightDrawable.getIntrinsicWidth() * rightDrawableScale) + drawablePadding; } @@ -1254,7 +1284,7 @@ public class SimpleTextView extends View implements Drawable.Callback { lastUpdateTime = newUpdateTime; if (scrollingOffset > totalDistance) { scrollingOffset = 0; - currentScrollDelay = SCROLL_DELAY_MS; + currentScrollDelay = scrollDelayMs; } checkUi_layerType(); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ThemeDescription.java b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ThemeDescription.java index fb967f8d..8c13993d 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ThemeDescription.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ThemeDescription.java @@ -252,14 +252,14 @@ public class ThemeDescription { if ((changeFlags & FLAG_BACKGROUNDFILTER) != 0) { ((CombinedDrawable) drawablesToUpdate[a]).getBackground().setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)); } else { - ((CombinedDrawable) drawablesToUpdate[a]).getIcon().setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)); + ((CombinedDrawable) drawablesToUpdate[a]).getIcon().setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)); } } else if (drawablesToUpdate[a] instanceof AvatarDrawable) { ((AvatarDrawable) drawablesToUpdate[a]).setColor(color); } else if (drawablesToUpdate[a] instanceof AnimatedArrowDrawable) { ((AnimatedArrowDrawable) drawablesToUpdate[a]).setColor(color); } else { - drawablesToUpdate[a].setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)); + drawablesToUpdate[a].setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)); } } } @@ -294,7 +294,7 @@ public class ThemeDescription { } else if (drawable instanceof ShapeDrawable) { ((ShapeDrawable) drawable).getPaint().setColor(color); } else { - drawable.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)); + drawable.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)); } } } @@ -402,7 +402,7 @@ public class ThemeDescription { Theme.setSelectorDrawableColor(drawable, color, (changeFlags & FLAG_DRAWABLESELECTEDSTATE) != 0); } } else { - ((ImageView) viewToInvalidate).setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)); + ((ImageView) viewToInvalidate).setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)); } } else if (viewToInvalidate instanceof BackupImageView) { //((BackupImageView) viewToInvalidate).setResourceImageColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)); @@ -414,7 +414,7 @@ public class ThemeDescription { if (drawables != null) { for (int a = 0; a < drawables.length; a++) { if (drawables[a] != null) { - drawables[a].setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)); + drawables[a].setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)); } } } @@ -525,7 +525,7 @@ public class ThemeDescription { } else if (drawable instanceof StateListDrawable || Build.VERSION.SDK_INT >= 21 && drawable instanceof RippleDrawable) { Theme.setSelectorDrawableColor(drawable, color, (changeFlags & FLAG_DRAWABLESELECTEDSTATE) != 0); } - drawable.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)); + drawable.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)); } } } else if ((changeFlags & FLAG_CELLBACKGROUNDCOLOR) != 0) { @@ -611,7 +611,7 @@ public class ThemeDescription { if (drawables != null) { for (int a = 0; a < drawables.length; a++) { if (drawables[a] != null) { - drawables[a].setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)); + drawables[a].setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)); } } } @@ -638,10 +638,10 @@ public class ThemeDescription { if ((changeFlags & FLAG_BACKGROUNDFILTER) != 0) { ((CombinedDrawable) drawable).getBackground().setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)); } else { - ((CombinedDrawable) drawable).getIcon().setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)); + ((CombinedDrawable) drawable).getIcon().setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)); } } else { - imageView.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)); + imageView.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)); } } else if (object instanceof BackupImageView) { Drawable drawable = ((BackupImageView) object).getImageReceiver().getStaticThumb(); @@ -649,10 +649,10 @@ public class ThemeDescription { if ((changeFlags & FLAG_BACKGROUNDFILTER) != 0) { ((CombinedDrawable) drawable).getBackground().setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)); } else { - ((CombinedDrawable) drawable).getIcon().setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)); + ((CombinedDrawable) drawable).getIcon().setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)); } } else if (drawable != null) { - drawable.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)); + drawable.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)); } } else if (object instanceof Drawable) { if (object instanceof LetterDrawable) { @@ -665,14 +665,14 @@ public class ThemeDescription { if ((changeFlags & FLAG_BACKGROUNDFILTER) != 0) { ((CombinedDrawable) object).getBackground().setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)); } else { - ((CombinedDrawable) object).getIcon().setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)); + ((CombinedDrawable) object).getIcon().setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)); } } else if (object instanceof StateListDrawable || Build.VERSION.SDK_INT >= 21 && object instanceof RippleDrawable) { Theme.setSelectorDrawableColor((Drawable) object, color, (changeFlags & FLAG_DRAWABLESELECTEDSTATE) != 0); } else if (object instanceof GradientDrawable) { ((GradientDrawable) object).setColor(color); } else { - ((Drawable) object).setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)); + ((Drawable) object).setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)); } } else if (object instanceof CheckBox) { if ((changeFlags & FLAG_CHECKBOX) != 0) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java index 5a98797c..46dbc1f6 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java @@ -4301,7 +4301,7 @@ public class ChatActivity extends BaseFragment implements }); getConnectionsManager().bindRequestToGuid(req, classGuid); } else { - actionBar.addView(avatarContainer, 0, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.MATCH_PARENT, Gravity.TOP | Gravity.LEFT, !inPreviewMode ? 56 : (chatMode == MODE_PINNED ? 10 : 0), 0, 40, 0)); + actionBar.addView(avatarContainer, 0, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.TOP | Gravity.LEFT, !inPreviewMode ? 56 : (chatMode == MODE_PINNED ? 10 : 0), 0, 40, 0)); } ActionBarMenu menu = actionBar.createMenu(); @@ -4335,7 +4335,8 @@ public class ChatActivity extends BaseFragment implements audioCallIconItem = menu.lazilyAddItem(call, R.drawable.call, themeDelegate); audioCallIconItem.setContentDescription(LocaleController.getString(R.string.Call)); userFull = getMessagesController().getUserFull(currentUser.id); - if (userFull != null && userFull.phone_calls_available) { + // In new layout calls are in the three-dots menu only, never as a toolbar icon + if (!tw.nekomimi.nekogram.NekoConfig.useNewLayout && userFull != null && userFull.phone_calls_available) { showAudioCallAsIcon = !inPreviewMode; audioCallIconItem.setVisibility(View.VISIBLE); } else { @@ -23921,7 +23922,8 @@ public class ChatActivity extends BaseFragment implements } } if (headerItem != null) { - showAudioCallAsIcon = userInfo.phone_calls_available && !inPreviewMode; + // In new layout: calls always go in the three-dots menu, not as toolbar icon + showAudioCallAsIcon = !tw.nekomimi.nekogram.NekoConfig.useNewLayout && userInfo.phone_calls_available && !inPreviewMode; if (avatarContainer != null) { avatarContainer.setTitleExpand(showAudioCallAsIcon); } @@ -29535,7 +29537,7 @@ public class ChatActivity extends BaseFragment implements super.setInPreviewMode(value); if (currentUser != null && audioCallIconItem != null) { TLRPC.UserFull userFull = getMessagesController().getUserFull(currentUser.id); - if (userFull != null && userFull.phone_calls_available) { + if (!tw.nekomimi.nekogram.NekoConfig.useNewLayout && userFull != null && userFull.phone_calls_available) { showAudioCallAsIcon = !inPreviewMode; audioCallIconItem.setVisibility(View.VISIBLE); } else { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAvatarContainer.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAvatarContainer.java index 611aed8b..a03a1d92 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAvatarContainer.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAvatarContainer.java @@ -272,6 +272,8 @@ public class ChatAvatarContainer extends FrameLayout implements NotificationCent titleTextView.setLeftDrawableTopPadding(-dp(1.3f)); titleTextView.setCanHideRightDrawable(false); titleTextView.setRightDrawableOutside(true); + // Marquee holds for 5s at the start of the title before scrolling (and after each loop). + titleTextView.setScrollDelayMs(5000); titleTextView.setPadding(0, dp(6), 0, dp(12)); addView(titleTextView); @@ -644,13 +646,31 @@ public class ChatAvatarContainer extends FrameLayout implements NotificationCent @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = MeasureSpec.getSize(widthMeasureSpec) + titleTextView.getPaddingRight(); - int availableWidth = width - dp((avatarImageView.getVisibility() == VISIBLE ? 54 : 0) + 16); + final boolean centerMode = tw.nekomimi.nekogram.NekoConfig.useNewLayout && avatarImageView.getVisibility() == VISIBLE; + // Center mode layout: + // - avatar is on the right: dp(42) wide + dp(4) right gap = dp(46) from right edge + // - text area: from dp(8) (leftPadding) to (width - dp(46)) + // - centerX of text area = leftPadding + (width - leftPadding - dp(46)) / 2 + // We measure text with the full text-area width so AT_MOST gives true text width. + int availableWidth; + if (centerMode) { + // Full container width minus avatar (dp(46)) — same zone as onLayout uses + availableWidth = width - dp(46); + } else { + availableWidth = width - dp((avatarImageView.getVisibility() == VISIBLE ? 54 : 0) + 16); + } + // Apply gravity BEFORE measuring children: SimpleTextView computes its horizontal + // text offset (offsetX) during measure based on the current gravity. Setting gravity + // only in onLayout has no effect because there's no re-measure afterwards, so the text + // stays left-aligned. Applying it here ensures calcOffset() centers the text in center mode. + applyGravityForMode(); avatarImageView.measure(MeasureSpec.makeMeasureSpec(dp(42), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(dp(42), MeasureSpec.EXACTLY)); titleTextView.measure(MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(dp(24 + 8) + titleTextView.getPaddingRight(), MeasureSpec.AT_MOST)); if (subtitleTextView != null) { subtitleTextView.measure(MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(dp(20), MeasureSpec.AT_MOST)); } else if (animatedSubtitleTextView != null) { - animatedSubtitleTextView.measure(MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(dp(20), MeasureSpec.AT_MOST)); + // AT_MOST so getMeasuredWidth() reflects actual text width, not full available width + animatedSubtitleTextView.measure(MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(dp(20), MeasureSpec.AT_MOST)); } if (timeItem != null) { timeItem.measure(MeasureSpec.makeMeasureSpec(dp(34), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(dp(34), MeasureSpec.EXACTLY)); @@ -667,7 +687,12 @@ public class ChatAvatarContainer extends FrameLayout implements NotificationCent } SimpleTextView titleTextLargerCopyView = this.titleTextLargerCopyView.get(); if (titleTextLargerCopyView != null) { - int largerAvailableWidth = largerWidth - dp((avatarImageView.getVisibility() == VISIBLE ? 54 : 0) + 16); + int largerAvailableWidth; + if (centerMode) { + largerAvailableWidth = largerWidth - dp(16) - dp(46); + } else { + largerAvailableWidth = largerWidth - dp((avatarImageView.getVisibility() == VISIBLE ? 54 : 0) + 16); + } titleTextLargerCopyView.measure(MeasureSpec.makeMeasureSpec(largerAvailableWidth, MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(dp(24), MeasureSpec.AT_MOST)); } lastWidth = width; @@ -734,37 +759,101 @@ public class ChatAvatarContainer extends FrameLayout implements NotificationCent protected void onLayout(boolean changed, int left, int top, int right, int bottom) { int actionBarHeight = ActionBar.getCurrentActionBarHeight(); int viewTop = (actionBarHeight - dp(42)) / 2 + (Build.VERSION.SDK_INT >= 21 && occupyStatusBar ? AndroidUtilities.statusBarHeight : 0); - avatarImageView.layout(leftPadding, viewTop + 1, leftPadding + dp(42), viewTop + 1 + dp(42)); - int l = leftPadding + (avatarImageView.getVisibility() == VISIBLE ? dp( 54) : 0) + rightAvatarPadding; + final boolean center = tw.nekomimi.nekogram.NekoConfig.useNewLayout && avatarImageView.getVisibility() == VISIBLE; + + applyGravityForMode(); + + if (center) { + final int avatarRight = getMeasuredWidth() - dp(4); + avatarImageView.layout(avatarRight - dp(42), viewTop + 1, avatarRight, viewTop + 1 + dp(42)); + } else { + avatarImageView.layout(leftPadding, viewTop + 1, leftPadding + dp(42), viewTop + 1 + dp(42)); + } + SimpleTextView titleTextLargerCopyView = this.titleTextLargerCopyView.get(); - if (getSubtitleTextView().getVisibility() != GONE) { - titleTextView.layout(l, viewTop + dp(1.3f) - titleTextView.getPaddingTop(), l + titleTextView.getMeasuredWidth(), viewTop + titleTextView.getTextHeight() + dp(1.3f) - titleTextView.getPaddingTop() + titleTextView.getPaddingBottom()); - if (titleTextLargerCopyView != null) { - titleTextLargerCopyView.layout(l, viewTop + dp(1.3f), l + titleTextLargerCopyView.getMeasuredWidth(), viewTop + titleTextLargerCopyView.getTextHeight() + dp(1.3f)); + + if (center) { + // The container spans exactly from ← button to ⋮ menu (MATCH_PARENT with margins). + // So getMeasuredWidth() is the full available zone. + // Avatar takes dp(46) from the right — text must not overlap it. + // Center of the full container = getMeasuredWidth() / 2. + final int W = getMeasuredWidth(); + // Avatar sits at the right of the container. Text zone = [0 .. avatarEdge]. + // We place BOTH title and subtitle views to span the full text zone [0..avatarEdge] + // and set gravity=CENTER_HORIZONTAL so each draws its text at the center of that zone. + // This guarantees identical visual center for both regardless of text length. + final int avatarEdge = W - dp(46); + + if (getSubtitleTextView().getVisibility() != GONE) { + titleTextView.layout(0, viewTop + dp(1.3f) - titleTextView.getPaddingTop(), + avatarEdge, viewTop + titleTextView.getTextHeight() + dp(1.3f) - titleTextView.getPaddingTop() + titleTextView.getPaddingBottom()); + if (titleTextLargerCopyView != null) { + titleTextLargerCopyView.setGravity(Gravity.CENTER_HORIZONTAL); + titleTextLargerCopyView.layout(0, viewTop + dp(1.3f), avatarEdge, viewTop + titleTextLargerCopyView.getTextHeight() + dp(1.3f)); + } + if (subtitleTextView != null) { + subtitleTextView.layout(0, viewTop + dp(24), avatarEdge, viewTop + subtitleTextView.getTextHeight() + dp(24)); + } else if (animatedSubtitleTextView != null) { + animatedSubtitleTextView.layout(0, viewTop + dp(24), avatarEdge, viewTop + animatedSubtitleTextView.getTextHeight() + dp(24)); + } + } else { + titleTextView.layout(0, viewTop + dp(11) - titleTextView.getPaddingTop(), + avatarEdge, viewTop + titleTextView.getTextHeight() + dp(11) - titleTextView.getPaddingTop() + titleTextView.getPaddingBottom()); + if (titleTextLargerCopyView != null) { + titleTextLargerCopyView.setGravity(Gravity.CENTER_HORIZONTAL); + titleTextLargerCopyView.layout(0, viewTop + dp(11), avatarEdge, viewTop + titleTextLargerCopyView.getTextHeight() + dp(11)); + } } } else { - titleTextView.layout(l, viewTop + dp(11) - titleTextView.getPaddingTop(), l + titleTextView.getMeasuredWidth(), viewTop + titleTextView.getTextHeight() + dp(11) - titleTextView.getPaddingTop() + titleTextView.getPaddingBottom()); - if (titleTextLargerCopyView != null) { - titleTextLargerCopyView.layout(l, viewTop + dp(11), l + titleTextLargerCopyView.getMeasuredWidth(), viewTop + titleTextLargerCopyView.getTextHeight() + dp(11)); + + final int l = leftPadding + (avatarImageView.getVisibility() == VISIBLE ? dp(54) : 0) + rightAvatarPadding; + if (getSubtitleTextView().getVisibility() != GONE) { + titleTextView.layout(l, viewTop + dp(1.3f) - titleTextView.getPaddingTop(), + l + titleTextView.getMeasuredWidth(), viewTop + titleTextView.getTextHeight() + dp(1.3f) - titleTextView.getPaddingTop() + titleTextView.getPaddingBottom()); + if (titleTextLargerCopyView != null) { + titleTextLargerCopyView.layout(l, viewTop + dp(1.3f), l + titleTextLargerCopyView.getMeasuredWidth(), viewTop + titleTextLargerCopyView.getTextHeight() + dp(1.3f)); + } + } else { + titleTextView.layout(l, viewTop + dp(11) - titleTextView.getPaddingTop(), + l + titleTextView.getMeasuredWidth(), viewTop + titleTextView.getTextHeight() + dp(11) - titleTextView.getPaddingTop() + titleTextView.getPaddingBottom()); + if (titleTextLargerCopyView != null) { + titleTextLargerCopyView.layout(l, viewTop + dp(11), l + titleTextLargerCopyView.getMeasuredWidth(), viewTop + titleTextLargerCopyView.getTextHeight() + dp(11)); + } + } + if (subtitleTextView != null) { + subtitleTextView.layout(l, viewTop + dp(24), l + subtitleTextView.getMeasuredWidth(), viewTop + subtitleTextView.getTextHeight() + dp(24)); + } else if (animatedSubtitleTextView != null) { + animatedSubtitleTextView.layout(l, viewTop + dp(24), l + animatedSubtitleTextView.getMeasuredWidth(), viewTop + animatedSubtitleTextView.getTextHeight() + dp(24)); } } if (timeItem != null) { - timeItem.layout(leftPadding + dp(16), viewTop + dp(15), leftPadding + dp(16 + 34), viewTop + dp(15 + 34)); + if (center) { + final int avatarRight = getMeasuredWidth() - dp(4); + timeItem.layout(avatarRight - dp(42) + dp(16), viewTop + dp(15), avatarRight - dp(42) + dp(16 + 34), viewTop + dp(15 + 34)); + } else { + timeItem.layout(leftPadding + dp(16), viewTop + dp(15), leftPadding + dp(16 + 34), viewTop + dp(15 + 34)); + } } if (starBgItem != null) { - starBgItem.layout(leftPadding + dp(28), viewTop + dp(24), leftPadding + dp(28) + starBgItem.getMeasuredWidth(), viewTop + dp(24) + starBgItem.getMeasuredHeight()); + if (center) { + final int avatarRight = getMeasuredWidth() - dp(4); + starBgItem.layout(avatarRight - dp(42) + dp(28), viewTop + dp(24), avatarRight - dp(42) + dp(28) + starBgItem.getMeasuredWidth(), viewTop + dp(24) + starBgItem.getMeasuredHeight()); + } else { + starBgItem.layout(leftPadding + dp(28), viewTop + dp(24), leftPadding + dp(28) + starBgItem.getMeasuredWidth(), viewTop + dp(24) + starBgItem.getMeasuredHeight()); + } } if (starFgItem != null) { - starFgItem.layout(leftPadding + dp(28), viewTop + dp(24), leftPadding + dp(28) + starFgItem.getMeasuredWidth(), viewTop + dp(24) + starFgItem.getMeasuredHeight()); - } - if (subtitleTextView != null) { - subtitleTextView.layout(l, viewTop + dp(24), l + subtitleTextView.getMeasuredWidth(), viewTop + subtitleTextView.getTextHeight() + dp(24)); - } else if (animatedSubtitleTextView != null) { - animatedSubtitleTextView.layout(l, viewTop + dp(24), l + animatedSubtitleTextView.getMeasuredWidth(), viewTop + animatedSubtitleTextView.getTextHeight() + dp(24)); + if (center) { + final int avatarRight = getMeasuredWidth() - dp(4); + starFgItem.layout(avatarRight - dp(42) + dp(28), viewTop + dp(24), avatarRight - dp(42) + dp(28) + starFgItem.getMeasuredWidth(), viewTop + dp(24) + starFgItem.getMeasuredHeight()); + } else { + starFgItem.layout(leftPadding + dp(28), viewTop + dp(24), leftPadding + dp(28) + starFgItem.getMeasuredWidth(), viewTop + dp(24) + starFgItem.getMeasuredHeight()); + } } SimpleTextView subtitleTextLargerCopyView = this.subtitleTextLargerCopyView.get(); - if (subtitleTextLargerCopyView != null) { - subtitleTextLargerCopyView.layout(l, viewTop + dp(24), l + subtitleTextLargerCopyView.getMeasuredWidth(), viewTop + subtitleTextLargerCopyView.getTextHeight() + dp(24)); + if (!center && subtitleTextLargerCopyView != null) { + final int l2 = leftPadding + (avatarImageView.getVisibility() == VISIBLE ? dp(54) : 0) + rightAvatarPadding; + subtitleTextLargerCopyView.layout(l2, viewTop + dp(24), l2 + subtitleTextLargerCopyView.getMeasuredWidth(), viewTop + subtitleTextLargerCopyView.getTextHeight() + dp(24)); } } @@ -981,6 +1070,24 @@ public class ChatAvatarContainer extends FrameLayout implements NotificationCent } + /** Apply the correct gravity to title/subtitle based on current layout mode. */ + private void applyGravityForMode() { + final boolean c = tw.nekomimi.nekogram.NekoConfig.useNewLayout && avatarImageView.getVisibility() == VISIBLE; + final int g = c ? Gravity.CENTER_HORIZONTAL : Gravity.LEFT; + titleTextView.setGravity(g); + if (subtitleTextView != null) subtitleTextView.setGravity(g); + if (animatedSubtitleTextView != null) animatedSubtitleTextView.setGravity(g); + // In center mode the right drawable (mute icon) must sit right after the title text and + // scroll together with it. "Outside" pins the drawable to the view's right edge instead, + // so disable it for center mode and keep stock behavior (pinned) otherwise. + titleTextView.setRightDrawableOutside(!c); + // Enable marquee for long titles in center mode. This must be set here (during measure, + // before SimpleTextView.calcOffset runs) rather than in onLayout, otherwise calcOffset + // computes the horizontal offset with a stale scroll flag and the paused text starts + // shifted instead of at the very beginning. + titleTextView.setScrollNonFitText(c); + } + public void setSubtitle(CharSequence value) { if (lastSubtitle == null) { if (subtitleTextView != null) { @@ -988,6 +1095,7 @@ public class ChatAvatarContainer extends FrameLayout implements NotificationCent } else if (animatedSubtitleTextView != null) { animatedSubtitleTextView.setText(value); } + applyGravityForMode(); } else { lastSubtitle = value; } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/UItem.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/UItem.java index bcc6eebe..893dd717 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/UItem.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/UItem.java @@ -38,6 +38,8 @@ public class UItem extends AdapterWithDiffUtils.Item { public int pad; public boolean hideDivider; public int iconResId; + public boolean colorfulIcon; + public int iconColorTop, iconColorBottom; public Drawable drawable; public CharSequence text, subtext, textValue; public CharSequence animatedText; @@ -693,6 +695,17 @@ public class UItem extends AdapterWithDiffUtils.Item { return this; } + public UItem colorfulIcon(int colorTop, int colorBottom) { + this.colorfulIcon = true; + this.iconColorTop = colorTop; + this.iconColorBottom = colorBottom; + return this; + } + + public UItem colorfulIcon(org.telegram.ui.Components.IconBackgroundColors colors) { + return colorfulIcon(colors.top, colors.bottom); + } + public UItem setSpanCount(int spanCount) { this.spanCount = spanCount; return this; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/UniversalAdapter.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/UniversalAdapter.java index b45921ab..c9c312c5 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/UniversalAdapter.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/UniversalAdapter.java @@ -664,7 +664,13 @@ public class UniversalAdapter extends AdapterWithDiffUtils { break; case VIEW_TYPE_TEXT: TextCell cell = (TextCell) holder.itemView; - if (item.object instanceof TLRPC.Document) { + if (item.colorfulIcon && item.iconResId != 0) { + if (TextUtils.isEmpty(item.textValue)) { + cell.setText(item.text, divider); + } else { + cell.setTextAndValue(item.text, item.textValue, divider); + } + } else if (item.object instanceof TLRPC.Document) { cell.setTextAndSticker(item.text, (TLRPC.Document) item.object, divider); } else if (item.object instanceof String) { cell.setTextAndSticker(item.text, (String) item.object, divider); @@ -696,9 +702,15 @@ public class UniversalAdapter extends AdapterWithDiffUtils { cell.setColors(Theme.key_windowBackgroundWhiteBlueText4, Theme.key_windowBackgroundWhiteBlueText4); } else if (item.red) { cell.setColors(Theme.key_text_RedBold, Theme.key_text_RedRegular); + } else if (item.colorfulIcon && item.iconResId != 0) { + // keep colorful gradient icon: only set text color, leave icon untouched + cell.setColors(-1, Theme.key_windowBackgroundWhiteBlackText); } else { cell.setColors(Theme.key_windowBackgroundWhiteGrayIcon, Theme.key_windowBackgroundWhiteBlackText); } + if (item.colorfulIcon && item.iconResId != 0) { + cell.setColorfulIcon(item.iconColorTop, item.iconColorBottom, item.iconResId); + } cell.setEnabled(item.enabled, true); break; case VIEW_TYPE_CHECK: diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/glass/GlassTabView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/glass/GlassTabView.java index 319199ea..f404358e 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/glass/GlassTabView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/glass/GlassTabView.java @@ -16,6 +16,7 @@ import android.text.TextPaint; import android.text.TextUtils; import android.util.TypedValue; import android.view.Gravity; +import android.view.View; import android.widget.FrameLayout; import android.widget.TextView; @@ -84,7 +85,7 @@ public class GlassTabView extends FrameLayout implements MainTabsLayout.Tab, Fac imageView.setColorFilter(new PorterDuffColorFilter(Color.BLACK, PorterDuff.Mode.SRC_IN)); textView = new TextView(context); - textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12f); + textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 11f); textView.setSingleLine(); textView.setLines(1); textView.setEllipsize(TextUtils.TruncateAt.END); @@ -152,15 +153,24 @@ public class GlassTabView extends FrameLayout implements MainTabsLayout.Tab, Fac final float selectedFactor = hasGestureSelectedOverride ? gestureSelectedOverride : isSelectedAnimator.getFloatValue(); if (selectedFactor > 0 && !skipDrawSelector) { final float alpha = AnimatorUtils.DECELERATE_INTERPOLATOR.getInterpolation(selectedFactor); - - paintCounterBackground.setColor(Theme.multAlpha(colorSelected, 0.09f * alpha)); - tmpRectF.set(0, 0, viewWidth, getHeight()); - final float r = Math.min(tmpRectF.width(), tmpRectF.height()) / 2f; - final float s = lerp(0.6f, 1, selectedFactor) * MathUtils.clamp(attachScale, 0, 1); - canvas.save(); - canvas.scale(s, s, tmpRectF.centerX(), tmpRectF.centerY()); - canvas.drawRoundRect(tmpRectF, r, r, paintCounterBackground); - canvas.restore(); + if (inlineTextMode) { + // Filled rounded background around the whole selected tab + final float inset = dpf2(4); + tmpRectF.set(inset, inset, viewWidth - inset, getHeight() - inset); + final float r = (tmpRectF.height()) / 2f; + paintCounterBackground.setStyle(Paint.Style.FILL); + paintCounterBackground.setColor(Theme.multAlpha(colorSelected, 0.15f * alpha)); + canvas.drawRoundRect(tmpRectF, r, r, paintCounterBackground); + } else { + paintCounterBackground.setColor(Theme.multAlpha(colorSelected, 0.09f * alpha)); + tmpRectF.set(0, 0, viewWidth, getHeight()); + final float r = Math.min(tmpRectF.width(), tmpRectF.height()) / 2f; + final float s = lerp(0.6f, 1, selectedFactor) * MathUtils.clamp(attachScale, 0, 1); + canvas.save(); + canvas.scale(s, s, tmpRectF.centerX(), tmpRectF.centerY()); + canvas.drawRoundRect(tmpRectF, r, r, paintCounterBackground); + canvas.restore(); + } } final float hasCounter = (usePremiumCounter ? 1f : isHasCounterAnimator.getFloatValue()) * attachScale; @@ -175,12 +185,20 @@ public class GlassTabView extends FrameLayout implements MainTabsLayout.Tab, Fac canvas.save(); final float gap = dpf2(1.33f); - final float cx = viewWidth / 2f + dpf2(11); + // In inline mode, position badge near the icon (right side of icon) + final float cx; + if (inlineTextMode) { + final int iconSize = dp(20); + final float iconLeft = (viewWidth - iconSize) / 2f; + cx = iconLeft + iconSize - dpf2(2); + } else { + cx = viewWidth / 2f + dpf2(11); + } final float cy = dpf2(10); - final float height = dpf2(16); - final float width = Math.max(height, counter.getCurrentWidth() + dp(8)); - final float rOuter = dpf2(9.333f); - final float rInner = dpf2(8f); + final float height = dpf2(14); + final float width = Math.max(height, counter.getCurrentWidth() + dp(6)); + final float rOuter = dpf2(8f); + final float rInner = dpf2(6.5f); tmpRectF.set( cx - width / 2f - gap, cy - height / 2f - gap, @@ -234,6 +252,116 @@ public class GlassTabView extends FrameLayout implements MainTabsLayout.Tab, Fac checkPlayAnimation(animated); textView.setTypeface(selected ? AndroidUtilities.getTypeface(AndroidUtilities.TYPEFACE_ROBOTO_EXTRA_BOLD) : AndroidUtilities.bold()); + if (inlineTextMode) { + // layout is updated via onFactorChanged -> updateInlineLayout + } else if (hideTextWhenUnselected) { + updateTextVisibility(animated); + } + } + + private boolean inlineTextMode; // when true: text appears to the right of icon + private boolean hideTextWhenUnselected; + private boolean classicTabMode; // when true: standard Telegram style — 24dp icon centered above text + + public void setInlineTextMode(boolean inline) { + this.inlineTextMode = inline; + requestLayout(); + invalidate(); + } + + /** + * Classic Telegram bottom-nav style: icon 24dp centered horizontally near top, + * text label centered below it. No floating pill, full-width equal tabs. + */ + public void setClassicTabMode(boolean classic) { + this.classicTabMode = classic; + if (classic) { + // Reposition icon: 24dp, centered horizontally, 8dp from top + final FrameLayout.LayoutParams iconLp = (FrameLayout.LayoutParams) imageView.getLayoutParams(); + iconLp.width = dp(24); + iconLp.height = dp(24); + iconLp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP; + iconLp.topMargin = dp(8); + imageView.setLayoutParams(iconLp); + + // Reposition text: below icon, centered + final FrameLayout.LayoutParams textLp = (FrameLayout.LayoutParams) textView.getLayoutParams(); + textLp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP; + textLp.topMargin = dp(34); + textView.setLayoutParams(textLp); + textView.setAlpha(1f); + textView.setScaleX(1f); + textView.setScaleY(1f); + } + requestLayout(); + invalidate(); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + if (inlineTextMode) { + updateInlineLayout(isSelectedAnimator.getFloatValue()); + } + } + + private void updateInlineLayout(float selectedFactor) { + if (!inlineTextMode) return; + final int w = getMeasuredWidth(); + final int h = getMeasuredHeight(); + final int iconSize = dp(20); + final int gap = dp(4); + // Use real text width from paint (textView is MATCH_PARENT so getMeasuredWidth is wrong) + final int textW = (int) Math.ceil(defaultTextPaint.measureText(textView.getText().toString())); + final int textH = textView.getMeasuredHeight(); + // Icon always centered vertically + final int iconTop = (h - iconSize) / 2; + // Row (icon + gap + text) centered horizontally when expanded; icon centered when collapsed + final float expandedRowW = iconSize + gap + textW; + final float rowW = lerp(iconSize, expandedRowW, selectedFactor); + final float rowLeft = (w - rowW) / 2f; + final View iv = backupImageView != null && imageView.getVisibility() == GONE ? backupImageView : imageView; + iv.layout((int) rowLeft, iconTop, (int) rowLeft + iconSize, iconTop + iconSize); + if (backupImageView != null && imageView.getVisibility() == GONE) { + backupImageView.layout((int) rowLeft, iconTop, (int) rowLeft + iconSize, iconTop + iconSize); + } + // Text: right of icon, vertically centered, fade in. + // textView is gravity CENTER so we lay it out in a rect exactly text-wide. + final float textLeft = rowLeft + iconSize + gap; + final int textTop = (h - textH) / 2; + textView.layout((int) textLeft, textTop, (int) textLeft + textW, textTop + textH); + textView.setAlpha(selectedFactor); + textView.setScaleX(lerp(0.8f, 1f, selectedFactor)); + textView.setScaleY(lerp(0.8f, 1f, selectedFactor)); + textView.setPivotX(0); + textView.setPivotY(textH / 2f); + iv.setTranslationX(0); + iv.setTranslationY(0); + textView.setTranslationX(0); + textView.setTranslationY(0); + } + + public void setHideTextWhenUnselected(boolean hide) { + this.hideTextWhenUnselected = hide; + updateTextVisibility(false); + } + + private void updateTextVisibility(boolean animated) { + final boolean selected = isSelectedAnimator.getValue(); + if (animated) { + textView.animate().cancel(); + textView.animate() + .alpha(selected ? 1f : 0f) + .scaleX(selected ? 1f : 0.7f) + .scaleY(selected ? 1f : 0.7f) + .setDuration(220) + .setInterpolator(CubicBezierInterpolator.EASE_OUT_QUINT) + .start(); + } else { + textView.setAlpha(selected ? 1f : 0f); + textView.setScaleX(selected ? 1f : 0.7f); + textView.setScaleY(selected ? 1f : 0.7f); + } } public boolean isTabSelected() { @@ -244,6 +372,12 @@ public class GlassTabView extends FrameLayout implements MainTabsLayout.Tab, Fac public void onFactorChanged(int id, float factor, float fraction, FactorAnimator callee) { if (id == ANIMATOR_ID_IS_SELECTED) { updateColors(); + if (inlineTextMode) { + updateInlineLayout(factor); + if (getParent() instanceof View) { + ((View) getParent()).requestLayout(); + } + } } invalidate(); } @@ -396,6 +530,21 @@ public class GlassTabView extends FrameLayout implements MainTabsLayout.Tab, Fac tab.tabAnimation = tabAnimation; tab.textView.setText(LocaleController.getString(stringRes)); tab.checkPlayAnimation(false); + tab.imageView.setLayoutParams(LayoutHelper.createFrame(20, 20, Gravity.CENTER_HORIZONTAL | Gravity.TOP, 0, 4, 0, 0)); + tab.colorDefault = Theme.getColor(Theme.key_glass_tabUnselected, resourcesProvider); + tab.colorSelected = Theme.getColor(Theme.key_glass_tabSelected, resourcesProvider); + tab.colorSelectedText = Theme.getColor(Theme.key_glass_tabSelectedText, resourcesProvider); + tab.updateColors(); + return tab; + } + + public static GlassTabView createMainTabIcon(Context context, Theme.ResourcesProvider resourcesProvider, @androidx.annotation.DrawableRes int iconRes, @StringRes int stringRes) { + GlassTabView tab = new GlassTabView(context); + tab.resourcesProvider = resourcesProvider; + tab.tabAnimation = null; + tab.textView.setText(LocaleController.getString(stringRes)); + tab.imageView.clearAnimationDrawable(); + tab.imageView.setImageResource(iconRes); tab.imageView.setLayoutParams(LayoutHelper.createFrame(24, 24, Gravity.CENTER_HORIZONTAL | Gravity.TOP, 0, 4, 0, 0)); tab.colorDefault = Theme.getColor(Theme.key_glass_tabUnselected, resourcesProvider); tab.colorSelected = Theme.getColor(Theme.key_glass_tabSelected, resourcesProvider); @@ -404,6 +553,61 @@ public class GlassTabView extends FrameLayout implements MainTabsLayout.Tab, Fac return tab; } + public static GlassTabView createComposeButton(Context context, Theme.ResourcesProvider resourcesProvider, @androidx.annotation.DrawableRes int iconRes) { + return createComposeButton(context, resourcesProvider, iconRes, 32); + } + + public static GlassTabView createComposeButton(Context context, Theme.ResourcesProvider resourcesProvider, @androidx.annotation.DrawableRes int iconRes, int iconSizeDp) { + GlassTabView tab = new GlassTabView(context); + tab.resourcesProvider = resourcesProvider; + tab.tabAnimation = null; + tab.textView.setVisibility(GONE); + tab.imageView.clearAnimationDrawable(); + tab.imageView.setImageResource(iconRes); + tab.imageView.setLayoutParams(LayoutHelper.createFrame(iconSizeDp, iconSizeDp, Gravity.CENTER, 0, 0, 0, 0)); + tab.colorDefault = Theme.getColor(Theme.key_glass_tabSelected, resourcesProvider); + tab.colorSelected = Theme.getColor(Theme.key_glass_tabSelected, resourcesProvider); + tab.colorSelectedText = Theme.getColor(Theme.key_glass_tabSelectedText, resourcesProvider); + tab.updateColors(); + return tab; + } + + public static GlassTabView createMainTabAnimatedIcon(Context context, Theme.ResourcesProvider resourcesProvider, @RawRes int animationRes, @StringRes int stringRes) { + GlassTabView tab = new GlassTabView(context); + tab.resourcesProvider = resourcesProvider; + tab.tabAnimation = null; + tab.isPlayOnceIcon = true; + tab.textView.setText(LocaleController.getString(stringRes)); + tab.imageView.setAnimation(animationRes, 20, 20); + final RLottieDrawable drawable = tab.imageView.getAnimatedDrawable(); + if (drawable != null) { + drawable.setPlayInDirectionOfCustomEndFrame(true); + drawable.setCurrentFrame(drawable.getFramesCount() - 1, false); + } + tab.imageView.setLayoutParams(LayoutHelper.createFrame(20, 20, Gravity.CENTER_HORIZONTAL | Gravity.TOP, 0, 4, 0, 0)); + tab.colorDefault = Theme.getColor(Theme.key_glass_tabUnselected, resourcesProvider); + tab.colorSelected = Theme.getColor(Theme.key_glass_tabSelected, resourcesProvider); + tab.colorSelectedText = Theme.getColor(Theme.key_glass_tabSelectedText, resourcesProvider); + tab.updateColors(); + return tab; + } + + private boolean isPlayOnceIcon; + + public void playIconAnimationOnce() { + if (!isPlayOnceIcon) { + return; + } + final RLottieDrawable drawable = imageView.getAnimatedDrawable(); + if (drawable == null) { + return; + } + drawable.setPlayInDirectionOfCustomEndFrame(false); + drawable.setCustomEndFrame(drawable.getFramesCount()); + drawable.setCurrentFrame(0, false); + imageView.playAnimation(); + } + public static GlassTabView createAvatar(Context context, Theme.ResourcesProvider resourcesProvider, int currentAccount, @StringRes int stringRes) { GlassTabView tab = new GlassTabView(context); tab.textView.setText(LocaleController.getString(stringRes)); @@ -417,7 +621,7 @@ public class GlassTabView extends FrameLayout implements MainTabsLayout.Tab, Fac backupImageView.setRoundRadius(dp(11)); tab.backupImageView = backupImageView; - tab.addView(backupImageView, LayoutHelper.createFrame(22, 22, Gravity.CENTER_HORIZONTAL | Gravity.TOP, 0, 5, 0, 0)); + tab.addView(backupImageView, LayoutHelper.createFrame(20, 20, Gravity.CENTER_HORIZONTAL | Gravity.TOP, 0, 5, 0, 0)); tab.colorDefault = Theme.getColor(Theme.key_glass_tabUnselected, resourcesProvider); tab.colorSelected = Theme.getColor(Theme.key_glass_tabSelected, resourcesProvider); tab.colorSelectedText = Theme.getColor(Theme.key_glass_tabSelectedText, resourcesProvider); @@ -507,7 +711,13 @@ public class GlassTabView extends FrameLayout implements MainTabsLayout.Tab, Fac @Override public float measureTextWidth() { - return defaultTextPaint.measureText(textView.getText().toString()); + final float textW = defaultTextPaint.measureText(textView.getText().toString()); + if (inlineTextMode) { + // Selected tab needs room for icon+gap+text; unselected only needs the icon. + final float factor = isSelectedAnimator.getFloatValue(); + return lerp(dp(20), dp(20) + dp(4) + textW, factor); + } + return textW; } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/LaunchActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/LaunchActivity.java index 3fb74107..0ff041da 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/LaunchActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/LaunchActivity.java @@ -425,6 +425,8 @@ public class LaunchActivity extends BasePermissionsActivity implements INavigati } requestWindowFeature(Window.FEATURE_NO_TITLE); setTheme(R.style.Theme_TMessages); + // Apply custom font to all TextViews inflated from XML + tw.nekomimi.nekogram.helpers.FontHelper.installFactory(this); try { setTaskDescription(new ActivityManager.TaskDescription(null, null, Theme.getColor(Theme.key_actionBarDefault) | 0xff000000)); } catch (Throwable ignore) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/MainTabsActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/MainTabsActivity.java index 4b3595b1..117e19f5 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/MainTabsActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/MainTabsActivity.java @@ -32,7 +32,6 @@ import androidx.core.view.WindowInsetsCompat; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.ApplicationLoader; import org.telegram.messenger.BuildConfig; -import org.telegram.messenger.ContactsController; import org.telegram.messenger.Emoji; import org.telegram.messenger.FileLoader; import org.telegram.messenger.FileLog; @@ -83,20 +82,27 @@ import tw.nekomimi.nekogram.NekoConfig; import tw.nekomimi.nekogram.helpers.PasscodeHelper; public class MainTabsActivity extends ViewPagerActivity implements NotificationCenter.NotificationCenterDelegate, FactorAnimator.Target { - public static final int TABS_COUNT = 4; + public static final int TABS_COUNT = 3; private static final int POSITION_CHATS = 0; - private static final int POSITION_CONTACTS = 1; - private static final int POSITION_CALLS_OR_SETTINGS = 2; - private static final int POSITION_PROFILE = 3; + private static final int POSITION_PROFILE = 1; + private static final int POSITION_SETTINGS = 2; private static final int INDEX_CHATS = 0; - private static final int INDEX_CONTACTS = 1; - private static final int INDEX_SETTINGS = 2; - private static final int INDEX_CALLS = 3; - private static final int INDEX_PROFILE = 4; + private static final int INDEX_SEARCH = 1; + private static final int INDEX_PROFILE = 2; + private static final int INDEX_SETTINGS = 3; private static int indexToPosition(int index) { - return index > 2 ? index - 1 : index; + switch (index) { + case INDEX_CHATS: + return POSITION_CHATS; + case INDEX_PROFILE: + return POSITION_PROFILE; + case INDEX_SETTINGS: + return POSITION_SETTINGS; + default: + return -1; // search tab has no page + } } @@ -107,11 +113,13 @@ public class MainTabsActivity extends ViewPagerActivity implements NotificationC private IUpdateLayout updateLayout; - private boolean dropCallsFragmentAfterPageScroll; private UpdateLayoutWrapper updateLayoutWrapper; private FrameLayout tabsViewWrapper; private MainTabsLayout tabsView; + private GlassTabView composeButton; + private View tabsSpacer; + private View tabsLeadingSpacer; private BlurredBackgroundDrawable tabsViewBackground; private View fadeView; @@ -212,7 +220,6 @@ public class MainTabsActivity extends ViewPagerActivity implements NotificationC public void onResume() { super.onResume(); blur3_updateColors(); - checkContactsTabBadge(); checkUnreadCount(true); Bulletin.Delegate delegate = new Bulletin.Delegate() { @@ -228,20 +235,6 @@ public class MainTabsActivity extends ViewPagerActivity implements NotificationC showAccountChangeHint(); } - private void checkContactsTabBadge() { - if (tabsView != null && tabs[INDEX_CONTACTS] != null) { - final boolean hasPermission = Build.VERSION.SDK_INT >= 23 && ContactsController.hasContactsPermission(); - if (hasPermission) { - MessagesController.getGlobalNotificationsSettings().edit().putBoolean("askAboutContacts2", true).apply(); - } - if (Build.VERSION.SDK_INT >= 23 && UserConfig.getInstance(currentAccount).syncContacts && !hasPermission && MessagesController.getGlobalNotificationsSettings().getBoolean("askAboutContacts2", true)) { - tabs[INDEX_CONTACTS].setCounter("!", true, true); - } else { - tabs[INDEX_CONTACTS].setCounter(null, true, true); - } - } - } - @Override public void onPause() { super.onPause(); @@ -258,23 +251,33 @@ public class MainTabsActivity extends ViewPagerActivity implements NotificationC tabsView = new MainTabsLayout(context, resourceProvider); tabsView.setClipChildren(false); + // New layout: compact inline tabs (text beside icon). Classic layout: text below the icon. + final boolean inlineTabs = tw.nekomimi.nekogram.NekoConfig.useNewLayout; tabsView.setPadding(dp(DialogsActivity.MAIN_TABS_MARGIN + 4), dp(DialogsActivity.MAIN_TABS_MARGIN + 4), dp(DialogsActivity.MAIN_TABS_MARGIN + 4), dp(DialogsActivity.MAIN_TABS_MARGIN + 4)); - tabs = new GlassTabView[5]; + tabs = new GlassTabView[4]; tabs[INDEX_CHATS] = GlassTabView.createMainTab(context, resourceProvider, GlassTabView.TabAnimation.CHATS, R.string.MainTabsChats); - tabs[INDEX_CONTACTS] = GlassTabView.createMainTab(context, resourceProvider, GlassTabView.TabAnimation.CONTACTS, R.string.MainTabsContacts); - tabs[INDEX_SETTINGS] = GlassTabView.createMainTab(context, resourceProvider, GlassTabView.TabAnimation.SETTINGS, R.string.Settings); - tabs[INDEX_CALLS] = GlassTabView.createMainTab(context, resourceProvider, GlassTabView.TabAnimation.CALLS, R.string.MainTabsCalls); + tabs[INDEX_SEARCH] = GlassTabView.createMainTabAnimatedIcon(context, resourceProvider, R.raw.options_to_search, R.string.Search); tabs[INDEX_PROFILE] = GlassTabView.createAvatar(context, resourceProvider, currentAccount, R.string.MainTabsProfile); + tabs[INDEX_SETTINGS] = GlassTabView.createMainTab(context, resourceProvider, GlassTabView.TabAnimation.SETTINGS, R.string.Settings); + tabs[INDEX_CHATS].setInlineTextMode(inlineTabs); + tabs[INDEX_SEARCH].setInlineTextMode(inlineTabs); + tabs[INDEX_PROFILE].setInlineTextMode(inlineTabs); + tabs[INDEX_SETTINGS].setInlineTextMode(inlineTabs); + if (!inlineTabs) { + // Classic Telegram style: 24dp icon on top, text label below + tabs[INDEX_CHATS].setClassicTabMode(true); + tabs[INDEX_SEARCH].setClassicTabMode(true); + tabs[INDEX_PROFILE].setClassicTabMode(true); + tabs[INDEX_SETTINGS].setClassicTabMode(true); + } tabs[INDEX_CHATS].setOnLongClickListener(this::openFoldersSelector); - tabs[INDEX_CONTACTS].setOnLongClickListener(this::openContactsSelector); - tabs[INDEX_CALLS].setOnLongClickListener(this::openCallsSelector); tabs[INDEX_PROFILE].setOnLongClickListener(this::openAccountSelector); tabsView.addTabToIgnoreClick(tabs[INDEX_CHATS]); - tabsView.addTabToIgnoreClick(tabs[INDEX_CONTACTS]); + tabsView.addTabToIgnoreClick(tabs[INDEX_SEARCH]); tabsView.addTabToIgnoreClick(tabs[INDEX_PROFILE]); - tabsView.addTabToIgnoreClick(tabs[INDEX_CALLS]); + tabsView.addTabToIgnoreClick(tabs[INDEX_SETTINGS]); for (int index = 0; index < tabs.length; index++) { final GlassTabView view = tabs[index]; @@ -285,6 +288,12 @@ public class MainTabsActivity extends ViewPagerActivity implements NotificationC return; } + if (position < 0) { + // Search tab: open search inside the chats list + onSearchTabClick(); + return; + } + if (viewPager.getCurrentPosition() == position) { final BaseFragment fragment = getCurrentVisibleFragment(); if (fragment instanceof MainTabsActivity.TabFragmentDelegate) { @@ -300,7 +309,36 @@ public class MainTabsActivity extends ViewPagerActivity implements NotificationC tabsView.addView(tabs[index]); tabsView.setViewVisible(view, true, false); } - checkUi_callTabVisible(getUserConfig().showCallsTab, false); + + // No leading spacer — tabs are left-aligned, trailing spacer pushes compose to the right. + // Classic mode also skips leading spacer (pill centered by wrapper gravity instead). + + // Flexible spacer pushes the compose button to the right edge of the bar + // In classic mode the spacer and compose button are hidden — pill wraps the 4 tabs only. + tabsSpacer = new View(context); + tabsView.setSpacerView(tabsSpacer); + tabsView.addView(tabsSpacer); + tabsView.setViewVisible(tabsSpacer, inlineTabs, false); + tabsView.addTabToIgnoreClick(tabsSpacer); + + // Compose button lives inside the same glass bar, on the right. + // Classic mode: hidden from tab bar (pill must wrap 4 tabs only). + composeButton = GlassTabView.createComposeButton(context, resourceProvider, R.drawable.filled_fab_compose_32, inlineTabs ? 32 : 40); + composeButton.setOnClickListener(v -> { + if (viewPager.isManualScrolling() || viewPager.isTouch()) { + return; + } + onComposeClick(); + }); + tabsView.addTabToIgnoreClick(composeButton); + tabsView.addView(composeButton); + tabsView.setViewVisible(composeButton, inlineTabs, false); + + // Fill the full width only in new layout; classic pill wraps its own content + tabsView.setMinTotalWidthDp(0); + if (inlineTabs) { + tabsView.setFillParentWidth(true); + } selectTab(viewPager.getCurrentPosition(), false); @@ -332,8 +370,14 @@ public class MainTabsActivity extends ViewPagerActivity implements NotificationC tabsViewWrapper = new FrameLayout(context); tabsViewWrapper.setOnClickListener(v -> {}); - tabsViewWrapper.addView(tabsView, LayoutHelper.createFrame(328 + DialogsActivity.MAIN_TABS_MARGIN * 2, DialogsActivity.MAIN_TABS_HEIGHT_WITH_MARGINS, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL)); + if (inlineTabs) { + tabsViewWrapper.addView(tabsView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, DialogsActivity.MAIN_TABS_HEIGHT_WITH_MARGINS, Gravity.BOTTOM | Gravity.FILL_HORIZONTAL, DialogsActivity.MAIN_TABS_MARGIN, 0, DialogsActivity.MAIN_TABS_MARGIN, 0)); + } else { + // Classic mode: pill wraps its content and is centered horizontally + tabsViewWrapper.addView(tabsView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, DialogsActivity.MAIN_TABS_HEIGHT_WITH_MARGINS, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL)); + } tabsViewWrapper.setClipToPadding(false); + contentView.addView(tabsViewWrapper, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.BOTTOM)); updateLayoutWrapper = new UpdateLayoutWrapper(context); @@ -363,50 +407,7 @@ public class MainTabsActivity extends ViewPagerActivity implements NotificationC } public boolean openContactsSelector(View anchor) { - if (getContext() == null || getParentActivity() == null) return false; - final ItemOptions o = ItemOptions.makeOptions(this, anchor); - o.add(R.drawable.msg_contact_add, getString(R.string.NewContact), () -> { - new NewContactBottomSheet(this, getContext()).show(); - }); - o.add(R.drawable.msg_calls, getString(R.string.VoipChatRecentCalls), () -> { - Bundle args = new Bundle(); - args.putBoolean("needFinishFragment", false); - presentFragment(new CallLogActivity(args)); - }); - o.setBlur(true); - o.translate(0, -dp(4)); - o.setGravity(Gravity.LEFT); - final ShapeDrawable bg = Theme.createRoundRectDrawable(dp(28), getThemedColor(Theme.key_windowBackgroundWhite)); - bg.getPaint().setShadowLayer(dp(6), 0, dp(1), Theme.multAlpha(0xFF000000, 0.15f)); - o.setScrimViewBackground(bg); - o.show(); - return true; - } - - public boolean openCallsSelector(View anchor) { - if (getContext() == null || getParentActivity() == null) return false; - final ItemOptions o = ItemOptions.makeOptions(this, anchor); - o.add(R.drawable.menu_call_create, getString(R.string.GroupCallCreate2), () -> CallLogActivity.openCreateCall(this)); - if (getUserConfig().showCallsTab) { - o.add(R.drawable.msg_archive_hide, getString(R.string.HideCallTab), () -> { - getUserConfig().setShowCallsTab(false); - checkUi_callTabVisible(false, true); - NotificationCenter.getInstance(currentAccount).postNotificationName(NotificationCenter.callTabsVisibleToggled); - }); - } else { - o.add(R.drawable.menu_add_tab_24, getString(R.string.GroupCallShowInMainTabs), () -> { - getUserConfig().setShowCallsTab(true); - checkUi_callTabVisible(true, true); - NotificationCenter.getInstance(currentAccount).postNotificationName(NotificationCenter.callTabsVisibleToggled); - }); - } - o.setBlur(true); - o.translate(0, -dp(4)); - final ShapeDrawable bg = Theme.createRoundRectDrawable(dp(28), getThemedColor(Theme.key_windowBackgroundWhite)); - bg.getPaint().setShadowLayer(dp(6), 0, dp(1), Theme.multAlpha(0xFF000000, 0.15f)); - o.setScrimViewBackground(bg); - o.show(); - return true; + return false; } private Integer pendingFolderId; @@ -457,8 +458,38 @@ public class MainTabsActivity extends ViewPagerActivity implements NotificationC return true; } - private void openFolder(int folderId) { - if (viewPager.getCurrentPosition() == POSITION_CHATS && dialogsActivity != null) { + private void onSearchTabClick() { + tabs[INDEX_SEARCH].playIconAnimationOnce(); + if (dialogsActivity == null) { + prepareDialogsActivity(null); + } + if (viewPager.getCurrentPosition() != POSITION_CHATS) { + selectTab(POSITION_CHATS, true); + viewPager.scrollToPosition(POSITION_CHATS); + AndroidUtilities.runOnUIThread(() -> { + if (dialogsActivity != null) { + dialogsActivity.openSearch(); + } + }, 200); + } else if (dialogsActivity != null) { + dialogsActivity.openSearch(); + } + } + + private void onComposeClick() { + if (dialogsActivity == null) { + prepareDialogsActivity(null); + } + if (viewPager.getCurrentPosition() != POSITION_CHATS) { + selectTab(POSITION_CHATS, true); + viewPager.scrollToPosition(POSITION_CHATS); + } + if (dialogsActivity != null) { + dialogsActivity.performComposeClick(); + } + } + + private void openFolder(int folderId) { if (viewPager.getCurrentPosition() == POSITION_CHATS && dialogsActivity != null) { dialogsActivity.scrollToFolder(folderId); } else { if (dialogsActivity == null) { @@ -603,10 +634,6 @@ public class MainTabsActivity extends ViewPagerActivity implements NotificationC if (viewPager != null) { final int currentPosition = viewPager.getCurrentPosition(); - if (currentPosition != POSITION_CALLS_OR_SETTINGS && dropCallsFragmentAfterPageScroll) { - dropFragmentAtPosition(POSITION_CALLS_OR_SETTINGS); - dropCallsFragmentAfterPageScroll = false; - } if (currentPosition != POSITION_PROFILE) { dropFragmentAtPosition(POSITION_PROFILE); } @@ -676,19 +703,7 @@ public class MainTabsActivity extends ViewPagerActivity implements NotificationC @Override protected BaseFragment createBaseFragmentAt(int position) { - if (position == POSITION_CONTACTS) { - Bundle args = new Bundle(); - args.putBoolean("needPhonebook", true); - args.putBoolean("needFinishFragment", false); - args.putBoolean("hasMainTabs", true); - return new ContactsActivity(args); - } else if (position == POSITION_CALLS_OR_SETTINGS) { - if (getUserConfig().showCallsTab) { - Bundle args = new Bundle(); - args.putBoolean("needFinishFragment", false); - args.putBoolean("hasMainTabs", true); - return new CallLogActivity(args); - } + if (position == POSITION_SETTINGS) { Bundle args = new Bundle(); args.putBoolean("hasMainTabs", true); return new SettingsActivity(args); @@ -761,7 +776,7 @@ public class MainTabsActivity extends ViewPagerActivity implements NotificationC } private boolean canScrollInternal(MotionEvent ev, boolean forward) { - if (NekoConfig.hideBottomNavigationBar) { + if (NekoConfig.isBottomNavigationBarHidden()) { return false; } final BaseFragment fragment = getCurrentVisibleFragment(); @@ -789,7 +804,7 @@ public class MainTabsActivity extends ViewPagerActivity implements NotificationC ViewGroup.MarginLayoutParams lp; { - final int height = navigationBarHeight + updateLayoutHeight + dp(NekoConfig.hideBottomNavigationBar ? 0 : DialogsActivity.MAIN_TABS_HEIGHT_WITH_MARGINS); + final int height = navigationBarHeight + updateLayoutHeight + dp(NekoConfig.isBottomNavigationBarHidden() ? 0 : DialogsActivity.MAIN_TABS_HEIGHT_WITH_MARGINS); lp = (ViewGroup.MarginLayoutParams) fadeView.getLayoutParams(); if (lp.height != height) { lp.height = height; @@ -851,22 +866,10 @@ public class MainTabsActivity extends ViewPagerActivity implements NotificationC } } else if (id == NotificationCenter.needSetDayNightTheme) { clearAllHiddenFragments(); - } else if (id == NotificationCenter.callTabsVisibleToggled) { - final boolean callTabsVisible = getUserConfig().showCallsTab; - checkUi_callTabVisible(callTabsVisible, true); - if (viewPager != null && viewPager.getCurrentPosition() == POSITION_CALLS_OR_SETTINGS) { - viewPager.scrollToPosition(POSITION_CHATS); - selectTab(POSITION_CHATS, true); - dropCallsFragmentAfterPageScroll = true; - } else { - dropFragmentAtPosition(POSITION_CALLS_OR_SETTINGS); - } } else if (id == NotificationCenter.mainUserInfoChanged) { if (tabs != null && tabs[INDEX_PROFILE] != null) { tabs[INDEX_PROFILE].updateUserAvatar(currentAccount); } - } else if (id == NotificationCenter.contactsPermissionBadgeCheck) { - checkContactsTabBadge(); } } @@ -882,9 +885,7 @@ public class MainTabsActivity extends ViewPagerActivity implements NotificationC .add(NotificationCenter.fileLoadFailed) .add(NotificationCenter.notificationsCountUpdated) .add(NotificationCenter.updateInterfaces) - .add(NotificationCenter.callTabsVisibleToggled) - .add(NotificationCenter.mainUserInfoChanged) - .add(NotificationCenter.contactsPermissionBadgeCheck); + .add(NotificationCenter.mainUserInfoChanged); globalObserversGroup = NotificationCenter.getGlobalInstance().createObserversGroup(this) .add(NotificationCenter.appUpdateAvailable) @@ -916,7 +917,7 @@ public class MainTabsActivity extends ViewPagerActivity implements NotificationC } private void checkUi_fadeView() { - if (viewPager == null || fadeView == null || NekoConfig.hideBottomNavigationBar) { + if (viewPager == null || fadeView == null || NekoConfig.isBottomNavigationBarHidden()) { return; } @@ -931,8 +932,9 @@ public class MainTabsActivity extends ViewPagerActivity implements NotificationC } private void checkUi_tabsPosition() { - if (NekoConfig.hideBottomNavigationBar) { + if (NekoConfig.isBottomNavigationBarHidden()) { tabsView.setVisibility(View.GONE); + if (composeButton != null) composeButton.setVisibility(View.GONE); return; } final boolean isUpdateLayoutVisible = updateLayoutWrapper.isUpdateLayoutVisible(); @@ -941,21 +943,15 @@ public class MainTabsActivity extends ViewPagerActivity implements NotificationC final int hiddenY = normalY + dp(40); final float factor = animatorTabsVisible.getFloatValue(); - final float scale = lerp(0.85f, 1f, factor); tabsViewWrapper.setTranslationY(lerp(hiddenY, normalY, factor)); - //tabsView.setScaleX(scale); - //tabsView.setScaleY(scale); tabsView.setClickable(factor > 1); tabsView.setEnabled(factor > 1); tabsView.setAlpha(factor); tabsView.setVisibility(factor > 0 ? View.VISIBLE : View.GONE); - } - - private void checkUi_callTabVisible(boolean callTabsVisible, boolean animated) { - if (tabsView != null) { - tabsView.setViewVisible(tabs[INDEX_SETTINGS], !callTabsVisible, animated); - tabsView.setViewVisible(tabs[INDEX_CALLS], callTabsVisible, animated); + if (composeButton != null) { + composeButton.setAlpha(factor); + composeButton.setVisibility(factor > 0 ? View.VISIBLE : View.GONE); } } @@ -1024,7 +1020,7 @@ public class MainTabsActivity extends ViewPagerActivity implements NotificationC private boolean accountSwitchHintShown; private void showAccountChangeHint() { - if (accountSwitchHintShown || NekoConfig.hideBottomNavigationBar) return; + if (accountSwitchHintShown || NekoConfig.isBottomNavigationBarHidden()) return; if (accountSwitchHint == null && HintsController.Hint.AccountSwitchHint.show()) { AndroidUtilities.runOnUIThread(() -> { @@ -1088,5 +1084,8 @@ public class MainTabsActivity extends ViewPagerActivity implements NotificationC tabView.updateColorsLottie(); } } + if (composeButton != null) { + composeButton.updateColorsLottie(); + } } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/MainTabsLayout.java b/TMessagesProj/src/main/java/org/telegram/ui/MainTabsLayout.java index 65fbb5f1..d98ca51b 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/MainTabsLayout.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/MainTabsLayout.java @@ -41,6 +41,42 @@ public class MainTabsLayout extends AnimatedLinearLayout { this.resourcesProvider = resourcesProvider; } + private int minTotalWidthDp = 320; + + public void setMinTotalWidthDp(int value) { + this.minTotalWidthDp = value; + requestLayout(); + } + + private View spacerView; + private View leadingSpacerView; + private boolean fillParentWidth; + private boolean equalTabs; // when true: all tabs share the full width equally (classic style) + + public void setSpacerView(View view) { + this.spacerView = view; + } + + public void setEqualTabs(boolean equal) { + this.equalTabs = equal; + requestLayout(); + } + + /** + * Optional spacer placed before the tabs. When set together with the trailing + * {@link #spacerView}, the free space is split evenly between the two so the tab + * group is centered while the trailing content (e.g. compose button) stays at the + * right edge. + */ + public void setLeadingSpacerView(View view) { + this.leadingSpacerView = view; + } + + public void setFillParentWidth(boolean fill) { + this.fillParentWidth = fill; + requestLayout(); + } + @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int width = MeasureSpec.getSize(widthMeasureSpec); @@ -49,8 +85,38 @@ public class MainTabsLayout extends AnimatedLinearLayout { measureTabTexts(); + // Classic equal-width mode: divide full width evenly among visible tabs + if (equalTabs && visibleChildCount > 0) { + final int totalW = width - getPaddingLeft() - getPaddingRight(); + final int tabW = totalW / visibleChildCount; + final int remainder = totalW - tabW * visibleChildCount; + setMeasuredDimension(width, height); + int leftPos = 0; + int tabIdx = 0; + for (int a = 0, N = getChildCount(); a < N; a++) { + final View child = getChildAt(a); + if (!isViewVisible(child)) { + tabsWidth[a] = 0; + tabsLeftPos[a] = leftPos; + child.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(tabHeight, MeasureSpec.EXACTLY)); + continue; + } + // Give the last tab any leftover pixel from integer division + final int w = (tabIdx == visibleChildCount - 1) ? tabW + remainder : tabW; + tabsWidth[a] = w; + tabsLeftPos[a] = leftPos; + leftPos += w; + tabIdx++; + child.measure(MeasureSpec.makeMeasureSpec(w, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(tabHeight, MeasureSpec.EXACTLY)); + } + calculateTotalSizesAfterMeasure(); + return; + } + final int maxTotalWidthForTabs = width - getPaddingLeft() - getPaddingRight(); - final int minTotalWidthForTabs = Math.min(dp(320), maxTotalWidthForTabs); + final int minTotalWidthForTabs = Math.min(dp(minTotalWidthDp), maxTotalWidthForTabs); final int tabPadding = dp(16); final int minTabTextWidthIfEq = (minTotalWidthForTabs / visibleChildCount) - tabPadding * 2; @@ -61,7 +127,7 @@ public class MainTabsLayout extends AnimatedLinearLayout { int totalWeight = 0; for (int a = 0, N = getChildCount(); a < N; a++) { final View child = getChildAt(a); - if (!isViewVisible(child)) { + if (!isViewVisible(child) || child == spacerView || child == leadingSpacerView) { tabsTextWidth[a] = tabsTextWidthWithMargin[a] = 0; tabsWeight[a] = 0; continue; @@ -81,56 +147,64 @@ public class MainTabsLayout extends AnimatedLinearLayout { if (totalWeight == 0) { for (int a = 0, N = getChildCount(); a < N; a++) { - tabsWeight[a] = isViewVisible(getChildAt(a)) ? 1 : 0; + final View child = getChildAt(a); + tabsWeight[a] = (isViewVisible(child) && child != spacerView && child != leadingSpacerView) ? 1 : 0; } totalWeight = visibleChildCount; } - if (totalWidth > maxTotalWidthForTabs) { - final float m = maxTotalWidthForTabs / totalWidth; - for (int a = 0, N = getChildCount(); a < N; a++) { - tabsTextWidthWithMargin[a] *= m; - } - } else if (totalWidth < minTotalWidthForTabs) { - final float growW = minTotalWidthForTabs - totalWidth; - final float growP = growW / totalWeight; - - //boolean needStage2 = false; - for (int a = 0, N = getChildCount(); a < N; a++) { - final float maxGrow = maxTabTextWidthIfEq - tabsTextWidthWithMargin[a]; - //if (tabsWeight[a] > 0 && growP * tabsWeight[a] > maxGrow) { - // needStage2 = true; - // tabsTextWidthWithMargin[a] = maxTabTextWidthIfEq; - //} else { - tabsTextWidthWithMargin[a] += growP * tabsWeight[a]; - //} - } - - /*if (needStage2) { - totalWidth = 0; - for (int a = 0, N = getChildCount(); a < N; a++) { - totalWidth += tabsTextWidthWithMargin[a]; - } - - final float m = minTotalWidthForTabs / totalWidth; + if (!fillParentWidth) { + if (totalWidth > maxTotalWidthForTabs) { + final float m = maxTotalWidthForTabs / totalWidth; for (int a = 0, N = getChildCount(); a < N; a++) { tabsTextWidthWithMargin[a] *= m; } - }*/ + } else if (totalWidth < minTotalWidthForTabs) { + final float growW = minTotalWidthForTabs - totalWidth; + final float growP = growW / totalWeight; + + for (int a = 0, N = getChildCount(); a < N; a++) { + tabsTextWidthWithMargin[a] += growP * tabsWeight[a]; + } + } + } else { + // tabs keep their natural width; spacer absorbs the remaining width + if (totalWidth > maxTotalWidthForTabs) { + final float m = maxTotalWidthForTabs / totalWidth; + for (int a = 0, N = getChildCount(); a < N; a++) { + tabsTextWidthWithMargin[a] *= m; + } + totalWidth = maxTotalWidthForTabs; + } } + final int remainingSpace = (int) Math.max(0, maxTotalWidthForTabs - totalWidth); + // When a leading spacer is present, split the free space evenly before and after the + // tab group so the tabs are centered while trailing content (compose) stays at the right. + final boolean hasLeading = leadingSpacerView != null && isViewVisible(leadingSpacerView); + final int leadingSpace = hasLeading ? remainingSpace / 2 : 0; + final int trailingSpace = remainingSpace - leadingSpace; int l = 0; for (int a = 0, N = getChildCount(); a < N; a++) { - if (!isViewVisible(getChildAt(a))) { + final View child = getChildAt(a); + if (!isViewVisible(child)) { continue; } - tabsWidth[a] = Math.round(tabsTextWidthWithMargin[a]); + if (child == leadingSpacerView) { + // Always give leading spacer half the free space (centers the tab group) + tabsWidth[a] = leadingSpace; + } else if (child == spacerView) { + tabsWidth[a] = fillParentWidth ? trailingSpace : leadingSpace; + } else { + tabsWidth[a] = Math.round(tabsTextWidthWithMargin[a]); + } tabsLeftPos[a] = l; l += tabsWidth[a]; } - setMeasuredDimension(l + getPaddingLeft() + getPaddingRight(), height); + final int measuredWidth = fillParentWidth ? (maxTotalWidthForTabs + getPaddingLeft() + getPaddingRight()) : (l + getPaddingLeft() + getPaddingRight()); + setMeasuredDimension(measuredWidth, height); for (int a = 0, N = getChildCount(); a < N; a++) { final View child = getChildAt(a); child.measure( @@ -279,7 +353,9 @@ public class MainTabsLayout extends AnimatedLinearLayout { for (int a = 0, N = getEntriesCount(); a < N; a++) { final ListAnimator.Entry entry = getEntry(a); final float width = entry.getRectF().width(); - ((GlassTabView) entry.item.view).setVisualWidth(width); + if (entry.item.view instanceof GlassTabView) { + ((GlassTabView) entry.item.view).setVisualWidth(width); + } } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ProxyListActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ProxyListActivity.java index f389e986..edd4e3d4 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ProxyListActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ProxyListActivity.java @@ -49,6 +49,7 @@ import org.telegram.messenger.ProxyRotationController; import org.telegram.messenger.R; import org.telegram.messenger.SharedConfig; import org.telegram.messenger.browser.Browser; +import org.telegram.messenger.ApplicationLoader; import org.telegram.tgnet.ConnectionsManager; import org.telegram.ui.ActionBar.ActionBar; import org.telegram.ui.ActionBar.ActionBarMenu; @@ -83,6 +84,7 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente private final static boolean IS_PROXY_ROTATION_AVAILABLE = true; private static final int MENU_DELETE = 0; private static final int MENU_SHARE = 1; + private static final int MENU_REFRESH_SERVERS = 2; private ListAdapter listAdapter; private RecyclerListView listView; @@ -113,6 +115,7 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente private int rotationTimeoutInfoRow; private int callsDetailRow; private int deleteAllRow; + private int refreshServersRow; private int newsShadowRow; private int newsStartRow; @@ -403,9 +406,14 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente public void onItemClick(int id) { if (id == -1) { finishFragment(); + } else if (id == MENU_REFRESH_SERVERS) { + onRefreshServersClicked(); } } }); + ActionBarMenu menu = actionBar.createMenu(); + menu.addItem(MENU_REFRESH_SERVERS, R.drawable.cloud_sync) + .setContentDescription(getString(R.string.FoxRefreshServers)); listAdapter = new ListAdapter(context); @@ -566,6 +574,8 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente } }); showDialog(builder.create()); + } else if (position == refreshServersRow) { + onRefreshServersClicked(); } else if (position == deleteAllRow) { AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); builder.setMessage(getString(R.string.DeleteAllProxiesConfirm)); @@ -710,6 +720,27 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente return super.onBackPressed(invoked); } + private void onRefreshServersClicked() { + Context ctx = getParentActivity(); + if (ctx == null) return; + // Show a brief loading toast + android.widget.Toast.makeText(ctx, + getString(R.string.FoxRefreshServersLoading), + android.widget.Toast.LENGTH_SHORT).show(); + XrayController.refreshServers(ctx, () -> + AndroidUtilities.runOnUIThread(() -> { + // Reload builtin proxies into SharedConfig + SharedConfig.reloadBuiltinProxies(); + updateRows(true); + if (listAdapter != null) listAdapter.notifyDataSetChanged(); + android.widget.Toast.makeText( + ApplicationLoader.applicationContext, + getString(R.string.FoxRefreshServersDone), + android.widget.Toast.LENGTH_SHORT).show(); + }) + ); + } + private void updateRows(boolean notify) { rowCount = 0; useProxyRow = rowCount++; @@ -741,10 +772,16 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente // Hide built-in proxies that don't match the current network // (e.g. "WiFi" servers on mobile data and vice versa), but never // hide the proxy that's currently selected. + // Also hide sponsor-only servers for non-sponsors. + boolean isSponsor = tw.nekomimi.nekogram.helpers.SponsorHelper.isCurrentUserSponsor(); for (java.util.Iterator it = proxyList.iterator(); it.hasNext(); ) { SharedConfig.ProxyInfo info = it.next(); if (info != SharedConfig.currentProxy && !XrayController.matchesCurrentNetwork(info)) { it.remove(); + continue; + } + if (info.sponsorOnly && !isSponsor && info != SharedConfig.currentProxy) { + it.remove(); } } @@ -785,6 +822,7 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente proxyEndRow = -1; } proxyAddRow = rowCount++; + refreshServersRow = rowCount++; proxyShadowRow = rowCount++; newsList.clear(); newsList.addAll(ConfigHelper.getNewsForProxy()); @@ -1022,7 +1060,9 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente TextSettingsCell textCell = (TextSettingsCell) holder.itemView; textCell.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); if (position == proxyAddRow) { - textCell.setText(getString(R.string.AddProxy), deleteAllRow != -1); + textCell.setText(getString(R.string.AddProxy), true); + } else if (position == refreshServersRow) { + textCell.setText(getString(R.string.FoxRefreshServers), deleteAllRow != -1); } else if (position == deleteAllRow) { textCell.setTextColor(Theme.getColor(Theme.key_text_RedRegular)); textCell.setText(getString(R.string.DeleteAllProxies), false); @@ -1135,7 +1175,7 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente @Override public boolean isEnabled(RecyclerView.ViewHolder holder) { int position = holder.getAdapterPosition(); - return position == useProxyRow || position == rotationRow || position == callsRow || position == proxyAddRow || position == deleteAllRow || position >= proxyStartRow && position < proxyEndRow || position >= newsStartRow && position < newsEndRow; + return position == useProxyRow || position == rotationRow || position == callsRow || position == proxyAddRow || position == deleteAllRow || position == refreshServersRow || position >= proxyStartRow && position < proxyEndRow || position >= newsStartRow && position < newsEndRow; } @Override @@ -1215,7 +1255,7 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente public int getItemViewType(int position) { if (position == useProxyShadowRow || position == proxyShadowRow || position == newsShadowRow) { return VIEW_TYPE_SHADOW; - } else if (position == proxyAddRow || position == deleteAllRow) { + } else if (position == proxyAddRow || position == deleteAllRow || position == refreshServersRow) { return VIEW_TYPE_TEXT_SETTING; } else if (position == useProxyRow || position == rotationRow || position == callsRow) { return VIEW_TYPE_TEXT_CHECK; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/SettingsActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/SettingsActivity.java index 281d6dc0..ee494c1d 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/SettingsActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/SettingsActivity.java @@ -1162,6 +1162,12 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter iconBackground.setColor(iconColorTop, iconColorBottom); iconView.setImageResource(icon); + // In light theme use the original icon color; in dark use white + if (!Theme.isCurrentThemeDark()) { + iconView.setColorFilter(new android.graphics.PorterDuffColorFilter(iconColorBottom, android.graphics.PorterDuff.Mode.SRC_IN)); + } else { + iconView.setColorFilter(new android.graphics.PorterDuffColorFilter(0xFFFFFFFF, android.graphics.PorterDuff.Mode.SRC_IN)); + } titleView.setText(title); subtitleView.setVisibility((twoLines = !TextUtils.isEmpty(subtitle)) ? View.VISIBLE : View.GONE); subtitleView.setText(subtitle); @@ -1190,6 +1196,12 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter } public void setColor(int topColor, int bottomColor) { + // In light theme blend with white for soft pastel look; keep vivid in dark theme + if (!Theme.isCurrentThemeDark()) { + final float mix = 0.45f; + topColor = androidx.core.graphics.ColorUtils.blendARGB(topColor, 0xFFFFFFFF, mix); + bottomColor = androidx.core.graphics.ColorUtils.blendARGB(bottomColor, 0xFFFFFFFF, mix); + } gradient = new LinearGradient(0, 0, 0, dp(28), new int[] { topColor, bottomColor }, new float[] { 0, 1 }, Shader.TileMode.CLAMP); paint.setShader(gradient); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Stories/DialogStoriesCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Stories/DialogStoriesCell.java index a663b16f..96d9d4a8 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Stories/DialogStoriesCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Stories/DialogStoriesCell.java @@ -457,6 +457,10 @@ public class DialogStoriesCell extends FrameLayout implements NotificationCenter this.menuItemsOffset = menuItemsOffset; } + public void setExtraLeftInset(int insetPx) { + recyclerListView.setPadding(dp(3) + insetPx, 0, dp(3), 0); + } + public void openStoryForCell(StoryCell cell) { openStoryForCell(cell, false); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/TopicsFragment.java b/TMessagesProj/src/main/java/org/telegram/ui/TopicsFragment.java index ca98b9cb..e2a519f0 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/TopicsFragment.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/TopicsFragment.java @@ -404,8 +404,8 @@ public class TopicsFragment extends BaseFragment implements NotificationCenter.N @Override public View createView(Context context) { - additionNavigationBarHeight = parentDialogsActivity != null && parentDialogsActivity.hasMainTabs && !NekoConfig.hideBottomNavigationBar ? dp(DialogsActivity.MAIN_TABS_HEIGHT_WITH_MARGINS) : 0; - additionFloatingButtonOffset = parentDialogsActivity != null && parentDialogsActivity.hasMainTabs && !NekoConfig.hideBottomNavigationBar ? dp(DialogsActivity.MAIN_TABS_HEIGHT + DialogsActivity.MAIN_TABS_MARGIN) : 0; + additionNavigationBarHeight = parentDialogsActivity != null && parentDialogsActivity.hasMainTabs && !NekoConfig.isBottomNavigationBarHidden() ? dp(DialogsActivity.MAIN_TABS_HEIGHT_WITH_MARGINS) : 0; + additionFloatingButtonOffset = parentDialogsActivity != null && parentDialogsActivity.hasMainTabs && !NekoConfig.isBottomNavigationBarHidden() ? dp(DialogsActivity.MAIN_TABS_HEIGHT + DialogsActivity.MAIN_TABS_MARGIN) : 0; fragmentView = contentView = new SizeNotifierFrameLayout(context) { { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ViewPagerActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ViewPagerActivity.java index 127952b0..7e6a869e 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ViewPagerActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ViewPagerActivity.java @@ -178,7 +178,7 @@ public abstract class ViewPagerActivity extends BaseFragment { @Override public void clearViews() { if (viewPager != null) { - initialFragmentPosition = NekoConfig.hideBottomNavigationBar ? 0 : viewPager.getCurrentPosition(); + initialFragmentPosition = NekoConfig.isBottomNavigationBarHidden() ? 0 : viewPager.getCurrentPosition(); } for (int a = 0, N = fragmentsArr.size(); a < N; a++) { final FragmentState state = fragmentsArr.valueAt(a); diff --git a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/NekoConfig.java b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/NekoConfig.java index 0c01a7c6..e132a06c 100644 --- a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/NekoConfig.java +++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/NekoConfig.java @@ -147,6 +147,11 @@ public class NekoConfig { public static boolean hideBottomNavigationBar = false; public static boolean bottomFilterTabs = false; public static boolean strokeOnViews = true; + public static boolean centerChatTitle = false; + public static boolean useSystemFont = false; + /** When true: new FoxiGram layout (centered title, avatar right, calls in menu). + * When false: stock Telegram layout. Default true. */ + public static boolean useNewLayout = true; public static boolean shouldNOTTrustMe = false; @@ -260,6 +265,9 @@ public class NekoConfig { hideBottomNavigationBar = preferences.getBoolean("hideBottomNavigationBar", false); bottomFilterTabs = preferences.getBoolean("bottomFilterTabs", false); strokeOnViews = preferences.getBoolean("strokeOnViews", true); + centerChatTitle = preferences.getBoolean("centerChatTitle", false); + useSystemFont = preferences.getBoolean("useSystemFont", false); + useNewLayout = preferences.getBoolean("useNewLayout", true); cameraInVideoMessages = preferences.getInt("cameraInVideoMessages", CAMERA_FRONT); LensHelper.checkLensSupportAsync(); @@ -451,6 +459,47 @@ public class NekoConfig { editor.apply(); } + /** + * Effective visibility of the custom bottom navigation bar (tabs). + * Only the explicit user toggle hides it; the bar stays visible regardless of the + * new-layout setting (its visual style adapts elsewhere instead of disappearing). + */ + public static boolean isBottomNavigationBarHidden() { + return hideBottomNavigationBar; + } + + public static void toggleCenterChatTitle() { + centerChatTitle = !centerChatTitle; + SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("nekoconfig", Activity.MODE_PRIVATE); + SharedPreferences.Editor editor = preferences.edit(); + editor.putBoolean("centerChatTitle", centerChatTitle); + editor.apply(); + } + + public static void toggleUseNewLayout() { + useNewLayout = !useNewLayout; + // centerChatTitle follows useNewLayout automatically + centerChatTitle = useNewLayout; + SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("nekoconfig", Activity.MODE_PRIVATE); + SharedPreferences.Editor editor = preferences.edit(); + editor.putBoolean("useNewLayout", useNewLayout); + editor.putBoolean("centerChatTitle", centerChatTitle); + editor.apply(); + // Invalidate typeface caches so layout rebuilds cleanly + AndroidUtilities.mediumTypeface = null; + } + + public static void toggleUseSystemFont() { + useSystemFont = !useSystemFont; + SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("nekoconfig", Activity.MODE_PRIVATE); + SharedPreferences.Editor editor = preferences.edit(); + editor.putBoolean("useSystemFont", useSystemFont); + editor.apply(); + // Clear typeface cache so changes take effect immediately + org.telegram.messenger.AndroidUtilities.mediumTypeface = null; + org.telegram.messenger.SharedConfig.useSystemBoldFont = useSystemFont; + } + public static void toggleKeepFormatting() { keepFormatting = !keepFormatting; SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("nekoconfig", Activity.MODE_PRIVATE); diff --git a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/helpers/EmojiHelper.java b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/helpers/EmojiHelper.java index 3d070926..0b8d151c 100644 --- a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/helpers/EmojiHelper.java +++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/helpers/EmojiHelper.java @@ -33,13 +33,19 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.Comparator; import java.util.HashMap; +import java.util.List; import java.util.Locale; import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; import tw.nekomimi.nekogram.NekoConfig; @@ -48,6 +54,35 @@ import tw.nekomimi.nekogram.NekoConfig; public class EmojiHelper { private static final String EMOJI_PACKS_FILE_DIR; public static EmojiPack DEFAULT_PACK = new EmojiPack("Apple", "default", "", "", 0); + + // Built-in downloadable emoji packs + public static final List BUILT_IN_PACKS = Collections.unmodifiableList(Arrays.asList( + new BuiltInEmojiPack( + "Noto Emoji", + "noto", + "https://github.com/googlefonts/noto-emoji/raw/main/fonts/NotoColorEmoji.ttf", + "Apache 2.0" + ), + new BuiltInEmojiPack( + "Twemoji", + "twemoji", + "https://github.com/mozilla/twemoji-colr/releases/latest/download/Twemoji.Mozilla.ttf", + "CC-BY 4.0" + ), + new BuiltInEmojiPack( + "Fluent Emoji", + "fluent", + "https://github.com/nweiz/fluent-emoji-font/releases/latest/download/FluentEmojiFont.ttf", + "MIT" + ), + new BuiltInEmojiPack( + "Blobmoji", + "blobmoji", + "https://github.com/C1710/blobmoji/releases/latest/download/BlobmojiCompat.ttf", + "Apache 2.0" + ) + )); + private static final Runnable invalidateUiRunnable = () -> NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.emojiLoaded); private static final String[] previewEmojis = { "\uD83D\uDE00", @@ -473,6 +508,74 @@ public class EmojiHelper { void onUndo(); } + public interface DownloadCallback { + void onProgress(int percent); + void onDone(EmojiPack pack); + void onError(Exception e); + } + + /** + * Download a built-in emoji pack from its URL, then install it. + * Calls back on the UI thread. + */ + public void downloadBuiltInPack(BuiltInEmojiPack builtIn, DownloadCallback callback) { + AtomicBoolean cancelled = new AtomicBoolean(false); + new Thread(() -> { + File tmpFile = null; + try { + URL url = new URL(builtIn.url); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setConnectTimeout(15000); + conn.setReadTimeout(60000); + conn.connect(); + int fileSize = conn.getContentLength(); + tmpFile = File.createTempFile("emoji_dl_", ".ttf", AndroidUtilities.getCacheDir()); + try (InputStream in = conn.getInputStream(); + FileOutputStream out = new FileOutputStream(tmpFile)) { + byte[] buf = new byte[8 * 1024]; + int read; + long total = 0; + int lastPercent = -1; + while ((read = in.read(buf)) != -1) { + if (cancelled.get()) return; + out.write(buf, 0, read); + total += read; + if (fileSize > 0) { + int percent = (int) (total * 100 / fileSize); + if (percent != lastPercent) { + lastPercent = percent; + final int p = percent; + AndroidUtilities.runOnUIThread(() -> callback.onProgress(p)); + } + } + } + } + File finalTmpFile = tmpFile; + EmojiPack pack = installEmoji(finalTmpFile, false); + AndroidUtilities.runOnUIThread(() -> callback.onDone(pack)); + } catch (Exception e) { + FileLog.e("Emoji download failed", e); + AndroidUtilities.runOnUIThread(() -> callback.onError(e)); + } finally { + if (tmpFile != null) tmpFile.delete(); + } + }).start(); + } + + public static class BuiltInEmojiPack { + public final String name; + public final String id; + public final String url; + public final String license; + + public BuiltInEmojiPack(String name, String id, String url, String license) { + this.name = name; + this.id = id; + this.url = url; + this.license = license; + } + } + public static class EmojiPack { protected String packName; protected String packId; diff --git a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/helpers/FontHelper.java b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/helpers/FontHelper.java new file mode 100644 index 00000000..7601294d --- /dev/null +++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/helpers/FontHelper.java @@ -0,0 +1,443 @@ +package tw.nekomimi.nekogram.helpers; + +import android.app.Activity; +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.Typeface; +import android.os.Build; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; + +import androidx.core.view.LayoutInflaterCompat; + +import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.ApplicationLoader; +import org.telegram.messenger.FileLog; +import org.telegram.messenger.SharedConfig; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import tw.nekomimi.nekogram.NekoConfig; + +/** + * Manages the custom font selection feature. + * + * Fonts are bundled as variable TTF files in assets/fonts/custom/. + * Regular-weight fonts are applied globally via LayoutInflater.Factory2. + * Bold/medium fonts are applied via {@link AndroidUtilities#bold()}. + */ +public class FontHelper { + + public static final String FONT_ID_DEFAULT = "default"; + public static final String FONT_ID_SYSTEM = "system"; + + public static final List FONTS = Collections.unmodifiableList(Arrays.asList( + new FontItem(FONT_ID_DEFAULT, "Roboto", null, "Default"), + new FontItem(FONT_ID_SYSTEM, "System font", null, "System"), + new FontItem("opensans", "Open Sans", "fonts/custom/opensans.ttf", "Apache 2.0"), + new FontItem("arimo", "Arimo", "fonts/custom/arimo.ttf", "Apache 2.0"), + new FontItem("notosans", "Noto Sans", "fonts/custom/notosans.ttf", "OFL"), + new FontItem("raleway", "Raleway", "fonts/custom/raleway.ttf", "OFL"), + new FontItem("nunito", "Nunito", "fonts/custom/nunito.ttf", "OFL") + )); + + private static final String PREF_NAME = "nekofonts"; + private static final String PREF_KEY = "selected_font"; + + private static String selectedFontId; + // Cached regular typeface for the currently active font + private static Typeface cachedRegular; + private static Typeface cachedMedium; + + // ------------------------------------------------------------------ init + + public static void init() { + SharedPreferences prefs = ApplicationLoader.applicationContext + .getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + if (prefs.contains(PREF_KEY)) { + selectedFontId = prefs.getString(PREF_KEY, FONT_ID_DEFAULT); + } else if (NekoConfig.useSystemFont) { + selectedFontId = FONT_ID_SYSTEM; + saveFont(FONT_ID_SYSTEM); + } else { + selectedFontId = FONT_ID_DEFAULT; + } + rebuildCache(); + } + + // ------------------------------------------------------------------ Activity hook + + /** + * Install a LayoutInflater.Factory2 on the given Activity so every TextView + * created from XML gets the currently selected regular typeface applied. + * Call this from Activity.onCreate() BEFORE super.onCreate(). + */ + public static void installFactory(Activity activity) { + LayoutInflater inflater = activity.getLayoutInflater(); + // Only install if no factory is set yet (AppCompat may set one too) + if (inflater.getFactory2() == null) { + LayoutInflaterCompat.setFactory2(inflater, new FontInflaterFactory(null)); + } + } + + /** Wrap an existing factory so we chain calls correctly. */ + public static void installFactory(Activity activity, LayoutInflater.Factory2 delegate) { + LayoutInflater inflater = activity.getLayoutInflater(); + LayoutInflaterCompat.setFactory2(inflater, new FontInflaterFactory(delegate)); + } + + private static class FontInflaterFactory implements LayoutInflater.Factory2 { + private final LayoutInflater.Factory2 delegate; + FontInflaterFactory(LayoutInflater.Factory2 delegate) { this.delegate = delegate; } + + @Override + public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { + View view = delegate != null ? delegate.onCreateView(parent, name, context, attrs) : null; + if (view == null) { + // Let the system create it + try { + if (name.contains(".")) { + view = LayoutInflater.from(context).createView(name, null, attrs); + } + } catch (Exception ignored) {} + } + applyToView(view); + return view; + } + + @Override + public View onCreateView(String name, Context context, AttributeSet attrs) { + return onCreateView(null, name, context, attrs); + } + } + + /** + * Apply the selected regular typeface to a single view. + * Safe to call with null. + */ + public static void applyToView(View view) { + if (view == null) return; + Typeface regular = cachedRegular; + if (regular == null) return; // default Roboto — nothing to do + if (view instanceof TextView) { + TextView tv = (TextView) view; + Typeface current = tv.getTypeface(); + if (current != null && current.isBold()) { + // Keep bold style but switch the face + view.post(() -> { + Typeface bold = cachedMedium; + if (bold != null) tv.setTypeface(bold); + }); + } else { + tv.setTypeface(regular); + } + } + } + + // ------------------------------------------------------------------ API + + public static String getSelectedFontId() { + if (selectedFontId == null) init(); + return selectedFontId; + } + + public static FontItem getSelectedFont() { + String id = getSelectedFontId(); + for (FontItem f : FONTS) { + if (f.id.equals(id)) return f; + } + return FONTS.get(0); + } + + /** Check whether a custom font file (user-installed) exists for the given ID. */ + public static boolean isUserFont(String fontId) { + File f = getUserFontFile(fontId); + return f != null && f.exists(); + } + + public static File getUserFontFile(String fontId) { + if (fontId == null || FONT_ID_DEFAULT.equals(fontId) || FONT_ID_SYSTEM.equals(fontId)) return null; + File dir = new File(ApplicationLoader.applicationContext.getFilesDir(), "user_fonts"); + return new File(dir, fontId + ".ttf"); + } + + public static void setFont(String fontId) { + selectedFontId = fontId; + saveFont(fontId); + rebuildCache(); + } + + private static void saveFont(String fontId) { + ApplicationLoader.applicationContext + .getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + .edit().putString(PREF_KEY, fontId).apply(); + // keep NekoConfig.useSystemFont in sync + boolean isSystem = FONT_ID_SYSTEM.equals(fontId); + if (NekoConfig.useSystemFont != isSystem) { + NekoConfig.useSystemFont = isSystem; + ApplicationLoader.applicationContext + .getSharedPreferences("nekoconfig", Context.MODE_PRIVATE) + .edit().putBoolean("useSystemFont", isSystem).apply(); + } + } + + /** Install a user-provided TTF file and activate it. Returns the new FontItem. */ + public static FontItem installUserFont(File src, String name) throws Exception { + String id = "user_" + name.replaceAll("[^a-zA-Z0-9]", "_").toLowerCase(); + File dir = new File(ApplicationLoader.applicationContext.getFilesDir(), "user_fonts"); + if (!dir.exists()) dir.mkdirs(); + File dest = new File(dir, id + ".ttf"); + AndroidUtilities.copyFile(src, dest); + // Validate it can be loaded + Typeface.createFromFile(dest); + return new FontItem(id, name, null, "User font") { + { userFile = dest; } + }; + } + + // ------------------------------------------------------------------ Google Fonts download + + public interface DownloadFontCallback { + void onProgress(int percent); + void onDone(FontItem font); + void onError(Exception e); + } + + /** + * Download a font by family name from Google Fonts CSS API v2. + * The TTF is saved to the user_fonts directory. + */ + public static void downloadFromGoogleFonts(String familyName, DownloadFontCallback callback) { + AtomicBoolean cancelled = new AtomicBoolean(false); + new Thread(() -> { + try { + // 1. Resolve download URL via Google Fonts CSS API + String cssUrl = "https://fonts.googleapis.com/css2?family=" + + familyName.replace(" ", "+") + ":wght@400;700&display=swap"; + // Use a desktop User-Agent to get TTF (not woff2) + URL url = new URL(cssUrl); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestProperty("User-Agent", "Mozilla/5.0"); + conn.setConnectTimeout(10000); + conn.setReadTimeout(10000); + conn.connect(); + String css; + try (InputStream in = conn.getInputStream()) { + byte[] buf = new byte[65536]; int n; + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + while ((n = in.read(buf)) != -1) baos.write(buf, 0, n); + css = baos.toString("UTF-8"); + } + // Extract first TTF or truetype url from CSS + String ttfUrl = extractFontUrl(css); + if (ttfUrl == null) throw new Exception("No TTF URL found in Google Fonts CSS"); + + // 2. Download the TTF + URL ttfConn = new URL(ttfUrl); + HttpURLConnection dl = (HttpURLConnection) ttfConn.openConnection(); + dl.setConnectTimeout(15000); + dl.setReadTimeout(60000); + dl.connect(); + int total = dl.getContentLength(); + + File dir = new File(ApplicationLoader.applicationContext.getFilesDir(), "user_fonts"); + if (!dir.exists()) dir.mkdirs(); + String safeId = "gf_" + familyName.replace(" ", "_").toLowerCase(); + File dest = new File(dir, safeId + ".ttf"); + + try (InputStream in = dl.getInputStream(); + FileOutputStream out = new FileOutputStream(dest)) { + byte[] buf = new byte[8192]; + int read; + long downloaded = 0; + int lastPercent = -1; + while ((read = in.read(buf)) != -1) { + if (cancelled.get()) return; + out.write(buf, 0, read); + downloaded += read; + if (total > 0) { + int pct = (int) (downloaded * 100 / total); + if (pct != lastPercent) { + lastPercent = pct; + int finalPct = pct; + AndroidUtilities.runOnUIThread(() -> callback.onProgress(finalPct)); + } + } + } + } + // Validate + Typeface.createFromFile(dest); + FontItem item = new FontItem(safeId, familyName, null, "Google Fonts") { + { userFile = dest; } + }; + AndroidUtilities.runOnUIThread(() -> callback.onDone(item)); + } catch (Exception e) { + FileLog.e("FontHelper: Google Fonts download failed", e); + AndroidUtilities.runOnUIThread(() -> callback.onError(e)); + } + }).start(); + } + + private static String extractFontUrl(String css) { + // Look for src: url(...) lines containing truetype or .ttf + for (String line : css.split("\n")) { + String trimmed = line.trim(); + if (trimmed.startsWith("src:") || trimmed.contains("url(")) { + // Extract URL between url( and ) + int start = trimmed.indexOf("url("); + int end = trimmed.indexOf(")", start); + if (start >= 0 && end > start) { + String raw = trimmed.substring(start + 4, end).replace("'", "").replace("\"", ""); + if (raw.contains(".ttf") || raw.contains("truetype")) { + return raw; + } + } + } + } + return null; + } + + // ------------------------------------------------------------------ typeface building + + private static void rebuildCache() { + String id = getSelectedFontId(); + + // Clear AndroidUtilities caches + AndroidUtilities.mediumTypeface = null; + try { + java.lang.reflect.Field f = AndroidUtilities.class.getDeclaredField("typefaceCache"); + f.setAccessible(true); + java.util.Hashtable cache = (java.util.Hashtable) f.get(null); + if (cache != null) cache.clear(); + } catch (Exception ignored) {} + + boolean isSystem = FONT_ID_SYSTEM.equals(id); + SharedConfig.useSystemBoldFont = isSystem; + + cachedRegular = buildRegular(id); + cachedMedium = buildMedium(id); + } + + private static Typeface buildRegular(String id) { + if (FONT_ID_DEFAULT.equals(id)) return null; + if (FONT_ID_SYSTEM.equals(id)) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) return Typeface.create((Typeface) null, 400, false); + return Typeface.create("sans-serif", Typeface.NORMAL); + } + try { + File userFile = getUserFontFile(id); + if (userFile != null && userFile.exists()) return Typeface.createFromFile(userFile); + // Check built-in asset + FontItem item = findBuiltIn(id); + if (item != null && item.assetPath != null) { + return Typeface.createFromAsset(ApplicationLoader.applicationContext.getAssets(), item.assetPath); + } + // Could be a user font with a different file path stored in FontItem.userFile + for (FontItem f : getUserFonts()) { + if (f.id.equals(id) && f.userFile != null) return Typeface.createFromFile(f.userFile); + } + } catch (Exception e) { + FileLog.e("FontHelper: buildRegular failed for " + id, e); + } + return null; + } + + private static Typeface buildMedium(String id) { + if (FONT_ID_DEFAULT.equals(id)) return null; + if (FONT_ID_SYSTEM.equals(id)) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) return Typeface.create((Typeface) null, 500, false); + return Typeface.create("sans-serif-medium", Typeface.NORMAL); + } + try { + Typeface base = buildRegular(id); + if (base == null) return null; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + File userFile = getUserFontFile(id); + if (userFile != null && userFile.exists()) { + return new Typeface.Builder(userFile).setWeight(700).build(); + } + FontItem item = findBuiltIn(id); + if (item != null && item.assetPath != null) { + return new Typeface.Builder( + ApplicationLoader.applicationContext.getAssets(), item.assetPath) + .setWeight(700).build(); + } + } + return Typeface.create(base, Typeface.BOLD); + } catch (Exception e) { + FileLog.e("FontHelper: buildMedium failed for " + id, e); + } + return null; + } + + private static FontItem findBuiltIn(String id) { + for (FontItem f : FONTS) { + if (f.id.equals(id)) return f; + } + return null; + } + + /** Returns any user-installed fonts found in the user_fonts directory. */ + public static List getUserFonts() { + java.util.List list = new java.util.ArrayList<>(); + File dir = new File(ApplicationLoader.applicationContext.getFilesDir(), "user_fonts"); + if (!dir.exists()) return list; + File[] files = dir.listFiles(); + if (files == null) return list; + for (File f : files) { + if (f.getName().endsWith(".ttf")) { + String fileId = f.getName().replace(".ttf", ""); + String displayName = fileId.replace("gf_", "").replace("user_", "").replace("_", " "); + // Capitalize first letter of each word + String[] words = displayName.split(" "); + StringBuilder sb = new StringBuilder(); + for (String w : words) { + if (!w.isEmpty()) { + sb.append(Character.toUpperCase(w.charAt(0))).append(w.substring(1)).append(" "); + } + } + FontItem item = new FontItem(fileId, sb.toString().trim(), null, + fileId.startsWith("gf_") ? "Google Fonts" : "User font") { + { userFile = f; } + }; + list.add(item); + } + } + return list; + } + + // ------------------------------------------------------------------ public typeface getters + + public static Typeface getRegularTypeface() { return cachedRegular; } + public static Typeface getMediumTypeface() { return cachedMedium; } + + // ------------------------------------------------------------------ model + + public static class FontItem { + public final String id; + public final String name; + /** Asset path for built-in fonts, or null for user/system fonts. */ + public final String assetPath; + public final String license; + /** File for user-installed fonts. */ + public File userFile; + + public FontItem(String id, String name, String assetPath, String license) { + this.id = id; + this.name = name; + this.assetPath = assetPath; + this.license = license; + } + } +} diff --git a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/EmojiSetCell.java b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/EmojiSetCell.java index ea078855..b659fbd1 100644 --- a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/EmojiSetCell.java +++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/EmojiSetCell.java @@ -27,6 +27,7 @@ import org.telegram.ui.Components.BackupImageView; import org.telegram.ui.Components.CheckBox2; import org.telegram.ui.Components.CubicBezierInterpolator; import org.telegram.ui.Components.LayoutHelper; +import org.telegram.ui.Components.RadialProgressView; import tw.nekomimi.nekogram.NekoConfig; import tw.nekomimi.nekogram.helpers.EmojiHelper; @@ -40,10 +41,22 @@ public class EmojiSetCell extends FrameLayout { private ImageView optionsButton; private CheckBox2 checkBox; + // Download button (shown for built-in packs that are not yet installed) + private TextView downloadButton; + // Progress shown while downloading + private RadialProgressView progressView; + private EmojiHelper.EmojiPack pack; + private EmojiHelper.BuiltInEmojiPack builtInPack; private boolean needDivider; private final boolean selection; + // Download state + public static final int STATE_INSTALLED = 0; + public static final int STATE_NOT_INSTALLED = 1; + public static final int STATE_DOWNLOADING = 2; + private int downloadState = STATE_INSTALLED; + public EmojiSetCell(Context context, boolean selection, Theme.ResourcesProvider resourcesProvider) { super(context); this.selection = selection; @@ -98,11 +111,35 @@ public class EmojiSetCell extends FrameLayout { checkBox.setDrawUnchecked(false); checkBox.setDrawBackgroundAsArc(3); addView(checkBox, LayoutHelper.createFrameRelatively(24, 24, Gravity.START, 34, 30, 0, 0)); + + // Download button — shown for built-in packs not yet installed + downloadButton = new TextView(context); + downloadButton.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13); + downloadButton.setTypeface(AndroidUtilities.getTypeface(AndroidUtilities.TYPEFACE_ROBOTO_MEDIUM)); + downloadButton.setTextColor(Theme.getColor(Theme.key_featuredStickers_addButton, resourcesProvider)); + downloadButton.setBackground(Theme.createSimpleSelectorRoundRectDrawable(AndroidUtilities.dp(4), + 0x1A000000 & Theme.getColor(Theme.key_featuredStickers_addButton, resourcesProvider), + Theme.multAlpha(Theme.getColor(Theme.key_featuredStickers_addButton, resourcesProvider), .15f))); + downloadButton.setPadding(AndroidUtilities.dp(8), AndroidUtilities.dp(4), AndroidUtilities.dp(8), AndroidUtilities.dp(4)); + downloadButton.setGravity(Gravity.CENTER); + downloadButton.setVisibility(GONE); + addView(downloadButton, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, + (LocaleController.isRTL ? Gravity.LEFT : Gravity.RIGHT) | Gravity.CENTER_VERTICAL, + LocaleController.isRTL ? 10 : 0, 0, LocaleController.isRTL ? 0 : 10, 0)); + + // Progress spinner shown while downloading + progressView = new RadialProgressView(context, resourcesProvider); + progressView.setSize(AndroidUtilities.dp(20)); + progressView.setVisibility(GONE); + addView(progressView, LayoutHelper.createFrame(28, 28, + (LocaleController.isRTL ? Gravity.LEFT : Gravity.RIGHT) | Gravity.CENTER_VERTICAL, + LocaleController.isRTL ? 13 : 0, 0, LocaleController.isRTL ? 0 : 13, 0)); } } public void setData(EmojiHelper.EmojiPack emojiPackInfo, boolean animated, boolean divider) { needDivider = divider; + builtInPack = null; if (selection) { textView.setText(emojiPackInfo.getPackName()); pack = emojiPackInfo; @@ -112,6 +149,7 @@ public class EmojiSetCell extends FrameLayout { valueTextView.setText(LocaleController.getString(R.string.InstalledEmojiSet), animated); } setPackPreview(pack); + setDownloadState(STATE_INSTALLED, animated); } else { textView.setText(LocaleController.getString(R.string.EmojiSets)); if (NekoConfig.useSystemEmoji) { @@ -128,6 +166,54 @@ public class EmojiSetCell extends FrameLayout { setWillNotDraw(!divider); } + /** + * Bind a built-in (downloadable) pack that is not yet installed. + */ + public void setBuiltInData(EmojiHelper.BuiltInEmojiPack info, int state, boolean animated, boolean divider) { + needDivider = divider; + pack = null; + builtInPack = info; + textView.setText(info.name); + valueTextView.setText(info.license, animated); + imageView.setImageDrawable(null); + setDownloadState(state, animated); + setWillNotDraw(!divider); + } + + public void setDownloadState(int state, boolean animated) { + this.downloadState = state; + if (downloadButton == null || progressView == null) return; + switch (state) { + case STATE_NOT_INSTALLED: + downloadButton.setText(LocaleController.getString(R.string.EmojiPackDownload)); + downloadButton.setVisibility(VISIBLE); + progressView.setVisibility(GONE); + break; + case STATE_DOWNLOADING: + downloadButton.setVisibility(GONE); + progressView.setVisibility(VISIBLE); + break; + case STATE_INSTALLED: + default: + downloadButton.setVisibility(GONE); + progressView.setVisibility(GONE); + break; + } + } + + public void setDownloadProgress(int percent) { + // RadialProgressView doesn't support determinate progress in all versions; + // just keep it spinning — good enough UX for now. + } + + public TextView getDownloadButton() { + return downloadButton; + } + + public EmojiHelper.BuiltInEmojiPack getBuiltInPack() { + return builtInPack; + } + private void setPackPreview(EmojiHelper.EmojiPack pack) { if ("default".equals(pack.getPackId())) { imageView.setImageDrawable(getContext().getDrawable(R.drawable.apple)); @@ -137,6 +223,7 @@ public class EmojiSetCell extends FrameLayout { } public void setChecked(boolean checked, boolean animated) { + if (optionsButton == null) return; if (animated) { optionsButton.animate().cancel(); optionsButton.animate().setListener(new AnimatorListenerAdapter() { @@ -163,13 +250,18 @@ public class EmojiSetCell extends FrameLayout { } public void setSelected(boolean selected, boolean animated) { - checkBox.setChecked(selected, animated); + if (checkBox != null) checkBox.setChecked(selected, animated); } public boolean isChecked() { + if (optionsButton == null) return false; return optionsButton.getVisibility() == VISIBLE; } + public int getDownloadState() { + return downloadState; + } + @Override protected void onDraw(@NonNull Canvas canvas) { if (needDivider) { @@ -185,4 +277,4 @@ public class EmojiSetCell extends FrameLayout { public EmojiHelper.EmojiPack getPack() { return pack; } -} \ No newline at end of file +} diff --git a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/FoxCloudActivity.java b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/FoxCloudActivity.java new file mode 100644 index 00000000..bb161704 --- /dev/null +++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/FoxCloudActivity.java @@ -0,0 +1,152 @@ +package tw.nekomimi.nekogram.settings; + +import android.content.Context; +import android.os.Bundle; +import android.view.Gravity; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.TextView; + +import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.ApplicationLoader; +import org.telegram.messenger.LocaleController; +import org.telegram.messenger.MessagesController; +import org.telegram.messenger.R; +import org.telegram.messenger.SendMessagesHelper; +import org.telegram.messenger.UserConfig; +import org.telegram.ui.ActionBar.Theme; +import org.telegram.ui.ChatActivity; +import org.telegram.ui.Components.AnimatedFileDrawable; +import org.telegram.ui.Components.BackupImageView; +import org.telegram.ui.Components.LayoutHelper; +import org.telegram.ui.Components.UItem; +import org.telegram.ui.Components.UniversalAdapter; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.util.ArrayList; + +import tw.nekomimi.nekogram.helpers.ShimmerHeartDrawable; + +public class FoxCloudActivity extends BaseNekoSettingsActivity { + + private final int serversRow = rowId++; + private final int sponsorRow = rowId++; + private final int iconsRow = rowId++; + private final int buttonRow = rowId++; + + private FrameLayout topView; + private BackupImageView stickerView; + private AnimatedFileDrawable stickerDrawable; + + @Override + public View createView(Context context) { + topView = new FrameLayout(context); + + stickerView = new BackupImageView(context); + topView.addView(stickerView, LayoutHelper.createFrame(130, 130, Gravity.CENTER_HORIZONTAL | Gravity.TOP, 0, 16, 0, 0)); + + var titleView = new TextView(context); + titleView.setTextSize(android.util.TypedValue.COMPLEX_UNIT_DIP, 22); + titleView.setTypeface(AndroidUtilities.bold()); + titleView.setGravity(Gravity.CENTER); + titleView.setText(LocaleController.getString(R.string.FoxCloudTitle)); + titleView.setTextColor(0xFF4A90D9); + topView.addView(titleView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL | Gravity.TOP, 16, 158, 16, 0)); + + var subtitleView = new TextView(context); + subtitleView.setTextSize(android.util.TypedValue.COMPLEX_UNIT_DIP, 14); + subtitleView.setGravity(Gravity.CENTER); + subtitleView.setText(LocaleController.getString(R.string.FoxCloudSubtitle)); + subtitleView.setTextColor(getThemedColor(Theme.key_windowBackgroundWhiteGrayText)); + topView.addView(subtitleView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL | Gravity.TOP, 24, 192, 24, 0)); + + var fragmentView = super.createView(context); + loadSticker(); + return fragmentView; + } + + private void loadSticker() { + try { + File cacheFile = new File(AndroidUtilities.getCacheDir(), "foxi_cloud.webm"); + if (!cacheFile.exists() || cacheFile.length() == 0) { + try (InputStream in = ApplicationLoader.applicationContext.getResources().openRawResource(R.raw.foxi_cloud); + FileOutputStream out = new FileOutputStream(cacheFile)) { + byte[] buffer = new byte[16 * 1024]; + int len; + while ((len = in.read(buffer)) > 0) { + out.write(buffer, 0, len); + } + } + } + stickerDrawable = new AnimatedFileDrawable(cacheFile, true, 0, 0, null, null, null, 0, UserConfig.selectedAccount, true, 260, 260, null); + stickerView.setImageDrawable(stickerDrawable); + stickerView.getImageReceiver().setAutoRepeat(1); + stickerDrawable.start(); + stickerView.setOnClickListener(v -> { + if (stickerDrawable != null) { + stickerDrawable.start(); + } + }); + } catch (Throwable ignore) { + } + } + + @Override + protected boolean needActionBarPadding() { + return false; + } + + @Override + protected void fillItems(ArrayList items, UniversalAdapter adapter) { + items.add(UItem.asCustomShadow(topView, 240)); + items.add(UItem.asButtonSubtext(serversRow, R.drawable.msg2_devices, LocaleController.getString(R.string.FoxPremiumFeatureServersTitle), LocaleController.getString(R.string.FoxPremiumFeatureServersAbout))); + UItem sponsorItem = UItem.asButton(sponsorRow, new ShimmerHeartDrawable(AndroidUtilities.dp(24)), LocaleController.getString(R.string.FoxPremiumFeatureSponsorTitle)); + sponsorItem.subtext = LocaleController.getString(R.string.FoxPremiumFeatureSponsorAbout); + items.add(sponsorItem); + items.add(UItem.asButtonSubtext(iconsRow, R.drawable.msg_emoji_smiles, LocaleController.getString(R.string.FoxCloudFeatureIconsTitle), LocaleController.getString(R.string.FoxCloudFeatureIconsAbout))); + items.add(UItem.asShadow(null)); + items.add(UItem.asButton(buttonRow, R.drawable.msg_input_like, LocaleController.getString(R.string.FoxPremiumButton))); + items.add(UItem.asShadow(null)); + } + + @Override + protected void onItemClick(UItem item, View view, int position, float x, float y) { + if (item.id == buttonRow) { + openBotWithCommand("vpnghostbot", "/quickreg"); + } + } + + private void openBotWithCommand(String username, String command) { + int account = UserConfig.selectedAccount; + MessagesController mc = MessagesController.getInstance(account); + mc.getUserNameResolver().resolve(username, peerId -> { + if (peerId == null || peerId == 0) { + return; + } + AndroidUtilities.runOnUIThread(() -> { + Bundle args = new Bundle(); + args.putLong("user_id", peerId); + presentFragment(new ChatActivity(args)); + AndroidUtilities.runOnUIThread(() -> + SendMessagesHelper.getInstance(account).sendMessage( + SendMessagesHelper.SendMessageParams.of(command, peerId)), 150); + }); + }); + } + + @Override + protected String getActionBarTitle() { + return LocaleController.getString(R.string.FoxCloudTitle); + } + + @Override + public void onFragmentDestroy() { + super.onFragmentDestroy(); + if (stickerDrawable != null) { + stickerDrawable.recycle(); + stickerDrawable = null; + } + } +} diff --git a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/FoxSpaceActivity.java b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/FoxSpaceActivity.java new file mode 100644 index 00000000..a2372c42 --- /dev/null +++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/FoxSpaceActivity.java @@ -0,0 +1,152 @@ +package tw.nekomimi.nekogram.settings; + +import android.content.Context; +import android.os.Bundle; +import android.view.Gravity; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.TextView; + +import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.ApplicationLoader; +import org.telegram.messenger.LocaleController; +import org.telegram.messenger.MessagesController; +import org.telegram.messenger.R; +import org.telegram.messenger.SendMessagesHelper; +import org.telegram.messenger.UserConfig; +import org.telegram.ui.ActionBar.Theme; +import org.telegram.ui.ChatActivity; +import org.telegram.ui.Components.AnimatedFileDrawable; +import org.telegram.ui.Components.BackupImageView; +import org.telegram.ui.Components.LayoutHelper; +import org.telegram.ui.Components.UItem; +import org.telegram.ui.Components.UniversalAdapter; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.util.ArrayList; + +import tw.nekomimi.nekogram.helpers.ShimmerHeartDrawable; + +public class FoxSpaceActivity extends BaseNekoSettingsActivity { + + private final int serversRow = rowId++; + private final int sponsorRow = rowId++; + private final int iconsRow = rowId++; + private final int buttonRow = rowId++; + + private FrameLayout topView; + private BackupImageView stickerView; + private AnimatedFileDrawable stickerDrawable; + + @Override + public View createView(Context context) { + topView = new FrameLayout(context); + + stickerView = new BackupImageView(context); + topView.addView(stickerView, LayoutHelper.createFrame(130, 130, Gravity.CENTER_HORIZONTAL | Gravity.TOP, 0, 16, 0, 0)); + + var titleView = new TextView(context); + titleView.setTextSize(android.util.TypedValue.COMPLEX_UNIT_DIP, 22); + titleView.setTypeface(AndroidUtilities.bold()); + titleView.setGravity(Gravity.CENTER); + titleView.setText(LocaleController.getString(R.string.FoxSpaceTitle)); + titleView.setTextColor(0xFF6A5ACD); + topView.addView(titleView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL | Gravity.TOP, 16, 158, 16, 0)); + + var subtitleView = new TextView(context); + subtitleView.setTextSize(android.util.TypedValue.COMPLEX_UNIT_DIP, 14); + subtitleView.setGravity(Gravity.CENTER); + subtitleView.setText(LocaleController.getString(R.string.FoxSpaceSubtitle)); + subtitleView.setTextColor(getThemedColor(Theme.key_windowBackgroundWhiteGrayText)); + topView.addView(subtitleView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL | Gravity.TOP, 24, 192, 24, 0)); + + var fragmentView = super.createView(context); + loadSticker(); + return fragmentView; + } + + private void loadSticker() { + try { + File cacheFile = new File(AndroidUtilities.getCacheDir(), "foxi_space.webm"); + if (!cacheFile.exists() || cacheFile.length() == 0) { + try (InputStream in = ApplicationLoader.applicationContext.getResources().openRawResource(R.raw.foxi_space); + FileOutputStream out = new FileOutputStream(cacheFile)) { + byte[] buffer = new byte[16 * 1024]; + int len; + while ((len = in.read(buffer)) > 0) { + out.write(buffer, 0, len); + } + } + } + stickerDrawable = new AnimatedFileDrawable(cacheFile, true, 0, 0, null, null, null, 0, UserConfig.selectedAccount, true, 260, 260, null); + stickerView.setImageDrawable(stickerDrawable); + stickerView.getImageReceiver().setAutoRepeat(1); + stickerDrawable.start(); + stickerView.setOnClickListener(v -> { + if (stickerDrawable != null) { + stickerDrawable.start(); + } + }); + } catch (Throwable ignore) { + } + } + + @Override + protected boolean needActionBarPadding() { + return false; + } + + @Override + protected void fillItems(ArrayList items, UniversalAdapter adapter) { + items.add(UItem.asCustomShadow(topView, 240)); + items.add(UItem.asButtonSubtext(serversRow, R.drawable.msg2_devices, LocaleController.getString(R.string.FoxPremiumFeatureServersTitle), LocaleController.getString(R.string.FoxPremiumFeatureServersAbout))); + UItem sponsorItem = UItem.asButton(sponsorRow, new ShimmerHeartDrawable(AndroidUtilities.dp(24)), LocaleController.getString(R.string.FoxPremiumFeatureSponsorTitle)); + sponsorItem.subtext = LocaleController.getString(R.string.FoxPremiumFeatureSponsorAbout); + items.add(sponsorItem); + items.add(UItem.asButtonSubtext(iconsRow, R.drawable.msg_emoji_smiles, LocaleController.getString(R.string.FoxSpaceFeatureIconsTitle), LocaleController.getString(R.string.FoxSpaceFeatureIconsAbout))); + items.add(UItem.asShadow(null)); + items.add(UItem.asButton(buttonRow, R.drawable.msg_input_like, LocaleController.getString(R.string.FoxPremiumButton))); + items.add(UItem.asShadow(null)); + } + + @Override + protected void onItemClick(UItem item, View view, int position, float x, float y) { + if (item.id == buttonRow) { + openBotWithCommand("vpnghostbot", "/quickreg"); + } + } + + private void openBotWithCommand(String username, String command) { + int account = UserConfig.selectedAccount; + MessagesController mc = MessagesController.getInstance(account); + mc.getUserNameResolver().resolve(username, peerId -> { + if (peerId == null || peerId == 0) { + return; + } + AndroidUtilities.runOnUIThread(() -> { + Bundle args = new Bundle(); + args.putLong("user_id", peerId); + presentFragment(new ChatActivity(args)); + AndroidUtilities.runOnUIThread(() -> + SendMessagesHelper.getInstance(account).sendMessage( + SendMessagesHelper.SendMessageParams.of(command, peerId)), 150); + }); + }); + } + + @Override + protected String getActionBarTitle() { + return LocaleController.getString(R.string.FoxSpaceTitle); + } + + @Override + public void onFragmentDestroy() { + super.onFragmentDestroy(); + if (stickerDrawable != null) { + stickerDrawable.recycle(); + stickerDrawable = null; + } + } +} diff --git a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/NekoAppearanceSettingsActivity.java b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/NekoAppearanceSettingsActivity.java index 8b381ae7..1fe8a5a0 100644 --- a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/NekoAppearanceSettingsActivity.java +++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/NekoAppearanceSettingsActivity.java @@ -19,6 +19,7 @@ import java.util.ArrayList; import tw.nekomimi.nekogram.NekoConfig; import tw.nekomimi.nekogram.helpers.EmojiHelper; +import tw.nekomimi.nekogram.helpers.FontHelper; import tw.nekomimi.nekogram.helpers.PopupHelper; public class NekoAppearanceSettingsActivity extends BaseNekoSettingsActivity implements NotificationCenter.NotificationCenterDelegate { @@ -39,6 +40,8 @@ public class NekoAppearanceSettingsActivity extends BaseNekoSettingsActivity imp private final int tabsPositionRow = rowId++; private final int strokeOnViewsRow = rowId++; + private final int useNewLayoutRow = rowId++; + private final int fontSettingsRow = rowId++; @Override public boolean onFragmentCreate() { @@ -96,6 +99,11 @@ public class NekoAppearanceSettingsActivity extends BaseNekoSettingsActivity imp items.add(UItem.asCheck(strokeOnViewsRow, LocaleController.getString(R.string.StrokeOnViews)).setChecked(NekoConfig.strokeOnViews).slug("strokeOnViews")); items.add(UItem.asShadow(null)); + items.add(UItem.asHeader(LocaleController.getString(R.string.ChangeChannelNameColor2))); + items.add(UItem.asCheck(useNewLayoutRow, LocaleController.getString(R.string.UseNewLayout)).setChecked(NekoConfig.useNewLayout).slug("useNewLayout")); + items.add(TextSettingsCellFactory.of(fontSettingsRow, LocaleController.getString(R.string.FontSettings), FontHelper.getSelectedFont().name).slug("fontSettings")); + items.add(UItem.asShadow(LocaleController.getString(R.string.UseNewLayoutHint))); + } @Override @@ -199,6 +207,14 @@ public class NekoAppearanceSettingsActivity extends BaseNekoSettingsActivity imp if (view instanceof TextCheckCell) { ((TextCheckCell) view).setChecked(NekoConfig.strokeOnViews); } + } else if (id == useNewLayoutRow) { + NekoConfig.toggleUseNewLayout(); + if (view instanceof TextCheckCell) { + ((TextCheckCell) view).setChecked(NekoConfig.useNewLayout); + } + parentLayout.rebuildAllFragmentViews(false, false); + } else if (id == fontSettingsRow) { + presentFragment(new NekoFontSettingsActivity()); } } diff --git a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/NekoDonateActivity.java b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/NekoDonateActivity.java index 5472ca3c..423c5b45 100644 --- a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/NekoDonateActivity.java +++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/NekoDonateActivity.java @@ -50,6 +50,8 @@ public class NekoDonateActivity extends BaseNekoSettingsActivity { private final int supportProjectRow = 1; private final int premiumRow = 3; + private final int spaceRow = 4; + private final int cloudRow = 5; private final int topSponsorsRow = 2; private final int cryptoRow = 200; @@ -68,6 +70,16 @@ public class NekoDonateActivity extends BaseNekoSettingsActivity { UItem premiumItem = UItem.asButton(premiumRow, new ShimmerHeartDrawable(AndroidUtilities.dp(24)), premiumTitle); premiumItem.subtext = LocaleController.getString(R.string.FoxPremiumSubtitle); items.add(premiumItem); + SpannableString spaceTitle = new SpannableString(LocaleController.getString(R.string.FoxSpaceTitle)); + spaceTitle.setSpan(new ForegroundColorSpan(0xFF6A5ACD), 0, spaceTitle.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + UItem spaceItem = UItem.asButton(spaceRow, new ShimmerHeartDrawable(AndroidUtilities.dp(24)), spaceTitle); + spaceItem.subtext = LocaleController.getString(R.string.FoxSpaceSubtitle); + items.add(spaceItem); + SpannableString cloudTitle = new SpannableString(LocaleController.getString(R.string.FoxCloudTitle)); + cloudTitle.setSpan(new ForegroundColorSpan(0xFF4A90D9), 0, cloudTitle.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + UItem cloudItem = UItem.asButton(cloudRow, new ShimmerHeartDrawable(AndroidUtilities.dp(24)), cloudTitle); + cloudItem.subtext = LocaleController.getString(R.string.FoxCloudSubtitle); + items.add(cloudItem); items.add(UItem.asButtonSubtext(topSponsorsRow, R.drawable.msg_premium_liststar, LocaleController.getString(R.string.FoxTopSponsors), LocaleController.getString(R.string.FoxTopSponsorsAbout))); items.add(UItem.asShadow(null)); @@ -88,6 +100,10 @@ public class NekoDonateActivity extends BaseNekoSettingsActivity { openBotWithCommand("vpnghostbot", "/quickreg"); } else if (id == premiumRow) { presentFragment(new FoxPremiumActivity()); + } else if (id == spaceRow) { + presentFragment(new FoxSpaceActivity()); + } else if (id == cloudRow) { + presentFragment(new FoxCloudActivity()); } else if (id == topSponsorsRow) { presentFragment(new FoxSponsorsActivity()); } else if (id >= cryptoRow) { diff --git a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/NekoEmojiSettingsActivity.java b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/NekoEmojiSettingsActivity.java index 5ab1185b..50a02746 100644 --- a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/NekoEmojiSettingsActivity.java +++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/NekoEmojiSettingsActivity.java @@ -9,6 +9,7 @@ import android.graphics.PorterDuffColorFilter; import android.graphics.drawable.Drawable; import android.net.Uri; import android.util.SparseBooleanArray; +import android.util.SparseIntArray; import android.view.View; import androidx.core.content.FileProvider; @@ -55,12 +56,17 @@ public class NekoEmojiSettingsActivity extends BaseNekoSettingsActivity implemen private final ArrayList emojiPacks = new ArrayList<>(); private final SparseBooleanArray selectedItems = new SparseBooleanArray(); + // Download states for built-in packs: key = index in BUILT_IN_PACKS + private final SparseIntArray builtInDownloadStates = new SparseIntArray(); + private final int useSystemEmojiRow = rowId++; private final int appleRow = rowId++; private final int emojiAddRow = rowId++; private final int emojiStartRow = 100; + // Built-in packs occupy rows 200..203 + private final int builtInStartRow = 200; private ChatAttachAlert chatAttachAlert; private NumberTextView selectedCountTextView; @@ -99,6 +105,23 @@ public class NekoEmojiSettingsActivity extends BaseNekoSettingsActivity implemen emojiPacks.addAll(EmojiHelper.getInstance().getEmojiPacksInfo()); } + /** Returns true if a built-in pack with given id is already installed. */ + private boolean isBuiltInInstalled(String builtInId) { + for (EmojiHelper.EmojiPack pack : emojiPacks) { + if (pack.getPackName().equalsIgnoreCase(getBuiltInNameById(builtInId))) { + return true; + } + } + return false; + } + + private String getBuiltInNameById(String id) { + for (EmojiHelper.BuiltInEmojiPack p : EmojiHelper.BUILT_IN_PACKS) { + if (p.id.equals(id)) return p.name; + } + return id; + } + @Override @SuppressLint("UseCompatLoadingForDrawables") protected void fillItems(ArrayList items, UniversalAdapter adapter) { @@ -122,6 +145,21 @@ public class NekoEmojiSettingsActivity extends BaseNekoSettingsActivity implemen CombinedDrawable combinedDrawable = new CombinedDrawable(drawable1, drawable2); items.add(TextCreationCellFactory.of(emojiAddRow, LocaleController.getString(R.string.AddEmojiSet), combinedDrawable).slug("emojiAdd")); items.add(UItem.asShadow(LocaleController.getString(R.string.EmojiSetHint))); + + // Built-in downloadable packs + items.add(UItem.asHeader(LocaleController.getString(R.string.EmojiPacksAvailable))); + BuiltInEmojiSetCellFactory.DownloadListener downloadListener = this::startBuiltInDownload; + for (int i = 0; i < EmojiHelper.BUILT_IN_PACKS.size(); i++) { + EmojiHelper.BuiltInEmojiPack builtIn = EmojiHelper.BUILT_IN_PACKS.get(i); + int state; + if (builtInDownloadStates.indexOfKey(i) >= 0) { + state = builtInDownloadStates.get(i); + } else { + state = isBuiltInInstalled(builtIn.id) ? EmojiSetCell.STATE_INSTALLED : EmojiSetCell.STATE_NOT_INSTALLED; + } + items.add(BuiltInEmojiSetCellFactory.of(builtInStartRow + i, builtIn, state, downloadListener)); + } + items.add(UItem.asShadow(null)); } @Override @@ -139,7 +177,7 @@ public class NekoEmojiSettingsActivity extends BaseNekoSettingsActivity implemen chatAttachAlert.setEmojiPicker(); chatAttachAlert.init(); chatAttachAlert.show(); - } else if (id == appleRow || id >= emojiStartRow) { + } else if (id == appleRow || (id >= emojiStartRow && id < builtInStartRow)) { EmojiSetCell cell = (EmojiSetCell) view; if (id != appleRow && hasSelected()) { toggleSelected(id); @@ -151,19 +189,56 @@ public class NekoEmojiSettingsActivity extends BaseNekoSettingsActivity implemen EmojiHelper.reloadEmoji(); notifyItemChanged(useSystemEmojiRow, PARTIAL); } + } else if (id >= builtInStartRow) { + // clicks on the row itself are ignored — only the download button triggers download } } @Override protected boolean onItemLongClick(UItem item, View view, int position, float x, float y) { var id = item.id; - if (id >= emojiStartRow) { + if (id >= emojiStartRow && id < builtInStartRow) { toggleSelected(id); return true; } return super.onItemLongClick(item, view, position, x, y); } + private void startBuiltInDownload(int builtInIndex) { + EmojiHelper.BuiltInEmojiPack builtIn = EmojiHelper.BUILT_IN_PACKS.get(builtInIndex); + builtInDownloadStates.put(builtInIndex, EmojiSetCell.STATE_DOWNLOADING); + notifyItemChanged(builtInStartRow + builtInIndex, PARTIAL); + + EmojiHelper.getInstance().downloadBuiltInPack(builtIn, new EmojiHelper.DownloadCallback() { + @Override + public void onProgress(int percent) { + // Could update a determinate progress — skipped for now + } + + @Override + public void onDone(EmojiHelper.EmojiPack pack) { + builtInDownloadStates.put(builtInIndex, EmojiSetCell.STATE_INSTALLED); + listView.adapter.update(true); + EmojiHelper.reloadEmoji(); + } + + @Override + public void onError(Exception e) { + builtInDownloadStates.put(builtInIndex, EmojiSetCell.STATE_NOT_INSTALLED); + notifyItemChanged(builtInStartRow + builtInIndex, PARTIAL); + if (getParentActivity() != null) { + AndroidUtilities.runOnUIThread(() -> + new AlertDialog.Builder(getParentActivity(), resourcesProvider) + .setTitle(LocaleController.getString(R.string.AppName)) + .setMessage(LocaleController.getString(R.string.EmojiPackDownloadError)) + .setPositiveButton(LocaleController.getString(R.string.OK), null) + .create().show() + ); + } + } + }); + } + private void updateEmojiSets() { var selectedPackId = EmojiHelper.getInstance().getSelectedEmojiPackId(); var appleItem = listView.findItemByItemId(appleRow); @@ -323,7 +398,6 @@ public class NekoEmojiSettingsActivity extends BaseNekoSettingsActivity implemen processFiles(filesToUpload); } - @Override public void startDocumentSelectActivity() { try { @@ -441,6 +515,8 @@ public class NekoEmojiSettingsActivity extends BaseNekoSettingsActivity implemen return super.onBackPressed(invoked); } + // ---- Cell factories ---- + private static class EmojiSetCellFactory extends UItem.UItemFactory { static { setup(new EmojiSetCellFactory()); @@ -471,6 +547,47 @@ public class NekoEmojiSettingsActivity extends BaseNekoSettingsActivity implemen } } + /** Factory for built-in downloadable packs. */ + private static class BuiltInEmojiSetCellFactory extends UItem.UItemFactory { + static { + setup(new BuiltInEmojiSetCellFactory()); + } + + interface DownloadListener { + void onDownloadClick(int builtInIndex); + } + + @Override + public EmojiSetCell createView(Context context, RecyclerListView listView, int currentAccount, int classGuid, Theme.ResourcesProvider resourcesProvider) { + return new EmojiSetCell(context, true, resourcesProvider); + } + + @Override + public void bindView(View view, UItem item, boolean divider, UniversalAdapter adapter, UniversalRecyclerView listView) { + EmojiSetCell cell = (EmojiSetCell) view; + EmojiHelper.BuiltInEmojiPack builtIn = (EmojiHelper.BuiltInEmojiPack) item.object; + int state = item.intValue; + cell.setBuiltInData(builtIn, state, false, divider); + if (cell.getDownloadButton() != null) { + DownloadListener listener = (DownloadListener) item.object2; + cell.getDownloadButton().setOnClickListener(v -> { + if (listener == null) return; + int idx = EmojiHelper.BUILT_IN_PACKS.indexOf(builtIn); + if (idx >= 0) listener.onDownloadClick(idx); + }); + } + } + + public static UItem of(int id, EmojiHelper.BuiltInEmojiPack pack, int state, DownloadListener listener) { + var item = UItem.ofFactory(BuiltInEmojiSetCellFactory.class); + item.id = id; + item.object = pack; + item.intValue = state; + item.object2 = listener; + return item; + } + } + protected static class TextCreationCellFactory extends UItem.UItemFactory { static { setup(new TextCreationCellFactory()); @@ -495,5 +612,4 @@ public class NekoEmojiSettingsActivity extends BaseNekoSettingsActivity implemen return item; } } - } diff --git a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/NekoFontSettingsActivity.java b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/NekoFontSettingsActivity.java new file mode 100644 index 00000000..389931bc --- /dev/null +++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/NekoFontSettingsActivity.java @@ -0,0 +1,360 @@ +package tw.nekomimi.nekogram.settings; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.graphics.Canvas; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.text.TextUtils; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; + +import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.LocaleController; +import org.telegram.messenger.R; +import org.telegram.messenger.Utilities; +import org.telegram.ui.ActionBar.AlertDialog; +import org.telegram.ui.ActionBar.Theme; +import org.telegram.ui.Components.AnimatedTextView; +import org.telegram.ui.Components.CombinedDrawable; +import org.telegram.ui.Components.CubicBezierInterpolator; +import org.telegram.ui.Components.LayoutHelper; +import org.telegram.ui.Components.RadialProgressView; +import org.telegram.ui.Components.RecyclerListView; +import org.telegram.ui.Components.UItem; +import org.telegram.ui.Components.UniversalAdapter; +import org.telegram.ui.Components.UniversalRecyclerView; + +import java.io.File; +import java.io.InputStream; +import java.util.ArrayList; + +import tw.nekomimi.nekogram.helpers.FontHelper; + +public class NekoFontSettingsActivity extends BaseNekoSettingsActivity { + + private static final int fontStartRow = 10; + private static final int addFileRow = 900; + private static final int addGFontsRow = 901; + + @Override + protected void fillItems(ArrayList items, UniversalAdapter adapter) { + items.add(UItem.asHeader(LocaleController.getString(R.string.FontSelectTitle))); + + String selectedId = FontHelper.getSelectedFontId(); + + // Built-in fonts + for (int i = 0; i < FontHelper.FONTS.size(); i++) { + FontHelper.FontItem font = FontHelper.FONTS.get(i); + items.add(FontCellFactory.of(fontStartRow + i, font, font.id.equals(selectedId))); + } + + // User-installed fonts + ArrayList userFonts = new ArrayList<>(FontHelper.getUserFonts()); + for (int i = 0; i < userFonts.size(); i++) { + FontHelper.FontItem font = userFonts.get(i); + items.add(FontCellFactory.of(fontStartRow + FontHelper.FONTS.size() + i, font, font.id.equals(selectedId))); + } + + items.add(UItem.asShadow(null)); + + // Add font buttons + items.add(UItem.asHeader(LocaleController.getString(R.string.FontAddTitle))); + + Drawable addIcon1 = getParentActivity().getDrawable(R.drawable.poll_add_circle); + Drawable addIcon2 = getParentActivity().getDrawable(R.drawable.poll_add_plus); + addIcon1.setColorFilter(new PorterDuffColorFilter(getThemedColor(Theme.key_switchTrackChecked), PorterDuff.Mode.MULTIPLY)); + addIcon2.setColorFilter(new PorterDuffColorFilter(getThemedColor(Theme.key_checkboxCheck), PorterDuff.Mode.MULTIPLY)); + CombinedDrawable iconFile = new CombinedDrawable(addIcon1, addIcon2); + + Drawable addIcon3 = getParentActivity().getDrawable(R.drawable.poll_add_circle); + Drawable addIcon4 = getParentActivity().getDrawable(R.drawable.poll_add_plus); + addIcon3.setColorFilter(new PorterDuffColorFilter(getThemedColor(Theme.key_switchTrackChecked), PorterDuff.Mode.MULTIPLY)); + addIcon4.setColorFilter(new PorterDuffColorFilter(getThemedColor(Theme.key_checkboxCheck), PorterDuff.Mode.MULTIPLY)); + CombinedDrawable iconGF = new CombinedDrawable(addIcon3, addIcon4); + + items.add(NekoEmojiSettingsActivity.TextCreationCellFactory.of(addFileRow, LocaleController.getString(R.string.FontAddFile), iconFile)); + items.add(NekoEmojiSettingsActivity.TextCreationCellFactory.of(addGFontsRow, LocaleController.getString(R.string.FontAddGoogleFonts), iconGF)); + items.add(UItem.asShadow(LocaleController.getString(R.string.FontSelectHint))); + } + + @Override + protected void onItemClick(UItem item, View view, int position, float x, float y) { + int id = item.id; + + if (id == addFileRow) { + openFilePicker(); + return; + } + if (id == addGFontsRow) { + showGoogleFontsDialog(); + return; + } + + // Font selection + if (id >= fontStartRow) { + int idx = id - fontStartRow; + FontHelper.FontItem font; + if (idx < FontHelper.FONTS.size()) { + font = FontHelper.FONTS.get(idx); + } else { + ArrayList userFonts = new ArrayList<>(FontHelper.getUserFonts()); + int userIdx = idx - FontHelper.FONTS.size(); + if (userIdx >= userFonts.size()) return; + font = userFonts.get(userIdx); + } + FontHelper.setFont(font.id); + listView.adapter.update(true); + parentLayout.rebuildAllFragmentViews(false, false); + } + } + + private void openFilePicker() { + try { + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.setType("font/*"); + intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"font/ttf", "font/otf", "application/octet-stream"}); + startActivityForResult(intent, 42); + } catch (Exception e) { + new AlertDialog.Builder(getParentActivity(), resourcesProvider) + .setTitle(LocaleController.getString(R.string.AppName)) + .setMessage(e.getMessage()) + .setPositiveButton(LocaleController.getString(R.string.OK), null) + .create().show(); + } + } + + private void showGoogleFontsDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity(), resourcesProvider); + builder.setTitle(LocaleController.getString(R.string.FontAddGoogleFonts)); + builder.setMessage(LocaleController.getString(R.string.FontGoogleFontsHint)); + + final android.widget.EditText input = new android.widget.EditText(getParentActivity()); + input.setHint("Open Sans"); + input.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_FLAG_CAP_WORDS); + int padding = AndroidUtilities.dp(16); + input.setPadding(padding, padding / 2, padding, padding / 2); + builder.setView(input); + + builder.setPositiveButton(LocaleController.getString(R.string.EmojiPackDownload), (dialog, which) -> { + String family = input.getText().toString().trim(); + if (family.isEmpty()) return; + startGoogleFontsDownload(family); + }); + builder.setNegativeButton(LocaleController.getString(R.string.Cancel), null); + builder.create().show(); + } + + private void startGoogleFontsDownload(String family) { + AlertDialog progress = new AlertDialog(getParentActivity(), 3); + progress.setCanCancel(false); + progress.showDelayed(200); + + FontHelper.downloadFromGoogleFonts(family, new FontHelper.DownloadFontCallback() { + @Override + public void onProgress(int percent) { + // progress dialog doesn't show percent, just spinning + } + + @Override + public void onDone(FontHelper.FontItem font) { + progress.dismiss(); + FontHelper.setFont(font.id); + listView.adapter.update(true); + parentLayout.rebuildAllFragmentViews(false, false); + } + + @Override + public void onError(Exception e) { + progress.dismiss(); + if (getParentActivity() != null) { + new AlertDialog.Builder(getParentActivity(), resourcesProvider) + .setTitle(LocaleController.getString(R.string.AppName)) + .setMessage(LocaleController.getString(R.string.FontDownloadError) + "\n" + e.getMessage()) + .setPositiveButton(LocaleController.getString(R.string.OK), null) + .create().show(); + } + } + }); + } + + @Override + public void onActivityResultFragment(int requestCode, int resultCode, Intent data) { + if (requestCode == 42 && resultCode == Activity.RESULT_OK && data != null && data.getData() != null) { + AlertDialog progress = new AlertDialog(getParentActivity(), 3); + progress.setCanCancel(false); + progress.showDelayed(200); + Utilities.globalQueue.postRunnable(() -> { + try { + // Copy file to cache + android.net.Uri uri = data.getData(); + String fileName = getFileName(uri); + if (fileName == null) fileName = "CustomFont"; + String fontName = fileName.replaceAll("\\.[^.]+$", "").replace("-", " ").replace("_", " "); + + File cacheFile = new File(AndroidUtilities.getCacheDir(), "font_import.ttf"); + try (InputStream is = getParentActivity().getContentResolver().openInputStream(uri); + java.io.FileOutputStream os = new java.io.FileOutputStream(cacheFile)) { + byte[] buf = new byte[8192]; int n; + while ((n = is.read(buf)) != -1) os.write(buf, 0, n); + } + FontHelper.FontItem installed = FontHelper.installUserFont(cacheFile, fontName); + AndroidUtilities.runOnUIThread(() -> { + progress.dismiss(); + FontHelper.setFont(installed.id); + listView.adapter.update(true); + parentLayout.rebuildAllFragmentViews(false, false); + }); + } catch (Exception e) { + AndroidUtilities.runOnUIThread(() -> { + progress.dismiss(); + if (getParentActivity() != null) { + new AlertDialog.Builder(getParentActivity(), resourcesProvider) + .setTitle(LocaleController.getString(R.string.AppName)) + .setMessage(e.getMessage()) + .setPositiveButton(LocaleController.getString(R.string.OK), null) + .create().show(); + } + }); + } + }); + } + } + + private String getFileName(android.net.Uri uri) { + try (android.database.Cursor cursor = getParentActivity().getContentResolver() + .query(uri, null, null, null, null)) { + if (cursor != null && cursor.moveToFirst()) { + int idx = cursor.getColumnIndex(android.provider.OpenableColumns.DISPLAY_NAME); + if (idx >= 0) return cursor.getString(idx); + } + } catch (Exception ignored) {} + return null; + } + + @Override + protected String getActionBarTitle() { + return LocaleController.getString(R.string.FontSettings); + } + + @Override + protected String getKey() { return "fonts"; } + + // ---- Font cell factory ---- + + private static class FontCellFactory extends UItem.UItemFactory { + static { setup(new FontCellFactory()); } + + @Override + public FontCell createView(Context context, RecyclerListView listView, int currentAccount, + int classGuid, Theme.ResourcesProvider resourcesProvider) { + return new FontCell(context, resourcesProvider); + } + + @Override + public void bindView(View view, UItem item, boolean divider, UniversalAdapter adapter, + UniversalRecyclerView listView) { + ((FontCell) view).setData((FontHelper.FontItem) item.object, item.checked, divider); + } + + public static UItem of(int id, FontHelper.FontItem font, boolean checked) { + UItem item = UItem.ofFactory(FontCellFactory.class); + item.id = id; + item.object = font; + item.checked = checked; + return item; + } + } + + @SuppressLint("ViewConstructor") + private static class FontCell extends FrameLayout { + + private final TextView nameView; + private final AnimatedTextView subtitleView; + private final ImageView checkView; + private boolean needDivider; + + public FontCell(Context context, Theme.ResourcesProvider resourcesProvider) { + super(context); + + nameView = new TextView(context); + nameView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); + nameView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText, resourcesProvider)); + nameView.setLines(1); + nameView.setMaxLines(1); + nameView.setSingleLine(true); + nameView.setEllipsize(TextUtils.TruncateAt.END); + nameView.setGravity(LayoutHelper.getAbsoluteGravityStart()); + addView(nameView, LayoutHelper.createFrameRelatively( + LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, + Gravity.START, 21, 9, 60, 0)); + + subtitleView = new AnimatedTextView(context); + subtitleView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteGrayText, resourcesProvider)); + subtitleView.setAnimationProperties(.55f, 0, 320, CubicBezierInterpolator.EASE_OUT_QUINT); + subtitleView.setTextSize(AndroidUtilities.dp(13)); + subtitleView.setGravity(LayoutHelper.getAbsoluteGravityStart()); + addView(subtitleView, LayoutHelper.createFrameRelatively( + LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, + Gravity.START, 21, 29, 60, 0)); + + checkView = new ImageView(context); + checkView.setScaleType(ImageView.ScaleType.CENTER); + checkView.setColorFilter( + Theme.getColor(Theme.key_featuredStickers_addedIcon, resourcesProvider), + PorterDuff.Mode.MULTIPLY); + checkView.setImageResource(R.drawable.floating_check); + checkView.setVisibility(GONE); + addView(checkView, LayoutHelper.createFrame(40, 40, + (LocaleController.isRTL ? Gravity.LEFT : Gravity.RIGHT) | Gravity.CENTER_VERTICAL, + LocaleController.isRTL ? 10 : 0, 0, LocaleController.isRTL ? 0 : 10, 0)); + } + + public void setData(FontHelper.FontItem font, boolean checked, boolean divider) { + needDivider = divider; + // Render the name in the font itself + Typeface tf = null; + try { + if (font.userFile != null && font.userFile.exists()) { + tf = Typeface.createFromFile(font.userFile); + } else if (font.assetPath != null) { + tf = Typeface.createFromAsset(getContext().getAssets(), font.assetPath); + } + } catch (Exception ignored) {} + nameView.setTypeface(tf != null ? tf : Typeface.DEFAULT); + nameView.setText(font.name); + subtitleView.setText(font.license, false); + checkView.setVisibility(checked ? VISIBLE : GONE); + setWillNotDraw(!divider); + } + + @Override + protected void onDraw(@NonNull Canvas canvas) { + if (needDivider) { + canvas.drawLine( + LocaleController.isRTL ? 0 : AndroidUtilities.dp(21), + getHeight() - 1, + getWidth() - (LocaleController.isRTL ? AndroidUtilities.dp(21) : 0), + getHeight() - 1, + Theme.dividerPaint); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure( + MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(58) + (needDivider ? 1 : 0), MeasureSpec.EXACTLY)); + } + } +} diff --git a/TMessagesProj/src/main/res/drawable/ic_launcher_foreground.xml b/TMessagesProj/src/main/res/drawable/ic_launcher_foreground.xml index 920dcfcf..eb7ebcc4 100644 --- a/TMessagesProj/src/main/res/drawable/ic_launcher_foreground.xml +++ b/TMessagesProj/src/main/res/drawable/ic_launcher_foreground.xml @@ -1,21 +1,3 @@ - - - - - - - - + diff --git a/TMessagesProj/src/main/res/drawable/msg_archive.xml b/TMessagesProj/src/main/res/drawable/msg_archive.xml new file mode 100644 index 00000000..48deb7fe --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/msg_archive.xml @@ -0,0 +1,13 @@ + + + + diff --git a/TMessagesProj/src/main/res/drawable/msg_arrow_back.xml b/TMessagesProj/src/main/res/drawable/msg_arrow_back.xml new file mode 100644 index 00000000..ea4f5fe2 --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/msg_arrow_back.xml @@ -0,0 +1,9 @@ + + + diff --git a/TMessagesProj/src/main/res/drawable/msg_arrowright.xml b/TMessagesProj/src/main/res/drawable/msg_arrowright.xml new file mode 100644 index 00000000..5e745dfb --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/msg_arrowright.xml @@ -0,0 +1,9 @@ + + + diff --git a/TMessagesProj/src/main/res/drawable/msg_block2.xml b/TMessagesProj/src/main/res/drawable/msg_block2.xml new file mode 100644 index 00000000..e728b227 --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/msg_block2.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/TMessagesProj/src/main/res/drawable/msg_calendar2.xml b/TMessagesProj/src/main/res/drawable/msg_calendar2.xml new file mode 100644 index 00000000..ecd1e075 --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/msg_calendar2.xml @@ -0,0 +1,13 @@ + + + + diff --git a/TMessagesProj/src/main/res/drawable/msg_calls.xml b/TMessagesProj/src/main/res/drawable/msg_calls.xml new file mode 100644 index 00000000..9b0d0429 --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/msg_calls.xml @@ -0,0 +1,9 @@ + + + diff --git a/TMessagesProj/src/main/res/drawable/msg_cancel.xml b/TMessagesProj/src/main/res/drawable/msg_cancel.xml new file mode 100644 index 00000000..de97580f --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/msg_cancel.xml @@ -0,0 +1,10 @@ + + + diff --git a/TMessagesProj/src/main/res/drawable/msg_contact_add.xml b/TMessagesProj/src/main/res/drawable/msg_contact_add.xml new file mode 100644 index 00000000..4a778408 --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/msg_contact_add.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/TMessagesProj/src/main/res/drawable/msg_contacts.xml b/TMessagesProj/src/main/res/drawable/msg_contacts.xml new file mode 100644 index 00000000..6c4ff82b --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/msg_contacts.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/TMessagesProj/src/main/res/drawable/msg_copy.xml b/TMessagesProj/src/main/res/drawable/msg_copy.xml new file mode 100644 index 00000000..23eb64f9 --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/msg_copy.xml @@ -0,0 +1,12 @@ + + + + diff --git a/TMessagesProj/src/main/res/drawable/msg_delete.xml b/TMessagesProj/src/main/res/drawable/msg_delete.xml new file mode 100644 index 00000000..2ab042c8 --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/msg_delete.xml @@ -0,0 +1,13 @@ + + + + diff --git a/TMessagesProj/src/main/res/drawable/msg_discussion.xml b/TMessagesProj/src/main/res/drawable/msg_discussion.xml new file mode 100644 index 00000000..b6d68882 --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/msg_discussion.xml @@ -0,0 +1,12 @@ + + + + diff --git a/TMessagesProj/src/main/res/drawable/msg_download.xml b/TMessagesProj/src/main/res/drawable/msg_download.xml new file mode 100644 index 00000000..19043640 --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/msg_download.xml @@ -0,0 +1,13 @@ + + + + diff --git a/TMessagesProj/src/main/res/drawable/msg_edit.xml b/TMessagesProj/src/main/res/drawable/msg_edit.xml new file mode 100644 index 00000000..56ce6a43 --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/msg_edit.xml @@ -0,0 +1,12 @@ + + + + diff --git a/TMessagesProj/src/main/res/drawable/msg_fave.xml b/TMessagesProj/src/main/res/drawable/msg_fave.xml new file mode 100644 index 00000000..fbee4f58 --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/msg_fave.xml @@ -0,0 +1,9 @@ + + + diff --git a/TMessagesProj/src/main/res/drawable/msg_forward.xml b/TMessagesProj/src/main/res/drawable/msg_forward.xml new file mode 100644 index 00000000..e6e6e089 --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/msg_forward.xml @@ -0,0 +1,10 @@ + + + diff --git a/TMessagesProj/src/main/res/drawable/msg_info.xml b/TMessagesProj/src/main/res/drawable/msg_info.xml new file mode 100644 index 00000000..c3b4deb5 --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/msg_info.xml @@ -0,0 +1,10 @@ + + + diff --git a/TMessagesProj/src/main/res/drawable/msg_input_like.xml b/TMessagesProj/src/main/res/drawable/msg_input_like.xml new file mode 100644 index 00000000..6d58633e --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/msg_input_like.xml @@ -0,0 +1,9 @@ + + + diff --git a/TMessagesProj/src/main/res/drawable/msg_link.xml b/TMessagesProj/src/main/res/drawable/msg_link.xml new file mode 100644 index 00000000..5708222d --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/msg_link.xml @@ -0,0 +1,12 @@ + + + + diff --git a/TMessagesProj/src/main/res/drawable/msg_media.xml b/TMessagesProj/src/main/res/drawable/msg_media.xml new file mode 100644 index 00000000..e1578fc5 --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/msg_media.xml @@ -0,0 +1,13 @@ + + + + diff --git a/TMessagesProj/src/main/res/drawable/msg_message.xml b/TMessagesProj/src/main/res/drawable/msg_message.xml new file mode 100644 index 00000000..95facc7d --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/msg_message.xml @@ -0,0 +1,10 @@ + + + diff --git a/TMessagesProj/src/main/res/drawable/msg_mute.xml b/TMessagesProj/src/main/res/drawable/msg_mute.xml new file mode 100644 index 00000000..9a1638ec --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/msg_mute.xml @@ -0,0 +1,13 @@ + + + + diff --git a/TMessagesProj/src/main/res/drawable/msg_pin.xml b/TMessagesProj/src/main/res/drawable/msg_pin.xml new file mode 100644 index 00000000..2fa124f2 --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/msg_pin.xml @@ -0,0 +1,9 @@ + + + diff --git a/TMessagesProj/src/main/res/drawable/msg_qrcode.xml b/TMessagesProj/src/main/res/drawable/msg_qrcode.xml new file mode 100644 index 00000000..16fecfac --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/msg_qrcode.xml @@ -0,0 +1,23 @@ + + + + + + + + + + diff --git a/TMessagesProj/src/main/res/drawable/msg_report.xml b/TMessagesProj/src/main/res/drawable/msg_report.xml new file mode 100644 index 00000000..e2176d89 --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/msg_report.xml @@ -0,0 +1,9 @@ + + + diff --git a/TMessagesProj/src/main/res/drawable/msg_reset.xml b/TMessagesProj/src/main/res/drawable/msg_reset.xml new file mode 100644 index 00000000..9677b407 --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/msg_reset.xml @@ -0,0 +1,10 @@ + + + diff --git a/TMessagesProj/src/main/res/drawable/msg_search.xml b/TMessagesProj/src/main/res/drawable/msg_search.xml new file mode 100644 index 00000000..41e39921 --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/msg_search.xml @@ -0,0 +1,10 @@ + + + diff --git a/TMessagesProj/src/main/res/drawable/msg_secret.xml b/TMessagesProj/src/main/res/drawable/msg_secret.xml new file mode 100644 index 00000000..547fbb09 --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/msg_secret.xml @@ -0,0 +1,10 @@ + + + diff --git a/TMessagesProj/src/main/res/drawable/msg_settings.xml b/TMessagesProj/src/main/res/drawable/msg_settings.xml new file mode 100644 index 00000000..d9651e2d --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/msg_settings.xml @@ -0,0 +1,10 @@ + + + diff --git a/TMessagesProj/src/main/res/drawable/msg_share.xml b/TMessagesProj/src/main/res/drawable/msg_share.xml new file mode 100644 index 00000000..0416db36 --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/msg_share.xml @@ -0,0 +1,13 @@ + + + + diff --git a/TMessagesProj/src/main/res/drawable/msg_share_filled.xml b/TMessagesProj/src/main/res/drawable/msg_share_filled.xml new file mode 100644 index 00000000..8e90f147 --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/msg_share_filled.xml @@ -0,0 +1,10 @@ + + + diff --git a/TMessagesProj/src/main/res/drawable/msg_stats.xml b/TMessagesProj/src/main/res/drawable/msg_stats.xml new file mode 100644 index 00000000..06867005 --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/msg_stats.xml @@ -0,0 +1,10 @@ + + + diff --git a/TMessagesProj/src/main/res/drawable/msg_theme.xml b/TMessagesProj/src/main/res/drawable/msg_theme.xml new file mode 100644 index 00000000..b6d68882 --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/msg_theme.xml @@ -0,0 +1,12 @@ + + + + diff --git a/TMessagesProj/src/main/res/drawable/msg_translate.xml b/TMessagesProj/src/main/res/drawable/msg_translate.xml new file mode 100644 index 00000000..11d256bf --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/msg_translate.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/TMessagesProj/src/main/res/drawable/msg_user_remove.xml b/TMessagesProj/src/main/res/drawable/msg_user_remove.xml new file mode 100644 index 00000000..e9a529ae --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/msg_user_remove.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/TMessagesProj/src/main/res/drawable/msg_videocall.xml b/TMessagesProj/src/main/res/drawable/msg_videocall.xml new file mode 100644 index 00000000..ae5760e8 --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/msg_videocall.xml @@ -0,0 +1,9 @@ + + + diff --git a/TMessagesProj/src/main/res/drawable/settings_account.xml b/TMessagesProj/src/main/res/drawable/settings_account.xml index d5d1d1b2..9f0da197 100644 --- a/TMessagesProj/src/main/res/drawable/settings_account.xml +++ b/TMessagesProj/src/main/res/drawable/settings_account.xml @@ -1,11 +1,12 @@ - + android:viewportWidth="24" + android:viewportHeight="24"> - \ No newline at end of file + android:fillColor="#FF000000" + android:pathData="M12,6m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0"/> + + diff --git a/TMessagesProj/src/main/res/drawable/settings_chat.xml b/TMessagesProj/src/main/res/drawable/settings_chat.xml index 84e49732..caa2ddda 100644 --- a/TMessagesProj/src/main/res/drawable/settings_chat.xml +++ b/TMessagesProj/src/main/res/drawable/settings_chat.xml @@ -1,11 +1,18 @@ - + android:viewportWidth="24" + android:viewportHeight="24"> - \ No newline at end of file + android:fillColor="#FF000000" + android:pathData="M12,22C17.5228,22 22,17.5228 22,12C22,6.4772 17.5228,2 12,2C6.4772,2 2,6.4772 2,12C2,13.5997 2.3756,15.1116 3.0435,16.4525C3.2209,16.8088 3.28,17.2161 3.1771,17.6006L2.5815,19.8267C2.323,20.793 3.207,21.677 4.1734,21.4185L6.3994,20.8229C6.7839,20.72 7.1912,20.7791 7.5475,20.9565C8.8884,21.6244 10.4003,22 12,22Z"/> + + + + diff --git a/TMessagesProj/src/main/res/drawable/settings_data.xml b/TMessagesProj/src/main/res/drawable/settings_data.xml index 9a164fbb..b9fec7ce 100644 --- a/TMessagesProj/src/main/res/drawable/settings_data.xml +++ b/TMessagesProj/src/main/res/drawable/settings_data.xml @@ -1,10 +1,13 @@ - - - \ No newline at end of file + android:viewportWidth="24" + android:viewportHeight="24"> + + + diff --git a/TMessagesProj/src/main/res/drawable/settings_devices.xml b/TMessagesProj/src/main/res/drawable/settings_devices.xml index 2e1f731a..bf5ef6b6 100644 --- a/TMessagesProj/src/main/res/drawable/settings_devices.xml +++ b/TMessagesProj/src/main/res/drawable/settings_devices.xml @@ -1,10 +1,13 @@ - - - \ No newline at end of file + android:viewportWidth="24" + android:viewportHeight="24"> + + + diff --git a/TMessagesProj/src/main/res/drawable/settings_folders.xml b/TMessagesProj/src/main/res/drawable/settings_folders.xml index 18f793a8..678e5152 100644 --- a/TMessagesProj/src/main/res/drawable/settings_folders.xml +++ b/TMessagesProj/src/main/res/drawable/settings_folders.xml @@ -1,10 +1,10 @@ - - - \ No newline at end of file + android:viewportWidth="24" + android:viewportHeight="24"> + + diff --git a/TMessagesProj/src/main/res/drawable/settings_language.xml b/TMessagesProj/src/main/res/drawable/settings_language.xml index 215ca81b..7040e9cc 100644 --- a/TMessagesProj/src/main/res/drawable/settings_language.xml +++ b/TMessagesProj/src/main/res/drawable/settings_language.xml @@ -1,10 +1,22 @@ - - - \ No newline at end of file + android:viewportWidth="24" + android:viewportHeight="24"> + + + + + + diff --git a/TMessagesProj/src/main/res/drawable/settings_power.xml b/TMessagesProj/src/main/res/drawable/settings_power.xml index eb82254d..f81094a0 100644 --- a/TMessagesProj/src/main/res/drawable/settings_power.xml +++ b/TMessagesProj/src/main/res/drawable/settings_power.xml @@ -1,10 +1,10 @@ - - - \ No newline at end of file + android:viewportWidth="24" + android:viewportHeight="24"> + + diff --git a/TMessagesProj/src/main/res/drawable/settings_privacy.xml b/TMessagesProj/src/main/res/drawable/settings_privacy.xml index 14a256b6..8fc31498 100644 --- a/TMessagesProj/src/main/res/drawable/settings_privacy.xml +++ b/TMessagesProj/src/main/res/drawable/settings_privacy.xml @@ -1,10 +1,10 @@ - - - \ No newline at end of file + android:viewportWidth="24" + android:viewportHeight="24"> + + diff --git a/TMessagesProj/src/main/res/drawable/settings_sounds.xml b/TMessagesProj/src/main/res/drawable/settings_sounds.xml index 01411628..69708bc5 100644 --- a/TMessagesProj/src/main/res/drawable/settings_sounds.xml +++ b/TMessagesProj/src/main/res/drawable/settings_sounds.xml @@ -1,10 +1,12 @@ - - - \ No newline at end of file + android:viewportWidth="24" + android:viewportHeight="24"> + + + diff --git a/TMessagesProj/src/main/res/raw/foxi_cloud.webm b/TMessagesProj/src/main/res/raw/foxi_cloud.webm new file mode 100644 index 00000000..404d5bac Binary files /dev/null and b/TMessagesProj/src/main/res/raw/foxi_cloud.webm differ diff --git a/TMessagesProj/src/main/res/raw/foxi_space.webm b/TMessagesProj/src/main/res/raw/foxi_space.webm new file mode 100644 index 00000000..404d5bac Binary files /dev/null and b/TMessagesProj/src/main/res/raw/foxi_space.webm differ diff --git a/TMessagesProj/src/main/res/values-ru/strings.xml b/TMessagesProj/src/main/res/values-ru/strings.xml index 24c56292..b428fe89 100644 --- a/TMessagesProj/src/main/res/values-ru/strings.xml +++ b/TMessagesProj/src/main/res/values-ru/strings.xml @@ -263,4 +263,7 @@ \'Отправить сегодня в\' HH:mm \'Отправить\' d MMM \'в\' HH:mm \'Напомнить\' d MMM yyyy \'в\' HH:mm + Обновить серверы + Загрузка списка серверов… + Список серверов обновлён \ No newline at end of file diff --git a/TMessagesProj/src/main/res/values/strings.xml b/TMessagesProj/src/main/res/values/strings.xml index 3a38ef37..d3335ce7 100644 --- a/TMessagesProj/src/main/res/values/strings.xml +++ b/TMessagesProj/src/main/res/values/strings.xml @@ -2662,6 +2662,9 @@ Connections Proxy added. Add Proxy + Refresh Servers + Fetching server list… + Server list updated Delete Proxy? Delete Proxy Are you sure you want to delete this proxy? diff --git a/TMessagesProj/src/main/res/values/strings_neko.xml b/TMessagesProj/src/main/res/values/strings_neko.xml index 0c343c1c..0c9d1b95 100644 --- a/TMessagesProj/src/main/res/values/strings_neko.xml +++ b/TMessagesProj/src/main/res/values/strings_neko.xml @@ -165,6 +165,14 @@ Premium, Space and Cloud app icons Support FoxiGram This badge is granted for supporting the project with 444+ rubles. + Space FoxiGram + Unlock the cosmos and enjoy exclusive space perks + Space app icon + Exclusive Space icon for your app + Cloud FoxiGram + Float above the rest with exclusive cloud perks + Cloud app icon + Exclusive Cloud icon for your app Support the project Subscribe to GhostCloud via our bot Top sponsors diff --git a/build.gradle b/build.gradle index cf8aeebb..f06d80a5 100644 --- a/build.gradle +++ b/build.gradle @@ -26,6 +26,11 @@ subprojects { coreLibraryDesugaringEnabled true } + tasks.withType(JavaCompile).configureEach { + options.forkOptions.javaHome = file('C:/jdk21/jdk-21.0.7+6') + options.fork = true + } + defaultConfig { minSdkVersion 23 targetSdkVersion 36 diff --git a/gradle.properties b/gradle.properties index 107169fb..55e8c039 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ android.useAndroidX=true android.enableAppCompileTimeRClass=true org.gradle.java.home=C:/jdk21/jdk-21.0.7+6 -org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8 -Djava.home=C:/jdk21/jdk-21.0.7+6 +org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8 org.gradle.parallel=true APP_VERSION_CODE=6750