feat: get remote avatar from datapod, and sync local avatars

This commit is contained in:
poka 2024-01-05 00:58:15 +01:00
parent 639b376dc1
commit 55eb61fb7d
23 changed files with 424 additions and 438 deletions

View File

@ -8,7 +8,7 @@ import 'package:hive_flutter/hive_flutter.dart';
import 'package:logger/logger.dart';
// Version of box data
const int dataVersion = 7;
const int dataVersion = 8;
late String appVersion;
const int pinLength = 5;
@ -21,13 +21,17 @@ late Box configBox;
late Box<G1WalletsList> g1WalletsBox;
late Box<G1WalletsList> contactsBox;
// late Box keystoreBox;
late Directory imageDirectory;
late Directory avatarsDirectory;
late Directory avatarsCacheDirectory;
late bool isTall;
String cesiumPod = "https://g1.data.le-sou.org";
const cesiumPod = "https://g1.data.le-sou.org";
// String cesiumPod = "https://g1.data.presles.fr";
// String cesiumPod = "https://g1.data.e-is.pro";
const datapodEndpoint = 'https://gdev-datapod.p2p.legal';
// const v2sDatapod = 'http://10.0.2.2:8080';
// Contexts
late BuildContext homeContext;

View File

@ -20,7 +20,6 @@ import 'package:gecko/globals.dart';
import 'package:gecko/models/chest_data.dart';
import 'package:gecko/models/g1_wallets_list.dart';
import 'package:gecko/models/wallet_data.dart';
import 'package:gecko/providers/cesium_plus.dart';
import 'package:gecko/providers/chest_provider.dart';
import 'package:gecko/providers/duniter_indexer.dart';
import 'package:gecko/providers/generate_wallets.dart';
@ -55,7 +54,7 @@ Future<void> main() async {
// await dotenv.load();
// }
HomeProvider homeProvider = HomeProvider();
final homeProvider = HomeProvider();
// DuniterIndexer _duniterIndexer = DuniterIndexer();
await initHiveForFlutter();
@ -131,7 +130,6 @@ class Gecko extends StatelessWidget {
ChangeNotifierProvider(create: (_) => GenerateWalletsProvider()),
ChangeNotifierProvider(create: (_) => WalletOptionsProvider()),
ChangeNotifierProvider(create: (_) => SearchProvider()),
ChangeNotifierProvider(create: (_) => CesiumPlusProvider()),
ChangeNotifierProvider(create: (_) => SubstrateSdk()),
ChangeNotifierProvider(create: (_) => DuniterIndexer()),
ChangeNotifierProvider(create: (_) => SettingsProvider()),

View File

@ -24,3 +24,19 @@ mutation ($addressOld: String!, $addressNew: String!, $hash: String!, $signature
}
}
''';
const String getAvatarQ = r'''
query ($address: String!) {
profiles_by_pk(address: $address) {
avatar64
}
}
''';
const String profileEditedAtQ = r'''
query ($address: String!) {
profiles_by_pk(address: $address) {
updated_at
}
}
''';

View File

@ -1,4 +1,12 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'package:gecko/globals.dart';
import 'package:gecko/providers/v2s_datapod.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:provider/provider.dart';
import 'package:uuid/uuid.dart';
part 'wallet_data.g.dart';
@HiveType(typeId: 0)
@ -36,6 +44,9 @@ class WalletData extends HiveObject {
@HiveField(10)
List<int>? certs;
@HiveField(11)
DateTime? profileUpdatedTime;
WalletData({
required this.address,
this.chest,
@ -44,6 +55,7 @@ class WalletData extends HiveObject {
this.derivation,
this.imageDefaultPath,
this.imageCustomPath,
this.profileUpdatedTime,
this.isOwned = false,
this.identityStatus = IdtyStatus.unknown,
this.balance = 0,
@ -75,11 +87,47 @@ class WalletData extends HiveObject {
return balance != 0;
}
Future<DateTime?> getUpdatedTime() async {
final datapod = Provider.of<V2sDatapodProvider>(homeContext, listen: false);
return await datapod.profileEditedAt(address);
}
Future<bool> shouldUpdateProfile() async {
final remoteUpdatedProfile = await getUpdatedTime();
late Duration difference;
if (profileUpdatedTime != null && remoteUpdatedProfile != null) {
difference = profileUpdatedTime!.difference(remoteUpdatedProfile);
} else if (remoteUpdatedProfile != null) {
return true;
} else {
difference = Duration.zero;
}
return difference.inSeconds.abs() >= 30;
}
/// This method get the remote avatar on v2s-datapod only if needed, and store it on disk
Future getDatapodAvatar() async {
if (!await shouldUpdateProfile()) return;
final datapod = Provider.of<V2sDatapodProvider>(homeContext, listen: false);
final avatarUuid = const Uuid().v4();
await datapod.getAvatar(address, saveOnDisk: true, uuid: avatarUuid);
final avatarPath = '${avatarsDirectory.path}/$address-$avatarUuid';
if (!await File(avatarPath).exists()) return;
profileUpdatedTime = await getUpdatedTime();
imageCustomPath = avatarPath;
walletBox.put(address, this);
datapod.reload();
}
bool hasCustomImage() {
return imageCustomPath != null;
}
// returns only the id part of the ':'-separated string
List<int?> id() {
return [chest, number];
}

View File

@ -24,6 +24,7 @@ class WalletDataAdapter extends TypeAdapter<WalletData> {
derivation: fields[4] as int?,
imageDefaultPath: fields[5] as String?,
imageCustomPath: fields[6] as String?,
profileUpdatedTime: fields[11] as DateTime?,
isOwned: fields[7] as bool,
identityStatus: fields[8] as IdtyStatus,
balance: fields[9] as double,
@ -34,7 +35,7 @@ class WalletDataAdapter extends TypeAdapter<WalletData> {
@override
void write(BinaryWriter writer, WalletData obj) {
writer
..writeByte(11)
..writeByte(12)
..writeByte(0)
..write(obj.address)
..writeByte(1)
@ -56,7 +57,9 @@ class WalletDataAdapter extends TypeAdapter<WalletData> {
..writeByte(9)
..write(obj.balance)
..writeByte(10)
..write(obj.certs);
..write(obj.certs)
..writeByte(11)
..write(obj.profileUpdatedTime);
}
@override

View File

@ -1,261 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:gecko/globals.dart';
import 'package:gecko/models/scale_functions.dart';
import 'package:gecko/providers/substrate_sdk.dart';
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
import 'package:crypto/crypto.dart';
// import 'package:http/http.dart' as http;
class CesiumPlusProvider with ChangeNotifier {
final cesiumName = TextEditingController();
CancelToken avatarCancelToken = CancelToken();
final Map<String, String> _headers = {
'Content-type': 'application/json',
'Accept': 'application/json',
};
// List _buildQueryGetAvatar(String pubkeyV1) {
// final queryGetAvatar = json.encode({
// "query": {
// "bool": {
// "should": [
// {
// "match": {
// '_id': {"query": pubkeyV1, "boost": 1}
// }
// },
// {
// "prefix": {'_id': pubkeyV1}
// }
// ]
// }
// },
// "_source": [
// "avatar",
// "avatar._content_type",
// ],
// "indices_boost": {"user": 1, "page": 1, "group": 0.01}
// });
// const requestUrl = "/user,page,group/profile,record/_search";
// final podRequest = cesiumPod + requestUrl;
// return [podRequest, queryGetAvatar];
// }
Future<List> _buildQuerySetAvatar(
String pubkeyV1, String address, String avatar) async {
int timeSent = DateTime.now().millisecondsSinceEpoch ~/ 1000;
final queryGetAvatar = json.encode({
"avatar": {"_content": avatar, "_content_type": "image/png"},
"time": timeSent,
"issuer": pubkeyV1,
"version": 2,
"tags": []
});
final requestUrl =
"/user/profile?pubkey=$pubkeyV1/_update?pubkey=$pubkeyV1";
final podRequest = cesiumPod + requestUrl;
final signedDocument = await signDoc(queryGetAvatar, address);
return [podRequest, signedDocument];
}
Future<String> signDoc(String document, String address) async {
final sub = Provider.of<SubstrateSdk>(homeContext, listen: false);
final hashDocBytes = utf8.encode(document);
final hashDoc = sha256.convert(hashDocBytes);
final hashDocHex = hashDoc.toString().toUpperCase();
// Generate signature of document
final signature = await sub.signDatapod(hashDocHex, address);
// Build final document
final Map<String, dynamic> data = {
'hash': hashDocHex,
'signature': signature
};
final signJSON = jsonEncode(data);
final Map<String, dynamic> finalJSON = {
...jsonDecode(signJSON),
...jsonDecode(document)
};
return jsonEncode(finalJSON);
}
// Future<String> getName(String address) async {
// String? name;
// if (g1WalletsBox.get(address)?.csName != null) {
// return g1WalletsBox.get(address)!.csName!;
// }
// List queryOptions = await _buildQueryName(address);
// var dio = Dio();
// late Response response;
// try {
// response = await dio.post(
// queryOptions[0],
// data: queryOptions[1],
// options: Options(
// headers: queryOptions[2],
// sendTimeout: const Duration(seconds: 3),
// receiveTimeout: const Duration(seconds: 5),
// ),
// );
// // response = await http.post((Uri.parse(queryOptions[0])),
// // body: queryOptions[1], headers: queryOptions[2]);
// } catch (e) {
// log.e(e);
// }
// if (response.data['hits']['hits'].toString() == '[]') {
// return '';
// }
// final bool nameExist =
// response.data['hits']['hits'][0]['_source'].containsKey("title");
// if (!nameExist) {
// return '';
// }
// name = response.data['hits']['hits'][0]['_source']['title'];
// name ??= '';
// g1WalletsBox.get(address)!.csName = name;
// return name;
// }
Future<Image> getAvatar(String address, double size) async {
return defaultAvatar(size);
// final sub = Provider.of<SubstrateSdk>(homeContext, listen: false);
// if (await isAvatarExist(address)) {
// return await getAvatarLocal(address, size);
// }
// final pubkeyV1 = await sub.addressToPubkeyB58(address);
// var dio = Dio();
// List queryOptions = _buildQueryGetAvatar(pubkeyV1);
// late Response response;
// try {
// response = await dio
// .post(queryOptions[0],
// data: queryOptions[1],
// options: Options(
// headers: _headers,
// sendTimeout: const Duration(seconds: 4),
// receiveTimeout: const Duration(seconds: 15),
// ),
// cancelToken: avatarCancelToken)
// .timeout(
// const Duration(seconds: 15),
// );
// } catch (e) {
// log.e(e);
// }
// if (response.data['hits']['hits'].toString() == '[]' ||
// !response.data['hits']['hits'][0]['_source'].containsKey("avatar")) {
// return defaultAvatar(size);
// }
// final avatar =
// response.data['hits']['hits'][0]['_source']['avatar']['_content'];
// final avatarFile = await saveAvatar(address, avatar);
// final finalAvatar = Image.file(
// avatarFile,
// height: size,
// fit: BoxFit.fitWidth,
// );
// return finalAvatar;
}
Future<bool> setAvatar(String address, String avatarPath) async {
final sub = Provider.of<SubstrateSdk>(homeContext, listen: false);
final pubkeyV1 = await sub.addressToPubkeyB58(address);
var dio = Dio();
final Uint8List avatarBytes = await File(avatarPath).readAsBytes();
final avatarString = base64Encode(avatarBytes);
List queryOptions =
await _buildQuerySetAvatar(pubkeyV1, address, avatarString);
late Response response;
try {
response = await dio
.post(queryOptions[0],
data: queryOptions[1],
options: Options(
headers: _headers,
sendTimeout: const Duration(seconds: 4),
receiveTimeout: const Duration(seconds: 15),
),
cancelToken: avatarCancelToken)
.timeout(
const Duration(seconds: 15),
);
return response.statusCode == 200;
} catch (e) {
log.e(e);
return false;
}
}
Future<String> getLocalPath() async {
final directory = await getApplicationDocumentsDirectory();
return directory.path;
}
Future<File> saveAvatar(String address, String data) async {
final path = await getLocalPath();
final avatarFolder = Directory('$path/avatars/');
if (!await avatarFolder.exists()) {
await avatarFolder.create();
}
final file = File('$path/avatars/$address');
return await file.writeAsBytes(base64.decode(data));
}
Future<Image> getAvatarLocal(String address, double size) async {
final path = await getLocalPath();
final avatarFile = File('$path/avatars/$address');
return Image.file(
avatarFile,
height: size,
fit: BoxFit.fitWidth,
);
}
Future<bool> isAvatarExist(String address) async {
final path = await getLocalPath();
final avatarFile = File('$path/avatars/$address');
return avatarFile.exists();
}
Future deleteAvatarFolder() async {
final path = await getLocalPath();
final avatarFolder = Directory('$path/avatars/');
if (await avatarFolder.exists()) {
await avatarFolder.delete(recursive: true);
}
}
}
Image defaultAvatar(double size) =>
Image.asset(('assets/icon_user.png'), height: scaleSize(size));

View File

@ -48,10 +48,11 @@ class HomeProvider with ChangeNotifier {
// Init app folders
final documentDir = await getApplicationDocumentsDirectory();
imageDirectory = Directory('${documentDir.path}/images');
avatarsDirectory = Directory('${documentDir.path}/avatars');
avatarsCacheDirectory = Directory('${documentDir.path}/avatarsCache');
if (!await imageDirectory.exists()) {
await imageDirectory.create();
if (!await avatarsDirectory.exists()) {
await avatarsDirectory.create();
}
}

View File

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'dart:async';
@ -5,6 +7,7 @@ import 'package:gecko/globals.dart';
import 'package:gecko/models/wallet_data.dart';
import 'package:gecko/providers/substrate_sdk.dart';
import 'package:gecko/widgets/commons/common_elements.dart';
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
class MyWalletsProvider with ChangeNotifier {
@ -112,6 +115,12 @@ class MyWalletsProvider with ChangeNotifier {
await configBox.delete('defaultWallet');
await sub.deleteAllAccounts();
final directory = await getApplicationDocumentsDirectory();
final avatarFolder = Directory('${directory.path}/avatars/');
if (await avatarFolder.exists()) {
await avatarFolder.delete(recursive: true);
}
myWalletProvider.pinCode = '';
await Navigator.of(context)

View File

@ -4,17 +4,17 @@ import 'package:crypto/crypto.dart';
import 'package:flutter/material.dart';
import 'package:gecko/globals.dart';
import 'package:gecko/models/queries_datapod.dart';
import 'package:gecko/models/scale_functions.dart';
import 'package:gecko/providers/my_wallets.dart';
import 'package:gecko/providers/substrate_sdk.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:provider/provider.dart';
import 'package:uuid/uuid.dart';
class V2sDatapodProvider with ChangeNotifier {
Future<QueryResult> _execQuery(
String query, Map<String, dynamic> variables) async {
final httpLink = HttpLink(
// 'http://10.0.2.2:8080/v1/graphql',
'https://gdev-datapod.p2p.legal/v1/graphql',
);
final httpLink = HttpLink('$datapodEndpoint/v1/graphql');
final GraphQLClient client = GraphQLClient(
cache: GraphQLCache(),
@ -36,6 +36,9 @@ class V2sDatapodProvider with ChangeNotifier {
List<Map<String, String>>? socials,
Map<String, double>? geoloc}) async {
final sub = Provider.of<SubstrateSdk>(homeContext, listen: false);
final myWallets =
Provider.of<MyWalletsProvider>(homeContext, listen: false);
final walletData = myWallets.getWalletDataByAddress(address);
final messageToSign = jsonEncode({
'address': address,
@ -67,6 +70,9 @@ class V2sDatapodProvider with ChangeNotifier {
return false;
}
log.d(result.data!['updateProfile']['message']);
walletData!.profileUpdatedTime = DateTime.now();
walletBox.put(address, walletData);
return true;
}
@ -122,4 +128,93 @@ class V2sDatapodProvider with ChangeNotifier {
final avatarString = base64Encode(avatarBytes);
return await updateProfile(address: address, avatar: avatarString);
}
Future<DateTime?> profileEditedAt(String address) async {
final variables = <String, dynamic>{
'address': address,
};
final result = await _execQuery(profileEditedAtQ, variables);
if (result.hasException) {
log.e(result.exception.toString());
return null;
}
final String? profileDateData =
result.data!['profiles_by_pk']?['updated_at'];
final profileDate =
profileDateData == null ? null : DateTime.tryParse(profileDateData);
return profileDate;
}
Future<Image> getAvatar(String address,
{double size = 20, bool saveOnDisk = false, String? uuid}) async {
final variables = <String, dynamic>{
'address': address,
};
final result = await _execQuery(getAvatarQ, variables);
if (result.hasException) {
log.e(result.exception.toString());
return defaultAvatar(size);
}
final String? avatar64 = result.data!['profiles_by_pk']?['avatar64'];
if (avatar64 == null) {
return defaultAvatar(size);
}
final sanitizedAvatar64 =
avatar64.replaceAll('\n', '').replaceAll('\r', '').replaceAll(' ', '');
if (saveOnDisk) {
log.d('We save avatar for $address');
await saveAvatar(address, sanitizedAvatar64, uuid);
} else {
await cacheAvatar(address, sanitizedAvatar64);
}
return Image.memory(
base64.decode(sanitizedAvatar64),
height: size,
fit: BoxFit.fitWidth,
);
}
Future<File> saveAvatar(String address, String data, String? uuid) async {
uuid = uuid ?? const Uuid().v4();
final file = File('${avatarsDirectory.path}/$address-$uuid');
return await file.writeAsBytes(base64.decode(data));
}
Future<File> cacheAvatar(String address, String data) async {
final file = File('${avatarsCacheDirectory.path}/$address');
return await file.writeAsBytes(base64.decode(data));
}
Image getAvatarLocal(String address, double size) {
final avatarFile = File('${avatarsCacheDirectory.path}/$address');
return Image.file(
avatarFile,
height: size,
fit: BoxFit.fitWidth,
);
}
Image defaultAvatar(double size) =>
Image.asset(('assets/icon_user.png'), height: scaleSize(size));
Future deleteAvatarsCacheDirectory() async {
if (await avatarsCacheDirectory.exists()) {
await avatarsCacheDirectory.delete(recursive: true);
}
}
Future deleteAvatarsDirectory() async {
if (await avatarsDirectory.exists()) {
await avatarsDirectory.delete(recursive: true);
}
}
reload() {
notifyListeners();
}
}

View File

@ -49,13 +49,13 @@ class WalletOptionsProvider with ChangeNotifier {
Future<int> deleteWallet(context, WalletData wallet) async {
final sub = Provider.of<SubstrateSdk>(context, listen: false);
final datapod = Provider.of<V2sDatapodProvider>(context, listen: false);
final bool? answer = await (confirmPopup(
context, 'areYouSureToForgetWallet'.tr(args: [wallet.name!])));
if (answer ?? false) {
//Check if balance is null
final balance = await sub.getBalance(wallet.address);
if (balance != {}) {
if (balanceCache[wallet.address] != 0) {
final myWalletProvider =
Provider.of<MyWalletsProvider>(context, listen: false);
final defaultWallet = myWalletProvider.getDefaultWallet();
@ -67,6 +67,11 @@ class WalletOptionsProvider with ChangeNotifier {
}
await walletBox.delete(wallet.key);
if (wallet.imageCustomPath != null) {
final avatarFile = File(wallet.imageCustomPath!);
await avatarFile.delete();
}
datapod.deleteProfile(address: wallet.address);
await sub.deleteAccounts([wallet.address]);
Navigator.pop(context);
@ -88,7 +93,7 @@ class WalletOptionsProvider with ChangeNotifier {
if (pickedFile != null) {
File imageFile = File(pickedFile.path);
if (!await imageDirectory.exists()) {
if (!await avatarsDirectory.exists()) {
log.e("Image folder doesn't exist");
return '';
}
@ -111,7 +116,7 @@ class WalletOptionsProvider with ChangeNotifier {
],
);
final newPath = "${imageDirectory.path}/${pickedFile.name}";
final newPath = "${avatarsDirectory.path}/${address.text}";
if (croppedFile != null) {
await File(croppedFile.path).rename(newPath);
@ -140,7 +145,7 @@ class WalletOptionsProvider with ChangeNotifier {
return showDialog<String>(
context: context,
barrierDismissible: true, // user must tap button!
barrierDismissible: true,
builder: (BuildContext context) {
return AlertDialog(
title: Text(

View File

@ -5,7 +5,6 @@ import 'package:gecko/models/chest_data.dart';
import 'package:gecko/models/g1_wallets_list.dart';
import 'package:gecko/models/scale_functions.dart';
import 'package:gecko/models/widgets_keys.dart';
import 'package:gecko/providers/cesium_plus.dart';
import 'package:gecko/providers/chest_provider.dart';
import 'package:gecko/providers/duniter_indexer.dart';
import 'package:gecko/providers/home.dart';
@ -13,6 +12,7 @@ import 'package:gecko/providers/substrate_sdk.dart';
import 'package:flutter/material.dart';
import 'package:gecko/providers/my_wallets.dart';
import 'package:gecko/models/wallet_data.dart';
import 'package:gecko/providers/v2s_datapod.dart';
import 'package:gecko/widgets/bubble_speak.dart';
import 'package:gecko/widgets/commons/animated_text.dart';
import 'package:gecko/widgets/commons/common_elements.dart';
@ -40,8 +40,7 @@ class _HomeScreenState extends State<HomeScreen> {
Provider.of<DuniterIndexer>(context, listen: false);
final myWalletProvider =
Provider.of<MyWalletsProvider>(context, listen: false);
final csProvider =
Provider.of<CesiumPlusProvider>(context, listen: false);
final datapod = Provider.of<V2sDatapodProvider>(context, listen: false);
final bool isWalletsExists = myWalletProvider.isWalletsExists();
@ -54,6 +53,8 @@ class _HomeScreenState extends State<HomeScreen> {
await infoPopup(context, "chestNotCompatibleMustReinstallGecko".tr());
await Hive.deleteBoxFromDisk('walletBox');
await Hive.deleteBoxFromDisk('chestBox');
await datapod.deleteAvatarsDirectory();
await avatarsDirectory.create();
chestBox = await Hive.openBox<ChestData>("chestBox");
await configBox.delete('defaultWallet');
if (!sub.sdkReady && !sub.sdkLoading) await sub.initApi();
@ -67,7 +68,8 @@ class _HomeScreenState extends State<HomeScreen> {
if (sub.sdkReady && !sub.nodeConnected) {
walletBox = await Hive.openBox<WalletData>("walletBox");
await Hive.deleteBoxFromDisk('g1WalletsBox');
await csProvider.deleteAvatarFolder();
await datapod.deleteAvatarsCacheDirectory();
await avatarsCacheDirectory.create();
g1WalletsBox = await Hive.openBox<G1WalletsList>("g1WalletsBox");
contactsBox = await Hive.openBox<G1WalletsList>("contactsBox");

View File

@ -10,6 +10,7 @@ import 'package:gecko/providers/duniter_indexer.dart';
import 'package:gecko/providers/my_wallets.dart';
import 'package:gecko/models/wallet_data.dart';
import 'package:gecko/providers/substrate_sdk.dart';
import 'package:gecko/providers/v2s_datapod.dart';
import 'package:gecko/providers/wallet_options.dart';
import 'package:gecko/providers/wallets_profiles.dart';
import 'package:gecko/screens/certifications.dart';
@ -258,55 +259,58 @@ class WalletOptions extends StatelessWidget {
}
Widget avatar(WalletOptionsProvider walletProvider) {
return Stack(
children: <Widget>[
InkWell(
onTap: () async {
final newPath = await (walletProvider.changeAvatar());
if (newPath != '') {
wallet.imageCustomPath = newPath;
walletBox.put(wallet.key, wallet);
// Uncomment to enable Cs+ avatar storage
// CesiumPlusProvider().setAvatar(wallet.address, newPath);
}
walletProvider.reload();
},
child: wallet.imageCustomPath == null || wallet.imageCustomPath == ''
? Image.asset(
'assets/avatars/${wallet.imageDefaultPath}',
width: scaleSize(122),
)
: Container(
width: scaleSize(122),
height: scaleSize(122),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.transparent,
image: DecorationImage(
fit: BoxFit.cover,
image: FileImage(
File(wallet.imageCustomPath!),
),
),
),
),
),
Positioned(
right: 0,
top: 0,
child: InkWell(
return Consumer<V2sDatapodProvider>(builder: (context, datapod, _) {
return Stack(
children: <Widget>[
InkWell(
onTap: () async {
wallet.imageCustomPath = await (walletProvider.changeAvatar());
final newPath = await (walletProvider.changeAvatar());
if (newPath != '') {
wallet.imageCustomPath = newPath;
walletBox.put(wallet.key, wallet);
// Uncomment to enable Cs+ avatar storage
// CesiumPlusProvider().setAvatar(wallet.address, newPath);
}
walletProvider.reload();
},
child: Image.asset(
'assets/walletOptions/camera.png',
height: scaleSize(38),
child:
wallet.imageCustomPath == null || wallet.imageCustomPath == ''
? Image.asset(
'assets/avatars/${wallet.imageDefaultPath}',
width: scaleSize(122),
)
: Container(
width: scaleSize(122),
height: scaleSize(122),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.transparent,
image: DecorationImage(
fit: BoxFit.cover,
image: FileImage(
File(wallet.imageCustomPath!),
),
),
),
),
),
Positioned(
right: 0,
top: 0,
child: InkWell(
onTap: () async {
wallet.imageCustomPath = await (walletProvider.changeAvatar());
walletProvider.reload();
},
child: Image.asset(
'assets/walletOptions/camera.png',
height: scaleSize(38),
),
),
),
),
],
);
],
);
});
}
Widget confirmIdentityButton(WalletOptionsProvider walletProvider) {

View File

@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:gecko/globals.dart';
import 'package:gecko/models/scale_functions.dart';
import 'package:gecko/models/widgets_keys.dart';
import 'package:gecko/providers/cesium_plus.dart';
import 'package:gecko/providers/substrate_sdk.dart';
import 'package:gecko/providers/v2s_datapod.dart';
import 'package:gecko/screens/wallet_view.dart';
class CertTile extends StatelessWidget {
@ -29,7 +29,7 @@ class CertTile extends StatelessWidget {
contentPadding: EdgeInsets.only(
left: 10, right: 0, top: scaleSize(3), bottom: scaleSize(3)),
leading: ClipOval(
child: defaultAvatar(avatarSize),
child: V2sDatapodProvider().defaultAvatar(avatarSize),
),
title: Padding(
padding: const EdgeInsets.only(bottom: 2),

View File

@ -1,36 +0,0 @@
import 'package:flutter/material.dart';
import 'package:gecko/globals.dart';
import 'package:gecko/providers/cesium_plus.dart';
import 'package:gecko/widgets/commons/loading.dart';
import 'package:provider/provider.dart';
class CesiumAvatar extends StatelessWidget {
const CesiumAvatar({Key? key, required this.address, this.size = 15})
: super(key: key);
final String address;
final double size;
@override
Widget build(BuildContext context) {
final csProvider = Provider.of<CesiumPlusProvider>(context, listen: false);
return ClipOval(
child: FutureBuilder(
future: csProvider.getAvatar(address, size),
builder: ((context, AsyncSnapshot<Image> avatar) {
if (avatar.hasError) {
log.e(avatar.error);
return (Icon(Icons.close_outlined,
color: Colors.red, size: size));
} else if (avatar.connectionState != ConnectionState.done) {
return SizedBox(
width: size,
height: size,
child: const FractionallySizedBox(
widthFactor: 0.6, heightFactor: 0.6, child: Loading()));
}
return avatar.data!;
})),
);
}
}

View File

@ -142,14 +142,14 @@ Future<bool?> confirmPopupCertification(BuildContext context, String question1,
Future<void> infoPopup(BuildContext context, String title) async {
return showDialog<void>(
context: context,
barrierDismissible: true, // user must tap button!
barrierDismissible: true,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor: backgroundColor,
content: Text(
title,
textAlign: TextAlign.center,
style: scaledTextStyle(fontSize: 20, fontWeight: FontWeight.w500),
style: scaledTextStyle(fontSize: 17, fontWeight: FontWeight.w500),
),
actions: <Widget>[
Row(
@ -162,7 +162,7 @@ Future<void> infoPopup(BuildContext context, String title) async {
child: Text(
"gotit".tr(),
style: scaledTextStyle(
fontSize: 21,
fontSize: 19,
color: const Color(0xffD80000),
),
),

View File

@ -10,7 +10,7 @@ import 'package:gecko/providers/substrate_sdk.dart';
import 'package:gecko/providers/wallets_profiles.dart';
import 'package:gecko/screens/wallet_view.dart';
import 'package:gecko/widgets/balance.dart';
import 'package:gecko/widgets/cesium_avatar.dart';
import 'package:gecko/widgets/datapod_avatar.dart';
import 'package:gecko/widgets/name_by_address.dart';
import 'package:provider/provider.dart';
@ -47,7 +47,7 @@ class ContactsList extends StatelessWidget {
horizontalTitleGap: 7,
contentPadding: const EdgeInsets.all(5),
dense: !isTall,
leading: CesiumAvatar(
leading: DatapodAvatar(
address: g1Wallet.address, size: scaleSize(50)),
title: Row(children: <Widget>[
Text(getShortPubkey(g1Wallet.address),

View File

@ -0,0 +1,86 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:gecko/globals.dart';
import 'package:gecko/models/queries_datapod.dart';
import 'package:gecko/models/scale_functions.dart';
import 'package:gecko/providers/v2s_datapod.dart';
import 'package:gecko/widgets/commons/loading.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:provider/provider.dart';
class DatapodAvatar extends StatelessWidget {
const DatapodAvatar({Key? key, required this.address, this.size = 15})
: super(key: key);
final String address;
final double size;
@override
Widget build(BuildContext context) {
final datapod = Provider.of<V2sDatapodProvider>(context, listen: false);
final cachedImage = File('${avatarsCacheDirectory.path}/$address');
if (cachedImage.existsSync()) {
return ScaledSizedBox(
width: size,
child: ClipOval(
child: datapod.getAvatarLocal(address, size),
),
);
}
final httpLink = HttpLink(
'$datapodEndpoint/v1/graphql',
);
final client = ValueNotifier(
GraphQLClient(
cache: GraphQLCache(store: HiveStore()),
link: httpLink,
),
);
return ScaledSizedBox(
width: size,
child: GraphQLProvider(
client: client,
child: Query(
options: QueryOptions(
document: gql(getAvatarQ),
variables: <String, dynamic>{
'address': address,
},
),
builder: (QueryResult result, {fetchMore, refetch}) {
if (result.isLoading) {
return const Center(
child: Loading(),
);
}
final String? avatar64 =
result.data!['profiles_by_pk']?['avatar64'];
if (avatar64 == null || result.data == null) {
return ClipOval(child: datapod.defaultAvatar(size));
}
final sanitizedAvatar64 = avatar64
.replaceAll('\n', '')
.replaceAll('\r', '')
.replaceAll(' ', '');
datapod.cacheAvatar(address, sanitizedAvatar64);
return ClipOval(
child: Image.memory(
base64.decode(sanitizedAvatar64),
height: size,
fit: BoxFit.fitWidth,
),
);
}),
),
);
}
}

View File

@ -10,7 +10,7 @@ import 'package:gecko/providers/wallets_profiles.dart';
import 'package:gecko/screens/certifications.dart';
import 'package:gecko/widgets/balance.dart';
import 'package:gecko/widgets/certifications.dart';
import 'package:gecko/widgets/cesium_avatar.dart';
import 'package:gecko/widgets/datapod_avatar.dart';
import 'package:gecko/widgets/commons/offline_info.dart';
import 'package:gecko/widgets/idty_status.dart';
import 'package:gecko/widgets/page_route_no_transition.dart';
@ -111,7 +111,7 @@ class HeaderProfile extends StatelessWidget {
// ScaledSizedBox(width: 20),
Column(children: <Widget>[
ScaledSizedBox(height: 15),
CesiumAvatar(address: address, size: avatarSize),
DatapodAvatar(address: address, size: avatarSize),
]),
]),
),

View File

@ -11,7 +11,7 @@ import 'package:gecko/providers/substrate_sdk.dart';
import 'package:gecko/providers/wallets_profiles.dart';
import 'package:gecko/screens/wallet_view.dart';
import 'package:gecko/widgets/balance.dart';
import 'package:gecko/widgets/cesium_avatar.dart';
import 'package:gecko/widgets/datapod_avatar.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:provider/provider.dart';
@ -82,7 +82,7 @@ class SearchIdentityQuery extends StatelessWidget {
key: keySearchResult(profile['pubkey']),
horizontalTitleGap: 10,
contentPadding: const EdgeInsets.only(right: 2),
leading: CesiumAvatar(
leading: DatapodAvatar(
address: profile['pubkey'], size: avatarSize),
title: Row(children: <Widget>[
Text(getShortPubkey(profile['pubkey']),

View File

@ -10,7 +10,7 @@ import 'package:gecko/providers/substrate_sdk.dart';
import 'package:gecko/providers/wallets_profiles.dart';
import 'package:gecko/screens/wallet_view.dart';
import 'package:gecko/widgets/balance.dart';
import 'package:gecko/widgets/cesium_avatar.dart';
import 'package:gecko/widgets/datapod_avatar.dart';
import 'package:gecko/widgets/name_by_address.dart';
import 'package:gecko/widgets/search_identity_query.dart';
@ -63,7 +63,7 @@ class SearchResult extends StatelessWidget {
key: keySearchResult(g1Wallet.address),
horizontalTitleGap: 10,
contentPadding: const EdgeInsets.all(5),
leading: CesiumAvatar(address: g1Wallet.address, size: avatarSize),
leading: DatapodAvatar(address: g1Wallet.address, size: avatarSize),
title: Row(children: <Widget>[
Text(getShortPubkey(g1Wallet.address),
style: scaledTextStyle(

View File

@ -2,9 +2,9 @@ import 'package:flutter/material.dart';
import 'package:gecko/globals.dart';
import 'package:gecko/models/scale_functions.dart';
import 'package:gecko/models/widgets_keys.dart';
import 'package:gecko/providers/cesium_plus.dart';
import 'package:gecko/providers/duniter_indexer.dart';
import 'package:gecko/providers/substrate_sdk.dart';
import 'package:gecko/providers/v2s_datapod.dart';
import 'package:gecko/screens/wallet_view.dart';
import 'package:gecko/widgets/page_route_no_transition.dart';
@ -40,7 +40,7 @@ class TransactionTile extends StatelessWidget {
contentPadding:
const EdgeInsets.symmetric(horizontal: 15, vertical: 5),
leading: ClipOval(
child: defaultAvatar(avatarSize),
child: V2sDatapodProvider().defaultAvatar(avatarSize),
),
title: Padding(
padding: const EdgeInsets.only(bottom: 5),

View File

@ -5,6 +5,7 @@ import 'package:gecko/models/scale_functions.dart';
import 'package:gecko/models/wallet_data.dart';
import 'package:gecko/models/widgets_keys.dart';
import 'package:gecko/providers/my_wallets.dart';
import 'package:gecko/providers/v2s_datapod.dart';
import 'package:gecko/screens/myWallets/wallet_options.dart';
import 'package:gecko/widgets/balance.dart';
import 'package:gecko/widgets/commons/smooth_transition.dart';
@ -24,6 +25,8 @@ class WalletTile extends StatelessWidget {
final myWalletProvider = Provider.of<MyWalletsProvider>(context);
final defaultWallet = myWalletProvider.getDefaultWallet();
repository.getDatapodAvatar();
return Padding(
padding: EdgeInsets.all(scaleSize(11)),
child: GestureDetector(
@ -50,39 +53,41 @@ class WalletTile extends StatelessWidget {
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(12)),
child: Column(children: <Widget>[
Expanded(
child: Container(
width: double.infinity,
height: double.infinity,
decoration: const BoxDecoration(
gradient: RadialGradient(
radius: 0.8,
colors: [
Color.fromARGB(255, 255, 255, 211),
yellowC,
],
Expanded(child: Consumer<V2sDatapodProvider>(
builder: (context, datapod, _) {
return Container(
width: double.infinity,
height: double.infinity,
decoration: const BoxDecoration(
gradient: RadialGradient(
radius: 0.8,
colors: [
Color.fromARGB(255, 255, 255, 211),
yellowC,
],
),
),
),
child: repository.imageCustomPath == null ||
repository.imageCustomPath == ''
? Image.asset(
'assets/avatars/${repository.imageDefaultPath}',
alignment: Alignment.bottomCenter,
scale: 0.5,
)
: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.transparent,
image: DecorationImage(
fit: BoxFit.fitHeight,
image: FileImage(
File(repository.imageCustomPath!),
child: repository.imageCustomPath == null ||
repository.imageCustomPath == ''
? Image.asset(
'assets/avatars/${repository.imageDefaultPath}',
alignment: Alignment.bottomCenter,
scale: 0.5,
)
: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.transparent,
image: DecorationImage(
fit: BoxFit.fitHeight,
image: FileImage(
File(repository.imageCustomPath!),
),
),
),
),
),
)),
);
})),
Stack(children: <Widget>[
BalanceBuilder(
address: repository.address,

View File

@ -5,6 +5,7 @@ import 'package:gecko/models/scale_functions.dart';
import 'package:gecko/models/wallet_data.dart';
import 'package:gecko/models/widgets_keys.dart';
import 'package:gecko/providers/my_wallets.dart';
import 'package:gecko/providers/v2s_datapod.dart';
import 'package:gecko/screens/myWallets/wallet_options.dart';
import 'package:gecko/widgets/balance.dart';
import 'package:gecko/widgets/certifications.dart';
@ -22,6 +23,9 @@ class WalletTileMembre extends StatelessWidget {
Widget build(BuildContext context) {
final myWalletProvider = Provider.of<MyWalletsProvider>(context);
final defaultWallet = myWalletProvider.getDefaultWallet();
repository.getDatapodAvatar();
return Padding(
padding: EdgeInsets.symmetric(
horizontal: scaleSize(52), vertical: scaleSize(15)),
@ -53,38 +57,41 @@ class WalletTileMembre extends StatelessWidget {
Expanded(
child: Stack(
children: [
Container(
width: double.infinity,
height: double.infinity,
decoration: const BoxDecoration(
gradient: RadialGradient(
radius: 0.8,
colors: [
Color.fromARGB(255, 255, 255, 211),
yellowC,
],
Consumer<V2sDatapodProvider>(
builder: (context, datapod, _) {
return Container(
width: double.infinity,
height: double.infinity,
decoration: const BoxDecoration(
gradient: RadialGradient(
radius: 0.8,
colors: [
Color.fromARGB(255, 255, 255, 211),
yellowC,
],
),
),
),
child: repository.imageCustomPath == null ||
repository.imageCustomPath == ''
? Image.asset(
'assets/avatars/${repository.imageDefaultPath}',
alignment: Alignment.bottomCenter,
scale: 0.5,
)
: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.transparent,
image: DecorationImage(
fit: BoxFit.fitHeight,
image: FileImage(
File(repository.imageCustomPath!),
child: repository.imageCustomPath == null ||
repository.imageCustomPath == ''
? Image.asset(
'assets/avatars/${repository.imageDefaultPath}',
alignment: Alignment.bottomCenter,
scale: 0.5,
)
: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.transparent,
image: DecorationImage(
fit: BoxFit.fitHeight,
image: FileImage(
File(repository.imageCustomPath!),
),
),
),
),
),
),
);
}),
Positioned(
left: 20,
top: 20,