From 47f9aef1606785d53675ab51afb94718d521dc15 Mon Sep 17 00:00:00 2001 From: instant992 Date: Tue, 9 Jun 2026 09:54:31 +0400 Subject: [PATCH] Sponsor: GhostCloud labeling, curved gloss streak, support link & top sponsors - Bulletin now reads 'Sponsor GhostCloud' + granted for 444+ rubles (en/ru) - Heart highlight reworked: thin curved streak sweeping diagonally instead of an orbiting blob; palette tuned closer to the reference - SponsorHelper.loadTopSponsors() hits mobile donors/leaderboard via X-Tg-Id - New FoxSponsorsActivity lists the top sponsors - Neko settings: 'Support the project' (t.me/vpnghostbot) + 'Top sponsors' --- .../helpers/ShimmerHeartDrawable.java | 97 ++++++++++++------- .../nekogram/helpers/SponsorHelper.java | 92 ++++++++++++++++++ .../settings/FoxSponsorsActivity.java | 83 ++++++++++++++++ .../settings/NekoSettingsActivity.java | 10 ++ .../src/main/res/values-ru/strings_neko.xml | 10 +- .../src/main/res/values/strings_neko.xml | 10 +- 6 files changed, 263 insertions(+), 39 deletions(-) create mode 100644 TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/FoxSponsorsActivity.java diff --git a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/helpers/ShimmerHeartDrawable.java b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/helpers/ShimmerHeartDrawable.java index 63c6fa58..73a93b7b 100644 --- a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/helpers/ShimmerHeartDrawable.java +++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/helpers/ShimmerHeartDrawable.java @@ -1,5 +1,6 @@ package tw.nekomimi.nekogram.helpers; +import android.graphics.BlurMaskFilter; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.LinearGradient; @@ -16,9 +17,9 @@ import androidx.annotation.NonNull; import org.telegram.messenger.AndroidUtilities; /** - * A glossy 3D-looking heart badge with a soft multicolor gradient (purple/blue - * to orange) and a moving glossy highlight ("blik") sweeping across it, like an - * iridescent emoji sticker. + * A glossy 3D-looking heart badge with a soft multicolor gradient (purple → + * blue → orange) and a thin curved glossy streak ("blik") that slowly sweeps + * diagonally across it, like light reflecting off a shiny sticker. * * It self-invalidates each frame, so when attached to a view via * {@code setRightDrawable(...)} / {@code setRightDrawable2(...)} the host keeps @@ -29,14 +30,16 @@ public class ShimmerHeartDrawable extends Drawable { private final Paint basePaint = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint tintPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - private final Paint highlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint shadePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint glossDotPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint streakPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private final Path heart = new Path(); + private final Path streak = new Path(); private int lastWidth = -1; private int lastHeight = -1; - private static final long CYCLE_MS = 3200L; + private static final long CYCLE_MS = 3600L; private final int size; public ShimmerHeartDrawable() { @@ -47,8 +50,10 @@ public class ShimmerHeartDrawable extends Drawable { this.size = sizePx; basePaint.setStyle(Paint.Style.FILL); tintPaint.setStyle(Paint.Style.FILL); - highlightPaint.setStyle(Paint.Style.FILL); shadePaint.setStyle(Paint.Style.FILL); + glossDotPaint.setStyle(Paint.Style.FILL); + streakPaint.setStyle(Paint.Style.STROKE); + streakPaint.setStrokeCap(Paint.Cap.ROUND); } private void buildHeart(Rect b) { @@ -72,25 +77,35 @@ public class ShimmerHeartDrawable extends Drawable { float l = b.left; float t = b.top; - // Base diagonal gradient: deep purple (top-left) -> blue -> warm orange (bottom-right). + // Base diagonal gradient matching the reference: violet (top-left) → + // blue → cyan → warm orange (bottom-right). basePaint.setShader(new LinearGradient( - l, t, l + w, t + h, - new int[]{0xFF7A3CE0, 0xFF5B6BFF, 0xFF4FA8FF, 0xFFFF9A4D, 0xFFFF7A3C}, - new float[]{0f, 0.32f, 0.55f, 0.82f, 1f}, + l + w * 0.15f, t, l + w * 0.9f, t + h, + new int[]{0xFF8E2DE2, 0xFF5B5BFF, 0xFF2F9BFF, 0xFFFFB24D, 0xFFFF8A3D}, + new float[]{0f, 0.30f, 0.55f, 0.85f, 1f}, Shader.TileMode.CLAMP)); - // A soft purple glow blob on the upper-left lobe for depth. + // Soft violet glow on the upper-left lobe for depth. tintPaint.setShader(new RadialGradient( - l + w * 0.30f, t + h * 0.30f, w * 0.55f, - new int[]{0xCC8A4DFF, 0x00000000}, + l + w * 0.32f, t + h * 0.28f, w * 0.6f, + new int[]{0x999B30FF, 0x00000000}, null, Shader.TileMode.CLAMP)); - // Soft inner shade at the bottom tip for a rounded 3D feel. + // Inner shade at the bottom tip for a rounded 3D feel. shadePaint.setShader(new RadialGradient( - l + w * 0.5f, t + h * 0.95f, w * 0.6f, - new int[]{0x66351A6B, 0x00000000}, + l + w * 0.55f, t + h * 0.92f, w * 0.55f, + new int[]{0x55401E7A, 0x00000000}, null, Shader.TileMode.CLAMP)); + // Fixed small specular dot, top-left (glossy sticker look). + glossDotPaint.setShader(new RadialGradient( + l + w * 0.33f, t + h * 0.27f, w * 0.16f, + new int[]{0xE6FFFFFF, 0x00FFFFFF}, + null, Shader.TileMode.CLAMP)); + + streakPaint.setStrokeWidth(Math.max(1f, w * 0.10f)); + streakPaint.setMaskFilter(new BlurMaskFilter(Math.max(1f, w * 0.06f), BlurMaskFilter.Blur.NORMAL)); + lastWidth = b.width(); lastHeight = b.height(); } @@ -115,34 +130,46 @@ public class ShimmerHeartDrawable extends Drawable { float w = b.width(); float h = b.height(); - float phase = (System.currentTimeMillis() % CYCLE_MS) / (float) CYCLE_MS; - double ang = phase * 2 * Math.PI; + float l = b.left; + float t = b.top; int save = canvas.save(); canvas.clipPath(heart); - // Base colors + purple glow + bottom shade. + // Base colors + violet glow + bottom shade. canvas.drawPath(heart, basePaint); canvas.drawPath(heart, tintPaint); canvas.drawPath(heart, shadePaint); - // Moving glossy highlight: a soft bright blob orbiting inside the heart. - float hx = b.left + w * (0.5f + 0.28f * (float) Math.cos(ang)); - float hy = b.top + h * (0.42f + 0.24f * (float) Math.sin(ang)); - float hr = w * 0.45f; - highlightPaint.setShader(new RadialGradient( - hx, hy, hr, - new int[]{0xCCFFFFFF, 0x33FFFFFF, 0x00FFFFFF}, - new float[]{0f, 0.4f, 1f}, - Shader.TileMode.CLAMP)); - canvas.drawPath(heart, highlightPaint); + // Moving curved glossy streak sweeping diagonally across the heart. + float phase = (System.currentTimeMillis() % CYCLE_MS) / (float) CYCLE_MS; + // travel from top-left (off-screen) to bottom-right (off-screen) + float p = -0.4f + phase * 1.8f; + float dx = w * p; // horizontal offset of the streak + streak.reset(); + // A gently curved (bowed) vertical-ish line, slanted diagonally. + float x0 = l + dx + w * 0.10f; + float y0 = t - h * 0.10f; + float xc = l + dx + w * 0.45f; + float yc = t + h * 0.50f; + float x1 = l + dx + w * 0.30f; + float y1 = t + h * 1.10f; + streak.moveTo(x0, y0); + streak.quadTo(xc, yc, x1, y1); - // A small fixed top-left specular dot for a glossy sticker look. - highlightPaint.setShader(new RadialGradient( - b.left + w * 0.34f, b.top + h * 0.28f, w * 0.18f, - new int[]{0xE6FFFFFF, 0x00FFFFFF}, - null, Shader.TileMode.CLAMP)); - canvas.drawPath(heart, highlightPaint); + // Fade the streak in/out at the edges of the sweep. + float edge = Math.min(1f, Math.min(phase, 1f - phase) * 3f); + int alpha = (int) (200 * Math.max(0f, edge)); + int whiteCore = (alpha << 24) | 0x00FFFFFF; + streakPaint.setShader(new LinearGradient( + x0, y0, x1, y1, + new int[]{0x00FFFFFF, whiteCore, 0x00FFFFFF}, + new float[]{0f, 0.5f, 1f}, + Shader.TileMode.CLAMP)); + canvas.drawPath(streak, streakPaint); + + // Fixed specular dot. + canvas.drawPath(heart, glossDotPaint); canvas.restoreToCount(save); diff --git a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/helpers/SponsorHelper.java b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/helpers/SponsorHelper.java index 4fb82ce9..7695108f 100644 --- a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/helpers/SponsorHelper.java +++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/helpers/SponsorHelper.java @@ -2,6 +2,7 @@ package tw.nekomimi.nekogram.helpers; import android.content.SharedPreferences; +import org.json.JSONArray; import org.json.JSONObject; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.ApplicationLoader; @@ -13,6 +14,8 @@ import org.telegram.messenger.Utilities; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; +import java.util.ArrayList; +import java.util.List; /** * Resolves the "sponsor" status of the current Telegram user against the @@ -142,4 +145,93 @@ public final class SponsorHelper { NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.sponsorStatusUpdated)); } } + + // ───────────────────────────────────────────────────────────── + // Top sponsors leaderboard + // ───────────────────────────────────────────────────────────── + + public static class Sponsor { + public int rank; + public String name; + public double totalAmount; + public boolean isMe; + } + + public interface SponsorsCallback { + /** Called on the UI thread. {@code sponsors} is null on error. */ + void onResult(List sponsors); + } + + /** + * Load the top-sponsors leaderboard from the backend. Authenticated by the + * current account's Telegram ID. The callback runs on the UI thread. + */ + public static void loadTopSponsors(SponsorsCallback callback) { + var user = UserConfig.getInstance(UserConfig.selectedAccount).getCurrentUser(); + final long telegramId = user != null ? user.id : 0; + Utilities.globalQueue.postRunnable(() -> { + List result = fetchTopSponsors(telegramId); + AndroidUtilities.runOnUIThread(() -> callback.onResult(result)); + }); + } + + // @WorkerThread + private static List fetchTopSponsors(long telegramId) { + HttpURLConnection connection = null; + try { + URL url = new URL(BASE_URL + "/api/auth/mobile/donors/leaderboard"); + connection = (HttpURLConnection) url.openConnection(); + connection.setInstanceFollowRedirects(true); + connection.setConnectTimeout(15000); + connection.setReadTimeout(15000); + if (telegramId != 0) { + connection.setRequestProperty("X-Tg-Id", String.valueOf(telegramId)); + } + connection.setRequestProperty("Accept", "application/json"); + + int code = connection.getResponseCode(); + if (code != HttpURLConnection.HTTP_OK) { + FileLog.d(TAG + ": donors/leaderboard returned HTTP " + code); + return null; + } + + StringBuilder sb = new StringBuilder(); + try (InputStream in = connection.getInputStream()) { + byte[] buffer = new byte[4096]; + int read; + while ((read = in.read(buffer)) >= 0) { + sb.append(new String(buffer, 0, read, "UTF-8")); + } + } + + JSONObject json = new JSONObject(sb.toString()); + if (!json.optBoolean("success", false)) { + return null; + } + JSONArray board = json.optJSONArray("leaderboard"); + List list = new ArrayList<>(); + if (board != null) { + for (int i = 0; i < board.length(); i++) { + JSONObject o = board.optJSONObject(i); + if (o == null) { + continue; + } + Sponsor s = new Sponsor(); + s.rank = o.optInt("rank", i + 1); + s.name = o.optString("name", ""); + s.totalAmount = o.optDouble("total_amount", 0); + s.isMe = o.optBoolean("is_me", false); + list.add(s); + } + } + return list; + } catch (Exception e) { + FileLog.e(e); + return null; + } finally { + if (connection != null) { + connection.disconnect(); + } + } + } } diff --git a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/FoxSponsorsActivity.java b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/FoxSponsorsActivity.java new file mode 100644 index 00000000..b84ff0c9 --- /dev/null +++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/FoxSponsorsActivity.java @@ -0,0 +1,83 @@ +package tw.nekomimi.nekogram.settings; + +import android.view.View; + +import org.telegram.messenger.LocaleController; +import org.telegram.messenger.R; +import org.telegram.ui.Components.UItem; +import org.telegram.ui.Components.UniversalAdapter; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import tw.nekomimi.nekogram.helpers.SponsorHelper; + +public class FoxSponsorsActivity extends BaseNekoSettingsActivity { + + private final int headerRow = rowId++; + private final int firstSponsorRow = 1000; + + private List sponsors = null; + private boolean loading = true; + private boolean failed = false; + + @Override + public boolean onFragmentCreate() { + super.onFragmentCreate(); + load(); + return true; + } + + private void load() { + loading = true; + failed = false; + SponsorHelper.loadTopSponsors(result -> { + loading = false; + if (result == null) { + failed = true; + sponsors = null; + } else { + failed = false; + sponsors = result; + } + if (listView != null) { + listView.adapter.update(true); + } + }); + } + + @Override + protected void fillItems(ArrayList items, UniversalAdapter adapter) { + items.add(UItem.asShadow(LocaleController.getString(R.string.FoxTopSponsorsAbout))); + if (loading) { + items.add(TextSettingsCellFactory.of(headerRow, LocaleController.getString(R.string.Loading))); + return; + } + if (failed) { + items.add(TextSettingsCellFactory.of(headerRow, LocaleController.getString(R.string.FoxTopSponsorsError))); + return; + } + if (sponsors == null || sponsors.isEmpty()) { + items.add(TextSettingsCellFactory.of(headerRow, LocaleController.getString(R.string.FoxTopSponsorsEmpty))); + return; + } + int i = 0; + for (SponsorHelper.Sponsor s : sponsors) { + String title = s.rank + ". " + s.name + (s.isMe ? " (" + LocaleController.getString(R.string.FromYou) + ")" : ""); + String amount = String.format(Locale.getDefault(), "%,.0f \u20bd", s.totalAmount); + items.add(TextSettingsCellFactory.of(firstSponsorRow + i, title, amount)); + i++; + } + items.add(UItem.asShadow(null)); + } + + @Override + protected void onItemClick(UItem item, View view, int position, float x, float y) { + } + + @Override + protected String getActionBarTitle() { + return LocaleController.getString(R.string.FoxTopSponsors); + } +} diff --git a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/NekoSettingsActivity.java b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/NekoSettingsActivity.java index 08c78917..6adbc61b 100644 --- a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/NekoSettingsActivity.java +++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/settings/NekoSettingsActivity.java @@ -68,6 +68,8 @@ public class NekoSettingsActivity extends BaseNekoSettingsActivity implements Fa private final int sourceCodeRow = rowId++; private final int translationRow = rowId++; private final int donateRow = rowId++; + private final int supportProjectRow = rowId++; + private final int topSponsorsRow = rowId++; private final int sponsorRow = 100; @@ -178,6 +180,10 @@ public class NekoSettingsActivity extends BaseNekoSettingsActivity implements Fa items.add(UItem.asButtonSubtext(donateRow, R.drawable.msg_input_like, LocaleController.getString(R.string.Donate), LocaleController.getString(R.string.DonateAbout)).slug("donate")); items.add(UItem.asShadow(null)); + items.add(UItem.asButtonSubtext(supportProjectRow, R.drawable.msg_input_like, LocaleController.getString(R.string.FoxSupportProject), LocaleController.getString(R.string.FoxSupportProjectAbout)).slug("supportProject")); + items.add(UItem.asButtonSubtext(topSponsorsRow, R.drawable.msg_premium_liststar, LocaleController.getString(R.string.FoxTopSponsors), LocaleController.getString(R.string.FoxTopSponsorsAbout)).slug("topSponsors")); + items.add(UItem.asShadow(null)); + newsList.clear(); newsList.addAll(ConfigHelper.getNewsForSettings()); if (!newsList.isEmpty()) { @@ -214,6 +220,10 @@ public class NekoSettingsActivity extends BaseNekoSettingsActivity implements Fa getMessagesController().openByUserName(LocaleController.getString(R.string.OfficialChannelUsername), this, 1); } else if (id == donateRow) { presentFragment(new NekoDonateActivity()); + } else if (id == supportProjectRow) { + Browser.openUrl(getParentActivity(), "https://t.me/vpnghostbot"); + } else if (id == topSponsorsRow) { + presentFragment(new FoxSponsorsActivity()); } else if (id == translationRow) { Browser.openUrl(getParentActivity(), "https://neko.crowdin.com/nekogram"); } else if (id == websiteRow) { diff --git a/TMessagesProj/src/main/res/values-ru/strings_neko.xml b/TMessagesProj/src/main/res/values-ru/strings_neko.xml index 9e55fada..47b3197a 100644 --- a/TMessagesProj/src/main/res/values-ru/strings_neko.xml +++ b/TMessagesProj/src/main/res/values-ru/strings_neko.xml @@ -323,6 +323,12 @@ Эффекты бликов Настройки специальных возможностей - Спонсор - Эта отметка выдана за поддержку проекта. Спасибо! + Спонсор GhostCloud + Эта отметка выдана за поддержание проекта на 444+ рублей. + Поддержать проект + Оформите GhostCloud через нашего бота + Топ спонсоров + Те, кто поддерживает проект + Пока нет спонсоров + Не удалось загрузить список diff --git a/TMessagesProj/src/main/res/values/strings_neko.xml b/TMessagesProj/src/main/res/values/strings_neko.xml index c6fef8fd..0d101197 100644 --- a/TMessagesProj/src/main/res/values/strings_neko.xml +++ b/TMessagesProj/src/main/res/values/strings_neko.xml @@ -149,8 +149,14 @@ Share FoxiGram... FoxiGram %1$s\nBased on Telegram %2$s\nDesigned by %3$s Installing update... - Sponsor - You received this badge for supporting the project. Thank you! + Sponsor GhostCloud + This badge is granted for supporting the project with 444+ rubles. + Support the project + Subscribe to GhostCloud via our bot + Top sponsors + People who support the project + No sponsors yet + Failed to load sponsors Downloading update... A notification will be shown when the update completes. The app will relaunch when the update completes.