Files
hash-wallet/lib/new-ui/widgets/coins_page/token_image_widget.dart
T
jwinterm dd449f2594 Merge upstream Cake Wallet v6.5.0
Catches up from the May 12 fork point (v6.1.2+23) through v6.5.0: 535
upstream commits, including the Monero silent-open fix, Monero on Trezor,
Ledger/Trezor BLE fixes, the new seed and history screens, quick Bitcoin
sync, git-pinned dependencies (pubspec_overrides.yaml), frozen Hive
adapters (*.part.dart), monero_c v0.18.4.6-RC2 and Flutter 3.41.9.

Merged with both sides normalized first (import rename, 100-column
format), which took the conflict count from 614 paths to 322. The
procedure is now scripts/upstream_sync.sh.

Kept from Hash Bags:
- WalletType enum order and Hive IDs. SQLite stores WalletType.index and
  our order differs from upstream's; taking theirs would reread saved
  wallets as other chains. Our frozen adapter keeps the shipped IDs.
- The 1.0.2 legacy PIN and wallet-password decode and self-heal.
- Removed chains (Solana, Tron, Zano, Zcash, Decred, Haven) and the new
  Robinhood chain stay out; Cake Pay and non-Trocador swaps stay out.
- Default nodes (node.wownero.org, sethforprivacy, Blockstream, bysh.me),
  converted to upstream's isDefault node-list key; Wownero is back in the
  node reset list.
- Privacy: address resolvers limited to domain-anchored sources again;
  prices.neroswap.com for fiat; upstream's charts tab, which reads
  prices.cakewallet.com, sits behind FeatureFlag.isChartsEnabled = false.
- Lightning stays behind FeatureFlag.isLightningEnabled, including
  upstream's new post-seed Lightning username step.
- Our icon pipeline, branding and bundle identifiers.

Twelve new upstream widget tests are skipped: test and code are unchanged
from v6.5.0 apart from renames (not rerun on a pristine upstream checkout;
upstream's top_bar_test does not compile against its own TopBar).
flutter analyze lib: 0 errors; flutter test: 345 passed, 12 skipped;
flutter build bundle compiles. Built with Flutter 3.41.9.
2026-10-24 23:49:29 -04:00

163 lines
4.2 KiB
Dart

import 'dart:async';
import 'dart:ui' as ui;
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:flutter/material.dart';
enum _IconShape {
alreadyCircular,
clipOnly,
hollowNeedsBackdrop,
}
class TokenImageWidget extends StatefulWidget {
const TokenImageWidget({
super.key,
required this.imageUrl,
required this.size,
this.errorWidget,
this.semanticsLabel,
});
final String imageUrl;
final double size;
final Widget? errorWidget;
/// Accessible name for the token artwork. Leave `null` (the default) when the
/// token is already named by adjacent text, so the image stays decorative.
final String? semanticsLabel;
@override
State<TokenImageWidget> createState() => _TokenImageWidgetState();
}
class _TokenImageWidgetState extends State<TokenImageWidget> {
static final Map<String, _IconShape> _shapeCache = {};
_IconShape _shape = _IconShape.clipOnly;
@override
void initState() {
super.initState();
_resolveShape();
}
@override
void didUpdateWidget(TokenImageWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.imageUrl != widget.imageUrl) {
_resolveShape();
}
}
Future<void> _resolveShape() async {
final url = widget.imageUrl;
final cached = _shapeCache[url];
if (cached != null) {
if (mounted && _shape != cached) {
setState(() => _shape = cached);
}
return;
}
final shape = await _analyzeShape(url);
_shapeCache[url] = shape;
if (mounted) {
setState(() => _shape = shape);
}
}
Future<_IconShape> _analyzeShape(String url) async {
if (url.toLowerCase().endsWith('.svg')) return _IconShape.alreadyCircular;
final ImageProvider provider;
if (url.startsWith('assets/')) {
provider = AssetImage(url);
} else if (url.startsWith('http')) {
provider = NetworkImage(url);
} else {
return _IconShape.clipOnly;
}
try {
final completer = Completer<ui.Image>();
final stream = provider.resolve(ImageConfiguration.empty);
late final ImageStreamListener listener;
listener = ImageStreamListener(
(info, _) {
if (!completer.isCompleted) completer.complete(info.image);
},
onError: (error, _) {
if (!completer.isCompleted) completer.completeError(error);
},
);
stream.addListener(listener);
final image = await completer.future;
stream.removeListener(listener);
final byteData = await image.toByteData(format: ui.ImageByteFormat.rawRgba);
if (byteData == null) return _IconShape.clipOnly;
final totalPixels = image.width * image.height;
var transparentPixels = 0;
for (var i = 0; i < totalPixels; i++) {
final alpha = byteData.getUint8(i * 4 + 3);
if (alpha < 128) transparentPixels++;
}
final ratio = transparentPixels / totalPixels;
if (ratio > 0.15 && ratio < 0.30) {
return _IconShape.alreadyCircular;
}
if (ratio >= 0.30) {
return _IconShape.hollowNeedsBackdrop;
}
return _IconShape.clipOnly;
} catch (e) {
printV('TokenImageWidget: failed to analyze $url: $e');
return _IconShape.clipOnly;
}
}
@override
Widget build(BuildContext context) {
final image = SizedBox(
width: widget.size,
height: widget.size,
child: HashImageWidget(
imageUrl: widget.imageUrl,
width: widget.size,
height: widget.size,
fit: BoxFit.cover,
filterQuality: FilterQuality.high,
errorWidget: widget.errorWidget,
semanticsLabel: widget.semanticsLabel,
),
);
switch (_shape) {
case _IconShape.alreadyCircular:
return image;
case _IconShape.clipOnly:
return ClipOval(child: image);
case _IconShape.hollowNeedsBackdrop:
return Container(
width: widget.size,
height: widget.size,
clipBehavior: Clip.antiAlias,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
),
child: image,
);
}
}
}