Skip to content

Commit

Permalink
✨ display dialog on uncaught errors
Browse files Browse the repository at this point in the history
 display a dialog on all errors which prompts the user to open a github issue.
 I filtered two errors that relate to loading emote images as these show up
 quite frequently and are already known.
  • Loading branch information
gthvmt committed Dec 18, 2023
1 parent 82a3b10 commit 7f16a9f
Show file tree
Hide file tree
Showing 9 changed files with 193 additions and 21 deletions.
2 changes: 1 addition & 1 deletion src/android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
buildscript {
ext.kotlin_version = '1.7.10'
ext.kotlin_version = '1.9.21'
repositories {
google()
mavenCentral()
Expand Down
98 changes: 95 additions & 3 deletions src/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,103 @@
import 'dart:math';
import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:seventv_for_whatsapp/screens/browser.dart';
// import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:url_launcher/url_launcher.dart';

final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
var _errorDialogIsVisible = false;

void main() {
FlutterError.onError = (details) {
FlutterError.presentError(details);

final error = details.exception.toString().toLowerCase();

// filter known errors (which should eventually be fixed...)
if (
// Some emote images, for example this one https://7tv.app/emotes/64789a4f0c7cd505faf2e7f6
// fail to load because of some error involving a frame.
// Probably only fixable by writing a custom ImageProvider(?)
error.contains('could not getpixels for frame ') ||
// Sometimes loading an image from the 7TV API just fails. Usually resolved by retrying.
// There is a NetworkImageWithRetry in the flutter_image package
// (https://github.com/flutter/packages/blob/main/packages/flutter_image/lib/network.dart)
// but it does not support animatied images which makes it kinda useless for us.
// might be a good starting point for our custom ImageProvider which should address the
// above error as well.
error.contains('connection closed while receiving data, uri =') ||
error.contains('connection reset by peer, uri =')
) {
return;
}

showErrorDialog(details.exceptionAsString(), details.stack);
};
PlatformDispatcher.instance.onError = (error, stack) {
showErrorDialog(error.toString(), stack);
return true;
};

runApp(const MyApp());
}

Future<void> showErrorDialog(String errorDescription, StackTrace? stackTrace) async {
if (_errorDialogIsVisible || _navigatorKey.currentState?.overlay == null) {
return;
}
_errorDialogIsVisible = true;
const issueTitleMaxLength = 125;

var issueTitle = errorDescription.length <= issueTitleMaxLength
? errorDescription
: "${errorDescription.substring(0, issueTitleMaxLength - 3)}...";
final issueBody = '### Exception:\n```\n$errorDescription\n```'
'${stackTrace?.toString().isEmpty ?? true ? '' : '\n\n### Stacktrace:\n```\n$stackTrace\n```'}\n### Steps to reproduce:\n';
final newIssueUri = Uri.parse('https://github.com/gthvmt/7TV-for-WhatsApp/issues/new'
'?title=${Uri.encodeComponent(issueTitle)}'
'&body=${Uri.encodeComponent(issueBody)}');

await showDialog(
context: _navigatorKey.currentState!.overlay!.context,
builder: (context) => AlertDialog(
title: const Text('Oops!'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Looks like you\'ve run into an error:'),
Container(
padding: const EdgeInsets.symmetric(horizontal: 5),
constraints:
BoxConstraints(maxHeight: max(MediaQuery.of(context).size.height * .4, 50)),
width: double.infinity,
child: Scrollbar(
child: SingleChildScrollView(
child: Text(
errorDescription +
(stackTrace?.toString().isEmpty ?? true
? ''
: '\n\nStacktrace:\n$stackTrace'),
textAlign: TextAlign.left,
style: TextStyle(color: Theme.of(context).colorScheme.error))),
),
),
const SizedBox(height: 15),
const Text('Feel free to open up a github issue.'
'(Please make sure there aren\'t any similar issues open already 😅)')
],
),
actions: [
TextButton(
onPressed: () async => await launchUrl(newIssueUri),
child: const Text('Open an issue')),
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Close'))
],
));
_errorDialogIsVisible = false;
}

class MyApp extends StatelessWidget {
const MyApp({super.key});

Expand All @@ -17,11 +108,12 @@ class MyApp extends StatelessWidget {
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);

var theme = ThemeData.from(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0x00f49236), brightness: Brightness.dark),
colorScheme:
ColorScheme.fromSeed(seedColor: const Color(0x00f49236), brightness: Brightness.dark),
useMaterial3: true);

return MaterialApp(
navigatorKey: _navigatorKey,
title: '7TV for WhatsApp',
theme: theme,
home: const Browser(),
Expand Down
16 changes: 10 additions & 6 deletions src/lib/models/seventv.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import 'dart:convert';
import 'dart:async';

class SevenTv {
static const _searchEmotesQuery = r'''
static const _searchEmotesQuery =
r'''
query SearchEmotes($query: String!, $page: Int, $sort: Sort, $limit: Int, $filter: EmoteSearchFilter) {
emotes(query: $query, page: $page, sort: $sort, limit: $limit, filter: $filter) {
count
Expand Down Expand Up @@ -43,15 +44,15 @@ class SevenTv {
''';

final _url = Uri.parse('https://7tv.io/v3/gql');
final _client = HttpClient();
final client = HttpClient();

Stream<Stream<Emote>> buildSearchRequests(Map<String, Object> args) async* {
int countCollected = 0;
int countTotal = -1;
int currentPage = 1;
while (countTotal < 0 || countTotal > countCollected) {
args['page'] = currentPage;
final req = await _client.postUrl(_url);
final req = await client.postUrl(_url);
req.headers.add(HttpHeaders.contentTypeHeader, ContentType.json.mimeType);
req.write(jsonEncode({'query': _searchEmotesQuery, 'variables': args}));
final resp = await req.close();
Expand Down Expand Up @@ -202,8 +203,11 @@ class Emote {
return data;
}

Uri getMaxSizeUrl({Format format = Format.webp}) => host!.getUrl(
host!.files!.where((f) => f.format == format).reduce((a, b) => a.height > b.height ? a : b));
Uri? getMaxSizeUrl({Format format = Format.webp}) {
final files = host?.files?.where((f) => f.format == format);
final file = files?.isEmpty ?? true ? null : files?.reduce((a, b) => a.height > b.height ? a : b);
return file == null ? null : host?.getUrl(file);
}

File getMaxSizeFile({Format format = Format.webp}) =>
host!.files!.reduce((a, b) => a.height > b.height ? a : b);
Expand Down Expand Up @@ -292,7 +296,7 @@ class Host {
throw "url does not lead to a valid webp";
}
// Check if the file is animated by looking for the "ANIM" chunk identifier
final isAnimated = listEquals(bytes.sublist(30, 30 + 4), utf8.encode('ANIM') as Uint8List);
final isAnimated = listEquals(bytes.sublist(30, 30 + 4), utf8.encode('ANIM'));
return isAnimated;
}
}
Expand Down
6 changes: 5 additions & 1 deletion src/lib/models/whatsapp.dart
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,11 @@ class Sticker {
static Future<Sticker> fromEmote(Emote emote, List<String> emojis) async {
final identifier = Ulid();
final httpClient = io.HttpClient();
final request = await httpClient.getUrl(emote.getMaxSizeUrl());
final url = emote.getMaxSizeUrl();
if (url == null) {
throw Exception('Unable to get image URL from emote');
}
final request = await httpClient.getUrl(url);
final response = await request.close();
final imagePath = '${(await WhatsApp.getStickerDirectory()).path}/$identifier.webp';

Expand Down
22 changes: 15 additions & 7 deletions src/lib/screens/browser.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import 'dart:async';
import 'dart:developer';

import 'package:flutter/material.dart';
import 'package:seventv_for_whatsapp/models/settings.dart';
Expand All @@ -9,7 +8,6 @@ import 'package:seventv_for_whatsapp/services/notification_service.dart';
import 'package:seventv_for_whatsapp/widgets/create_stickerpack_dialog.dart';
import 'package:seventv_for_whatsapp/widgets/emote_emoji_picker.dart';
import 'package:shimmer/shimmer.dart';
import 'package:universal_io/io.dart';

import '../models/whatsapp.dart';
import 'stickerpack.dart' as views;
Expand Down Expand Up @@ -157,7 +155,7 @@ class _BrowserState extends State<Browser> {
//TODO: minor performance improvement if we download the full webp in the background here after returning whether
//it is animated or not and then pass it when creating the Sticker
final emoteIsAnimated = await emote.host!.checkIfAnimated(emote.getMaxSizeFile());
debugPrint('emote is animated: ${emoteIsAnimated}');
debugPrint('emote is animated: $emoteIsAnimated');
if (!mounted) {
return;
}
Expand Down Expand Up @@ -282,7 +280,9 @@ class _BrowserState extends State<Browser> {
onTap: () => _emoteTapped(emote),
child: Stack(
children: [
Center(child: Image.network(emote.getMaxSizeUrl().toString())),
Center(
child: _buildEmoteImage(emote),
),
Align(
alignment: Alignment.bottomCenter,
child: Text(
Expand All @@ -307,16 +307,24 @@ class _BrowserState extends State<Browser> {
),
),
);

Widget _buildEmoteImage(Emote emote) {
final url = emote.getMaxSizeUrl();
if (url == null) {
// TODO: should probably not list these broken emotes alltogether
return const Placeholder();
}
return Image.network(url.toString());
}
}

class Skeleton extends StatelessWidget {
const Skeleton({
Key? key,
super.key,
required SliverGridDelegateWithMaxCrossAxisExtent gridDelegate,
required int searchCunkSize,
}) : _gridDelegate = gridDelegate,
_searchCunkSize = searchCunkSize,
super(key: key);
_searchCunkSize = searchCunkSize;

final SliverGridDelegateWithMaxCrossAxisExtent _gridDelegate;
final int _searchCunkSize;
Expand Down
4 changes: 2 additions & 2 deletions src/lib/screens/stickerpack.dart
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ class _StickerPackState extends State<StickerPack> {
backgroundColor: !isValidPack ? const Color.fromARGB(255, 120, 120, 120) : null,
foregroundColor: !isValidPack ? const Color.fromARGB(255, 70, 70, 70) : null,
onPressed: !isValidPack ? null : () => _addToWhatsApp(widget.stickerPack),
label: Row(
children: const [Icon(Icons.add), SizedBox(width: 2), Text('Add to WhatsApp')],
label: const Row(
children: [Icon(Icons.add), SizedBox(width: 2), Text('Add to WhatsApp')],
)),
);
}
Expand Down
1 change: 0 additions & 1 deletion src/lib/screens/stickerpacks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:seventv_for_whatsapp/models/whatsapp.dart';
import 'package:seventv_for_whatsapp/widgets/create_stickerpack_dialog.dart';

import 'package:seventv_for_whatsapp/services/notification_service.dart';

class StickerPackSelectedCallbackResult {
final bool reloadRequired;
Expand Down
64 changes: 64 additions & 0 deletions src/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -862,6 +862,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.2"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
sha256: e9aa5ea75c84cf46b3db4eea212523591211c3cf2e13099ee4ec147f54201c86
url: "https://pub.dev"
source: hosted
version: "6.2.2"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "31222ffb0063171b526d3e569079cf1f8b294075ba323443fdc690842bfd4def"
url: "https://pub.dev"
source: hosted
version: "6.2.0"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: bba3373219b7abb6b5e0d071b0fe66dfbe005d07517a68e38d4fc3638f35c6d3
url: "https://pub.dev"
source: hosted
version: "6.2.1"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811
url: "https://pub.dev"
source: hosted
version: "3.1.1"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234
url: "https://pub.dev"
source: hosted
version: "3.1.0"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "980e8d9af422f477be6948bdfb68df8433be71f5743a188968b0c1b887807e50"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "7286aec002c8feecc338cc33269e96b73955ab227456e9fb2a91f7fab8a358e9"
url: "https://pub.dev"
source: hosted
version: "2.2.2"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7
url: "https://pub.dev"
source: hosted
version: "3.1.1"
uuid:
dependency: transitive
description:
Expand Down
1 change: 1 addition & 0 deletions src/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ dependencies:
flutter_local_notifications: ^16.2.0
json_annotation: ^4.8.0
get_it: ^7.2.0
url_launcher: ^6.2.2
dev_dependencies:
flutter_test:
sdk: flutter
Expand Down

0 comments on commit 7f16a9f

Please sign in to comment.