diff --git a/assets/terms.md b/assets/terms.md index 752eb1e13..101150057 100644 --- a/assets/terms.md +++ b/assets/terms.md @@ -14,4 +14,4 @@ __We collect anonymous data to improve the app.__ We use Google Firebase for Cra ## Links [Sources](https://github.com/deckerst/aves) -[License](https://github.com/deckerst/aves/blob/master/LICENSE) +[License](https://github.com/deckerst/aves/blob/main/LICENSE) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 7bd7cf0cd..49cd2dc3e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -363,8 +363,11 @@ "aboutPageTitle": "About", "@aboutPageTitle": {}, - "aboutFlutter": "Flutter", - "@aboutFlutter": {}, + "aboutLinkSources": "Sources", + "@aboutLinkSources": {}, + "aboutLinkLicense": "License", + "@aboutLinkLicense": {}, + "aboutUpdate": "New Version Available", "@aboutUpdate": {}, "aboutUpdateLinks1": "A new version of Aves is available on", @@ -377,12 +380,29 @@ "@aboutUpdateGitHub": {}, "aboutUpdateGooglePlay": "Google Play", "@aboutUpdateGooglePlay": {}, + + "aboutBug": "Bug Report", + "@aboutBug": {}, + "aboutBugSaveLogInstruction": "Save app logs to a file", + "@aboutBugSaveLogInstruction": {}, + "aboutBugSaveLogButton": "Save", + "@aboutBugSaveLogButton": {}, + "aboutBugCopyInfoInstruction": "Copy system information", + "@aboutBugCopyInfoInstruction": {}, + "aboutBugCopyInfoButton": "Copy", + "@aboutBugCopyInfoButton": {}, + "aboutBugReportInstruction": "Report on GitHub with the logs and system information", + "@aboutBugReportInstruction": {}, + "aboutBugReportButton": "Report", + "@aboutBugReportButton": {}, + "aboutCredits": "Credits", "@aboutCredits": {}, "aboutCreditsWorldAtlas1": "This app uses a TopoJSON file from", "@aboutCreditsWorldAtlas1": {}, "aboutCreditsWorldAtlas2": "under ISC License.", "@aboutCreditsWorldAtlas2": {}, + "aboutLicenses": "Open-Source Licenses", "@aboutLicenses": {}, "aboutLicensesBanner": "This app uses the following open-source packages and libraries.", @@ -714,7 +734,7 @@ "settingsSectionPrivacy": "Privacy", "@settingsSectionPrivacy": {}, - "settingsEnableCrashReport": "Allow anonymous crash reporting", + "settingsEnableCrashReport": "Allow anonymous error reporting", "@settingsEnableCrashReport": {}, "settingsSaveSearchHistory": "Save search history", "@settingsSaveSearchHistory": {}, diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 70bb9a5b6..a416fa91b 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -170,16 +170,28 @@ "menuActionStats": "통계", "aboutPageTitle": "앱 정보", - "aboutFlutter": "플러터", + "aboutLinkSources": "소스 코드", + "aboutLinkLicense": "라이선스", + "aboutUpdate": "업데이트 사용 가능", "aboutUpdateLinks1": "앱의 최신 버전을", "aboutUpdateLinks2": "와", "aboutUpdateLinks3": "에서 다운로드 사용 가능합니다.", "aboutUpdateGitHub": "깃허브", "aboutUpdateGooglePlay": "구글 플레이", + + "aboutBug": "버그 보고", + "aboutBugSaveLogInstruction": "앱 로그를 파일에 저장하기", + "aboutBugSaveLogButton": "저장", + "aboutBugCopyInfoInstruction": "시스템 정보를 복사하기", + "aboutBugCopyInfoButton": "복사", + "aboutBugReportInstruction": "로그와 시스템 정보를 첨부하여 깃허브에서 이슈를 제툴하기", + "aboutBugReportButton": "제출", + "aboutCredits": "크레딧", "aboutCreditsWorldAtlas1": "이 앱은", "aboutCreditsWorldAtlas2": "의 TopoJSON 파일(ISC 라이선스)을 이용합니다.", + "aboutLicenses": "오픈 소스 라이선스", "aboutLicensesBanner": "이 앱은 다음의 오픈 소스 패키지와 라이브러리를 이용합니다.", "aboutLicensesAndroidLibraries": "안드로이드 라이브러리", diff --git a/lib/ref/mime_types.dart b/lib/ref/mime_types.dart index 1228551a6..9868a5110 100644 --- a/lib/ref/mime_types.dart +++ b/lib/ref/mime_types.dart @@ -50,6 +50,7 @@ class MimeTypes { static const webm = 'video/webm'; static const json = 'application/json'; + static const plainText = 'text/plain'; // groups diff --git a/lib/theme/icons.dart b/lib/theme/icons.dart index a23273655..ffe0201b0 100644 --- a/lib/theme/icons.dart +++ b/lib/theme/icons.dart @@ -102,4 +102,7 @@ class AIcons { static const IconData threeSixty = Icons.threesixty_outlined; static const IconData selected = Icons.check_circle_outline; static const IconData unselected = Icons.radio_button_unchecked; + + static const IconData github = MdiIcons.github; + static const IconData legal = MdiIcons.scaleBalance; } diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index 9b6b69ec4..4ef6ce2d9 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -38,6 +38,8 @@ class Constants { static const int infoGroupMaxValueLength = 140; + static const String avesGithub = 'https://github.com/deckerst/aves'; + static const List androidDependencies = [ Dependency( name: 'AndroidX Core-KTX', @@ -91,6 +93,12 @@ class Constants { licenseUrl: 'https://github.com/fluttercommunity/plus_plugins/blob/main/packages/connectivity_plus/connectivity_plus/LICENSE', sourceUrl: 'https://github.com/fluttercommunity/plus_plugins/tree/main/packages/connectivity_plus', ), + Dependency( + name: 'Device Info Plus', + license: 'BSD 3-Clause', + licenseUrl: 'https://github.com/fluttercommunity/plus_plugins/blob/main/packages/device_info_plus/device_info_plus/LICENSE', + sourceUrl: 'https://github.com/fluttercommunity/plus_plugins/tree/main/packages/device_info_plus', + ), Dependency( name: 'FlutterFire (Core, Crashlytics)', license: 'BSD 3-Clause', diff --git a/lib/widgets/about/about_page.dart b/lib/widgets/about/about_page.dart index 564d36509..7e124c114 100644 --- a/lib/widgets/about/about_page.dart +++ b/lib/widgets/about/about_page.dart @@ -1,4 +1,5 @@ import 'package:aves/widgets/about/app_ref.dart'; +import 'package:aves/widgets/about/bug_report.dart'; import 'package:aves/widgets/about/credits.dart'; import 'package:aves/widgets/about/licenses.dart'; import 'package:aves/widgets/about/update.dart'; @@ -27,6 +28,8 @@ class AboutPage extends StatelessWidget { AppReference(), Divider(), AboutUpdate(), + BugReport(), + Divider(), AboutCredits(), Divider(), ], diff --git a/lib/widgets/about/app_ref.dart b/lib/widgets/about/app_ref.dart index f995f0a7a..77f8d0b5f 100644 --- a/lib/widgets/about/app_ref.dart +++ b/lib/widgets/about/app_ref.dart @@ -1,6 +1,7 @@ import 'dart:ui'; -import 'package:aves/flutter_version.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/basic/link_chip.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_logo.dart'; @@ -29,8 +30,8 @@ class _AppReferenceState extends State { child: Column( children: [ _buildAvesLine(), - _buildFlutterLine(), const SizedBox(height: 16), + _buildLinks(), ], ), ); @@ -47,37 +48,45 @@ class _AppReferenceState extends State { return FutureBuilder( future: _packageInfoLoader, builder: (context, snapshot) { - return LinkChip( - leading: AvesLogo( - size: style.fontSize! * MediaQuery.textScaleFactorOf(context) * 1.25, - ), - text: '${context.l10n.appName} ${snapshot.data?.version}', - url: 'https://github.com/deckerst/aves', - textStyle: style, + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + AvesLogo( + size: style.fontSize! * MediaQuery.textScaleFactorOf(context) * 1.25, + ), + const SizedBox(width: 8), + Text( + '${context.l10n.appName} ${snapshot.data?.version}', + style: style, + ), + ], ); }, ); } - Widget _buildFlutterLine() { - final style = DefaultTextStyle.of(context).style; - final subColor = style.color!.withOpacity(.6); - - return Text.rich( - TextSpan( - children: [ - WidgetSpan( - child: Padding( - padding: const EdgeInsetsDirectional.only(end: 4), - child: FlutterLogo( - size: style.fontSize! * 1.25, - ), - ), + Widget _buildLinks() { + return Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 16, + children: [ + LinkChip( + leading: const Icon( + AIcons.github, + size: 24, ), - TextSpan(text: '${context.l10n.aboutFlutter} ${version['frameworkVersion']}'), - ], - ), - style: TextStyle(color: subColor), + text: context.l10n.aboutLinkSources, + url: Constants.avesGithub, + ), + LinkChip( + leading: const Icon( + AIcons.legal, + size: 22, + ), + text: context.l10n.aboutLinkLicense, + url: '${Constants.avesGithub}/blob/main/LICENSE', + ), + ], ); } } diff --git a/lib/widgets/about/bug_report.dart b/lib/widgets/about/bug_report.dart new file mode 100644 index 000000000..1d39146ca --- /dev/null +++ b/lib/widgets/about/bug_report.dart @@ -0,0 +1,163 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:aves/flutter_version.dart'; +import 'package:aves/ref/mime_types.dart'; +import 'package:aves/services/services.dart'; +import 'package:aves/utils/constants.dart'; +import 'package:aves/widgets/common/action_mixins/feedback.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:intl/intl.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class BugReport extends StatefulWidget { + const BugReport({Key? key}) : super(key: key); + + @override + _BugReportState createState() => _BugReportState(); +} + +class _BugReportState extends State with FeedbackMixin { + late Future _infoLoader; + bool _showInstructions = false; + + @override + void initState() { + super.initState(); + _infoLoader = _getInfo(); + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return ExpansionPanelList( + expansionCallback: (index, isExpanded) { + setState(() => _showInstructions = !isExpanded); + }, + expandedHeaderPadding: EdgeInsets.zero, + elevation: 0, + children: [ + ExpansionPanel( + headerBuilder: (context, isExpanded) => ConstrainedBox( + constraints: const BoxConstraints(minHeight: 48), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16), + alignment: AlignmentDirectional.centerStart, + child: Text(l10n.aboutBug, style: Constants.titleTextStyle), + ), + ), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildStep(1, l10n.aboutBugSaveLogInstruction, l10n.aboutBugSaveLogButton, _saveLogs), + _buildStep(2, l10n.aboutBugCopyInfoInstruction, l10n.aboutBugCopyInfoButton, _copySystemInfo), + FutureBuilder( + future: _infoLoader, + builder: (context, snapshot) { + final info = snapshot.data; + if (info == null) return const SizedBox(); + return Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.grey.shade800, + border: Border.all( + color: Colors.white, + ), + borderRadius: const BorderRadius.all(Radius.circular(8)), + ), + margin: const EdgeInsets.symmetric(vertical: 8), + child: SelectableText(info)); + }, + ), + _buildStep(3, l10n.aboutBugReportInstruction, l10n.aboutBugReportButton, _goToGithub), + const SizedBox(height: 16), + ], + ), + ), + isExpanded: _showInstructions, + canTapOnHeader: true, + backgroundColor: Colors.transparent, + ), + ], + ); + } + + Widget _buildStep(int step, String text, String buttonText, VoidCallback onPressed) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + border: Border.fromBorderSide(BorderSide( + color: Theme.of(context).accentColor, + width: AvesFilterChip.outlineWidth, + )), + shape: BoxShape.circle, + ), + child: Text('$step'), + ), + const SizedBox(width: 8), + Expanded(child: Text(text)), + const SizedBox(width: 8), + OutlinedButton( + onPressed: onPressed, + style: ButtonStyle( + side: MaterialStateProperty.all(BorderSide(color: Theme.of(context).accentColor)), + foregroundColor: MaterialStateProperty.all(Colors.white), + ), + child: Text(buttonText), + ) + ], + ), + ); + } + + Future _getInfo() async { + final packageInfo = await PackageInfo.fromPlatform(); + final androidInfo = await DeviceInfoPlugin().androidInfo; + final hasPlayServices = await availability.hasPlayServices; + return [ + 'Aves version: ${packageInfo.version} (Build ${packageInfo.buildNumber})', + 'Flutter version: ${version['frameworkVersion']} (Channel ${version['channel']})', + 'Android version: ${androidInfo.version.release} (SDK ${androidInfo.version.sdkInt})', + 'Device: ${androidInfo.manufacturer} ${androidInfo.model}', + 'Google Play services: ${hasPlayServices ? 'ready' : 'not available'}', + ].join('\n'); + } + + Future _saveLogs() async { + final result = await Process.run('logcat', ['-d']); + final logs = result.stdout; + final success = await storageService.createFile( + 'aves-logs-${DateFormat('yyyyMMdd_HHmmss').format(DateTime.now())}.txt', + MimeTypes.plainText, + Uint8List.fromList(utf8.encode(logs)), + ); + if (success != null) { + if (success) { + showFeedback(context, context.l10n.genericSuccessFeedback); + } else { + showFeedback(context, context.l10n.genericFailureFeedback); + } + } + } + + Future _copySystemInfo() async { + await Clipboard.setData(ClipboardData(text: await _infoLoader)); + showFeedback(context, context.l10n.genericSuccessFeedback); + } + + Future _goToGithub() async { + await launch('${Constants.avesGithub}/issues/new'); + } +} diff --git a/lib/widgets/about/update.dart b/lib/widgets/about/update.dart index 3c904f5b4..dbd9e2d4d 100644 --- a/lib/widgets/about/update.dart +++ b/lib/widgets/about/update.dart @@ -62,7 +62,7 @@ class _AboutUpdateState extends State { WidgetSpan( child: LinkChip( text: context.l10n.aboutUpdateGitHub, - url: 'https://github.com/deckerst/aves/releases', + url: '${Constants.avesGithub}/releases', textStyle: const TextStyle(fontWeight: FontWeight.bold), ), alignment: PlaceholderAlignment.middle, diff --git a/pubspec.lock b/pubspec.lock index 9d3f39bf0..f58765f0c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -190,6 +190,48 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.1" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + device_info_plus_linux: + dependency: transitive + description: + name: device_info_plus_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + device_info_plus_macos: + dependency: transitive + description: + name: device_info_plus_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + device_info_plus_web: + dependency: transitive + description: + name: device_info_plus_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + device_info_plus_windows: + dependency: transitive + description: + name: device_info_plus_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" equatable: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 40d2d8f1b..8fe4f0d65 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,6 +19,7 @@ dependencies: # TODO TLAD as of 2021/08/04, null safe version is pre-release custom_rounded_rectangle_border: '>=0.2.0-nullsafety.0' decorated_icon: + device_info_plus: equatable: event_bus: expansion_tile_card: