Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions lib/networking/tor_aware_network_image.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import 'dart:async';
import 'dart:convert';
import 'dart:ui' as ui;

import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_svg/flutter_svg.dart';

import '../app_config.dart';
import '../services/tor_service.dart';
import '../utilities/prefs.dart';
import 'http.dart';

/// Fetches [url] through the app's [HTTP] client, routing over Tor when it is
/// enabled. Throws if Tor is enabled but not connected, so nothing falls back
/// to clearnet.
Future<Uint8List> fetchTorAwareBytes(String url) async {
final response = await const HTTP().get(
url: Uri.parse(url),
proxyInfo: !AppConfig.hasFeature(AppFeature.tor)
? null
: Prefs.instance.useTor
? TorService.sharedInstance.getProxyInfo()
: null,
);

if (response.code != 200) {
throw Exception("Failed to load $url: status=${response.code}");
}

return Uint8List.fromList(response.bodyBytes);
}

/// Drop-in replacement for [NetworkImage] that respects the Tor preference.
/// Use with [Image] instead of [Image.network], which always goes clearnet.
@immutable
class TorAwareNetworkImage extends ImageProvider<TorAwareNetworkImage> {
const TorAwareNetworkImage(this.url);

final String url;

@override
Future<TorAwareNetworkImage> obtainKey(ImageConfiguration configuration) =>
SynchronousFuture<TorAwareNetworkImage>(this);

@override
ImageStreamCompleter loadImage(
TorAwareNetworkImage key,
ImageDecoderCallback decode,
) {
final chunkEvents = StreamController<ImageChunkEvent>();
return MultiFrameImageStreamCompleter(
codec: _load(key, decode, chunkEvents),
chunkEvents: chunkEvents.stream,
scale: 1.0,
debugLabel: key.url,
);
}

Future<ui.Codec> _load(
TorAwareNetworkImage key,
ImageDecoderCallback decode,
StreamController<ImageChunkEvent> chunkEvents,
) async {
try {
// Lets loadingBuilder show its indicator while the request runs.
chunkEvents.add(
const ImageChunkEvent(
cumulativeBytesLoaded: 0,
expectedTotalBytes: null,
),
);
final bytes = await fetchTorAwareBytes(key.url);
final buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
return await decode(buffer);
} catch (_) {
// Don't keep a failed load in the cache.
scheduleMicrotask(() {
PaintingBinding.instance.imageCache.evict(key);
});
rethrow;
} finally {
unawaited(chunkEvents.close());
}
}

@override
bool operator ==(Object other) =>
other is TorAwareNetworkImage && other.url == url;

@override
int get hashCode => url.hashCode;

@override
String toString() =>
"${objectRuntimeType(this, "TorAwareNetworkImage")}"
"(\"$url\")";
}

/// Drop-in replacement for the loader behind [SvgPicture.network] that
/// respects the Tor preference.
/// Use as `SvgPicture(TorAwareSvgNetworkLoader(url))`.
@immutable
class TorAwareSvgNetworkLoader extends SvgLoader<Uint8List> {
const TorAwareSvgNetworkLoader(this.url);

final String url;

@override
Future<Uint8List?> prepareMessage(BuildContext? context) =>
fetchTorAwareBytes(url);

@override
String provideSvg(Uint8List? message) =>
utf8.decode(message!, allowMalformed: true);

@override
bool operator ==(Object other) =>
other is TorAwareSvgNetworkLoader && other.url == url;

@override
int get hashCode => url.hashCode;
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import 'package:isar_community/isar.dart';
import '../../../../models/isar/exchange_cache/currency.dart';
import '../../../../models/isar/models/contract.dart';
import '../../../../models/isar/models/ethereum/eth_contract.dart';
import '../../../../networking/tor_aware_network_image.dart';
import '../../../../services/exchange/change_now/change_now_exchange.dart';
import '../../../../services/exchange/exchange_data_loading_service.dart';
import '../../../../themes/stack_colors.dart';
Expand Down Expand Up @@ -101,8 +102,8 @@ class _AddTokenListElementState extends ConsumerState<AddTokenListElement> {
)
else
currency != null
? SvgPicture.network(
currency!.image,
? SvgPicture(
TorAwareSvgNetworkLoader(currency!.image),
width: iconSize,
height: iconSize,
placeholderBuilder: (_) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import '../../../../models/add_wallet_list_entity/add_wallet_list_entity.dart';
import '../../../../models/add_wallet_list_entity/sub_classes/eth_token_entity.dart';
import '../../../../models/add_wallet_list_entity/sub_classes/sol_token_entity.dart';
import '../../../../models/isar/exchange_cache/currency.dart';
import '../../../../networking/tor_aware_network_image.dart';
import '../../../../providers/providers.dart';
import '../../../../services/exchange/change_now/change_now_exchange.dart';
import '../../../../services/exchange/exchange_data_loading_service.dart';
Expand Down Expand Up @@ -146,15 +147,15 @@ class _CoinSelectItemState extends ConsumerState<CoinSelectItem> {
else
tokenImageUri != null
? tokenImageUri!.toLowerCase().endsWith(".svg")
? SvgPicture.network(
tokenImageUri!,
? SvgPicture(
TorAwareSvgNetworkLoader(tokenImageUri!),
width: 26,
height: 26,
placeholderBuilder: (_) =>
const AppIcon(width: 26, height: 26),
)
: Image.network(
tokenImageUri!,
: Image(
image: TorAwareNetworkImage(tokenImageUri!),
width: 26,
height: 26,
errorBuilder: (_, _, _) => SvgPicture.file(
Expand Down
5 changes: 3 additions & 2 deletions lib/pages/cakepay/cakepay_card_detail_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../../networking/tor_aware_network_image.dart';
import '../../providers/global/locale_provider.dart';
import '../../services/cakepay/cakepay_service.dart';
import '../../services/cakepay/src/models/card.dart';
Expand Down Expand Up @@ -340,8 +341,8 @@ class _CardImage extends StatelessWidget {
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
imageUrl,
child: Image(
image: TorAwareNetworkImage(imageUrl),
width: isDesktop ? 300 : null,
fit: isDesktop ? .contain : .fitWidth,
loadingBuilder: (_, child, event) {
Expand Down
49 changes: 23 additions & 26 deletions lib/pages/cakepay/cakepay_vendors_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';

import '../../networking/tor_aware_network_image.dart';
import '../../services/cakepay/cakepay_service.dart';
import '../../services/cakepay/src/models/card.dart';
import '../../themes/stack_colors.dart';
Expand Down Expand Up @@ -87,9 +88,8 @@ class _CakePayVendorsViewState extends State<CakePayVendorsView> {
}

Future<void> _onCardTapped(CakePayCard card) async {
await Navigator.of(
context,
).pushNamed(CakePayCardDetailView.routeName, arguments: card);
await Navigator.of(context)
.pushNamed(CakePayCardDetailView.routeName, arguments: card);
}

@override
Expand Down Expand Up @@ -130,9 +130,9 @@ class _CakePayVendorsViewState extends State<CakePayVendorsView> {
condition: !isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor: Theme.of(
context,
).extension<StackColors>()!.background,
backgroundColor: Theme.of(context)
.extension<StackColors>()!
.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () => Navigator.of(context).pop(),
Expand Down Expand Up @@ -210,9 +210,9 @@ class _CakePayVendorsViewState extends State<CakePayVendorsView> {
child: Text(
"No items",
style: STextStyles.w500_14(context).copyWith(
color: Theme.of(
context,
).extension<StackColors>()!.textSubtitle1,
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
),
Expand All @@ -226,9 +226,9 @@ class _CakePayVendorsViewState extends State<CakePayVendorsView> {
Text(
error.toString(),
style: STextStyles.w500_14(context).copyWith(
color: Theme.of(
context,
).extension<StackColors>()!.textSubtitle1,
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
const SizedBox(height: 16),
Expand Down Expand Up @@ -276,9 +276,9 @@ class _SearchField extends StatelessWidget {
focusNode: focusNode,
style: isDesktop
? STextStyles.desktopTextExtraSmall(context).copyWith(
color: Theme.of(
context,
).extension<StackColors>()!.textFieldActiveText,
color: Theme.of(context)
.extension<StackColors>()!
.textFieldActiveText,
)
: STextStyles.field(context),
decoration:
Expand Down Expand Up @@ -327,9 +327,8 @@ class _CountryDropdown extends StatelessWidget {
);

final itemStyle = isDesktop
? STextStyles.desktopTextExtraSmall(
context,
).copyWith(color: colors.textFieldActiveText)
? STextStyles.desktopTextExtraSmall(context)
.copyWith(color: colors.textFieldActiveText)
: STextStyles.w500_14(context);

return ClipRRect(
Expand All @@ -341,9 +340,8 @@ class _CountryDropdown extends StatelessWidget {
hint: Text(
"All countries",
style: isDesktop
? STextStyles.desktopTextExtraSmall(
context,
).copyWith(color: colors.textFieldDefaultSearchIconLeft)
? STextStyles.desktopTextExtraSmall(context)
.copyWith(color: colors.textFieldDefaultSearchIconLeft)
: STextStyles.fieldLabel(context),
),
items: [
Expand Down Expand Up @@ -446,8 +444,8 @@ class _CardTile extends StatelessWidget {
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: card.cardImageUrl != null
? Image.network(
card.cardImageUrl!,
? Image(
image: TorAwareNetworkImage(card.cardImageUrl!),
width: isDesktop ? 60 : 48,
height: isDesktop ? 40 : 32,
fit: BoxFit.cover,
Expand Down Expand Up @@ -483,9 +481,8 @@ class _CardTile extends StatelessWidget {
].join(' '),
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(context)
: STextStyles.itemSubtitle12(
context,
).copyWith(color: colors.textSubtitle1),
: STextStyles.itemSubtitle12(context)
.copyWith(color: colors.textSubtitle1),
),
],
),
Expand Down
Loading