From 87ed955c22afd7ad1924376f43112e35d89bc563 Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Thu, 9 Mar 2023 17:26:09 -0800 Subject: [PATCH 01/47] 1 --- packages/devtools_app/lib/src/app.dart | 5 +++ .../screens/deep_link/deep_link_screen.dart | 37 +++++++++++++++++++ .../src/shared/primitives/simple_items.dart | 1 + .../devtools_app/lib/src/shared/ui/icons.dart | 1 + 4 files changed, 44 insertions(+) create mode 100644 packages/devtools_app/lib/src/screens/deep_link/deep_link_screen.dart diff --git a/packages/devtools_app/lib/src/app.dart b/packages/devtools_app/lib/src/app.dart index 08d08e9103b..39c0c3412a1 100644 --- a/packages/devtools_app/lib/src/app.dart +++ b/packages/devtools_app/lib/src/app.dart @@ -21,6 +21,7 @@ import 'screens/app_size/app_size_controller.dart'; import 'screens/app_size/app_size_screen.dart'; import 'screens/debugger/debugger_controller.dart'; import 'screens/debugger/debugger_screen.dart'; +import 'screens/deep_link/deep_link_screen.dart'; import 'screens/inspector/inspector_controller.dart'; import 'screens/inspector/inspector_screen.dart'; import 'screens/inspector/inspector_tree_controller.dart'; @@ -579,6 +580,10 @@ class CheckboxSetting extends StatelessWidget { /// be shown or hidden based on the [Screen.conditionalLibrary] provided. List get defaultScreens { return devtoolsScreens ??= [ + DevToolsScreen( + DeepLinkScreen(), + createController: (_) {}, + ), DevToolsScreen( InspectorScreen(), createController: (_) => InspectorController( diff --git a/packages/devtools_app/lib/src/screens/deep_link/deep_link_screen.dart b/packages/devtools_app/lib/src/screens/deep_link/deep_link_screen.dart new file mode 100644 index 00000000000..3ed551edff4 --- /dev/null +++ b/packages/devtools_app/lib/src/screens/deep_link/deep_link_screen.dart @@ -0,0 +1,37 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +import '../../shared/analytics/analytics.dart' as ga; +import '../../shared/globals.dart'; +import '../../shared/primitives/auto_dispose.dart'; +import '../../shared/primitives/simple_items.dart'; +import '../../shared/screen.dart'; +import '../../shared/theme.dart'; +import '../../shared/ui/icons.dart'; +import '../../shared/utils.dart'; + + + + +class DeepLinkScreen extends Screen { + DeepLinkScreen() + : super.conditional( + id: id, + worksOffline: true, + title: ScreenMetaData.deepLink.title, + icon: Octicons.link, + ); + + static final id = ScreenMetaData.deepLink.id; + + @override + String get docPageId => id; + + @override + Widget build(BuildContext context) => Container(); +} + + diff --git a/packages/devtools_app/lib/src/shared/primitives/simple_items.dart b/packages/devtools_app/lib/src/shared/primitives/simple_items.dart index 814ca75f8d4..b83c65f0065 100644 --- a/packages/devtools_app/lib/src/shared/primitives/simple_items.dart +++ b/packages/devtools_app/lib/src/shared/primitives/simple_items.dart @@ -31,6 +31,7 @@ enum ScreenMetaData { cpuProfiler('cpu-profiler', 'CPU Profiler'), memory('memory', 'Memory'), debugger('debugger', 'Debugger'), + deepLink('deep-link', 'Deep Link'), network('network', 'Network'), logging('logging', 'Logging'), provider('provider', 'Provider'), diff --git a/packages/devtools_app/lib/src/shared/ui/icons.dart b/packages/devtools_app/lib/src/shared/ui/icons.dart index cc19f9e2505..3815a639490 100644 --- a/packages/devtools_app/lib/src/shared/ui/icons.dart +++ b/packages/devtools_app/lib/src/shared/ui/icons.dart @@ -348,4 +348,5 @@ class Octicons { static const IconData package = IconData(61812, fontFamily: 'Octicons'); static const IconData dashboard = IconData(61733, fontFamily: 'Octicons'); static const IconData pulse = IconData(61823, fontFamily: 'Octicons'); + static const IconData link = IconData(128279, fontFamily: 'Octicons'); } From 139148eaf4bba33c84b446281e42ea4ff1c1fc56 Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Tue, 2 May 2023 11:48:11 -0700 Subject: [PATCH 02/47] http request --- .../screens/deep_link/deep_link_screen.dart | 58 +++++++++++++++++-- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/deep_link/deep_link_screen.dart b/packages/devtools_app/lib/src/screens/deep_link/deep_link_screen.dart index 3ed551edff4..5c26088b92f 100644 --- a/packages/devtools_app/lib/src/screens/deep_link/deep_link_screen.dart +++ b/packages/devtools_app/lib/src/screens/deep_link/deep_link_screen.dart @@ -9,12 +9,10 @@ import '../../shared/globals.dart'; import '../../shared/primitives/auto_dispose.dart'; import '../../shared/primitives/simple_items.dart'; import '../../shared/screen.dart'; -import '../../shared/theme.dart'; import '../../shared/ui/icons.dart'; -import '../../shared/utils.dart'; - - +import 'dart:convert'; +import 'package:http/http.dart' as http; class DeepLinkScreen extends Screen { DeepLinkScreen() @@ -31,7 +29,57 @@ class DeepLinkScreen extends Screen { String get docPageId => id; @override - Widget build(BuildContext context) => Container(); + Widget build(BuildContext context) { + return MyHomePage(); + + } } +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key}); + + @override + State createState() => _MyHomePageState(); +} +class _MyHomePageState extends State { + String responseBody = 'empty response'; + + void setResponse(String response) { + setState(() { + responseBody=response; + }); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [//v1/packageName/com.deeplinkexperiment.android:validateAppLinkDomain + FilledButton( + onPressed: () async { + var url = Uri.parse( + 'https://autopush-deeplinkassistant-pa.sandbox.googleapis.com/android/validation/v1/domain:validate?key=AIzaSyDVE6FP3GpwxgS4q8rbS7qaf6cAbxc_elc'); + var headers = {'Content-Type': 'application/json'}; + var payload = { + 'package_name': 'com.deeplinkexperiment.android', + 'app_link_domain': 'android.deeplink.store', + 'supplemental_sha256_cert_fingerprints': [ + '5A:33:EA:64:09:97:F2:F0:24:21:0F:B6:7A:A8:18:1C:18:A9:83:03:20:21:8F:9B:0B:98:BF:43:69:C2:AF:4A' + ] + }; + + var response = await http.post( + url, + headers: headers, + body: jsonEncode(payload), + ); + setResponse(response.body); + print(response.body); + }, + child: Text('validate'), + ), + Text(responseBody), + ], + ); + } +} From 703b180b1f37240e40e9e1da18470723e5b6c8de Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Thu, 27 Jul 2023 10:15:20 -0700 Subject: [PATCH 03/47] 1 --- .../screens/deep_link/deep_link_screen.dart | 279 ++++++++++++++++-- .../screens/deep_link/one_link_screen.dart | 196 ++++++++++++ 2 files changed, 443 insertions(+), 32 deletions(-) create mode 100644 packages/devtools_app/lib/src/screens/deep_link/one_link_screen.dart diff --git a/packages/devtools_app/lib/src/screens/deep_link/deep_link_screen.dart b/packages/devtools_app/lib/src/screens/deep_link/deep_link_screen.dart index 5c26088b92f..c0172b3c4d4 100644 --- a/packages/devtools_app/lib/src/screens/deep_link/deep_link_screen.dart +++ b/packages/devtools_app/lib/src/screens/deep_link/deep_link_screen.dart @@ -4,15 +4,109 @@ import 'package:flutter/material.dart'; -import '../../shared/analytics/analytics.dart' as ga; -import '../../shared/globals.dart'; -import '../../shared/primitives/auto_dispose.dart'; import '../../shared/primitives/simple_items.dart'; import '../../shared/screen.dart'; import '../../shared/ui/icons.dart'; -import 'dart:convert'; -import 'package:http/http.dart' as http; +import 'one_link_screen.dart'; + +const List paths = [ + '/shoes/..*', + '/Clothes/..*', + '/Toys/..*', + '/Jewelry/..*', + '/Watches/..* ', + '/Glasses/..*', +]; + +List allLinkDatas = [ + for (var path in paths) + LinkData( + os: 'Android, iOS', + domain: 'm.shopping.com', + path: path, + domainError: true, + pathError: path.contains('shoe'), + ), + for (var path in paths) + LinkData( + os: 'iOS', + domain: 'm.french.shopping.com', + path: path, + pathError: path.contains('shoe'), + ), + for (var path in paths) + LinkData( + os: 'Android', + domain: 'm.chinese.shopping.com', + path: path, + pathError: path.contains('shoe'), + ), +]; + +class LinkData { + LinkData({ + required this.os, + required this.domain, + required this.path, + this.scheme = 'Http://, Https://', + this.domainError = false, + this.pathError = false, + }); + + String os; + String path; + String domain; + String scheme; + bool domainError; + bool pathError; + String get searchLabel => os + path + domain + scheme; + + DataRow buildRow( + BuildContext context, { + MaterialStateProperty? color, + }) { + return DataRow( + color: color, + cells: [ + DataCell(Text(os)), + DataCell(Text(scheme)), + DataCell( + domainError + ? Row( + children: [ + Icon(Icons.error, + color: Theme.of(context).colorScheme.error), + SizedBox(width: 10), + Text(domain), + ], + ) + : Text(domain), + ), + DataCell( + pathError + ? Row( + children: [ + Icon(Icons.error, + color: Theme.of(context).colorScheme.error), + SizedBox(width: 10), + Text(path), + ], + ) + : Text(path), + ), + ], + ); + } + + LinkData mergeByDomain(LinkData? linkData) { + if (linkData == null) { + return this; + } + + return LinkData(os: os, domain: domain, path: '${linkData.path}\n$path'); + } +} class DeepLinkScreen extends Screen { DeepLinkScreen() @@ -31,7 +125,6 @@ class DeepLinkScreen extends Screen { @override Widget build(BuildContext context) { return MyHomePage(); - } } @@ -45,41 +138,163 @@ class MyHomePage extends StatefulWidget { class _MyHomePageState extends State { String responseBody = 'empty response'; + int? selectedRowIndex; + String searchContent = ''; + bool bundleByDomain = false; + void setResponse(String response) { setState(() { - responseBody=response; + responseBody = response; }); } @override Widget build(BuildContext context) { - return Column( - children: [//v1/packageName/com.deeplinkexperiment.android:validateAppLinkDomain - FilledButton( - onPressed: () async { - var url = Uri.parse( - 'https://autopush-deeplinkassistant-pa.sandbox.googleapis.com/android/validation/v1/domain:validate?key=AIzaSyDVE6FP3GpwxgS4q8rbS7qaf6cAbxc_elc'); - var headers = {'Content-Type': 'application/json'}; - var payload = { - 'package_name': 'com.deeplinkexperiment.android', - 'app_link_domain': 'android.deeplink.store', - 'supplemental_sha256_cert_fingerprints': [ - '5A:33:EA:64:09:97:F2:F0:24:21:0F:B6:7A:A8:18:1C:18:A9:83:03:20:21:8F:9B:0B:98:BF:43:69:C2:AF:4A' - ] - }; - - var response = await http.post( - url, - headers: headers, - body: jsonEncode(payload), - ); - setResponse(response.body); - print(response.body); - }, - child: Text('validate'), + if (selectedRowIndex != null) { + return OneLinkPage( + onBack: () { + setState(() { + selectedRowIndex = null; + }); + }, + ); + } + + return ListView( + children: [ + Row( + //mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded(child: buildTitle('All deep links', context)), + const Text('Bundle by domain'), + const SizedBox(width: 10), + SearchBar( + leading: const Icon(Icons.search), + onChanged: (value) { + setState(() { + searchContent = value; + }); + }, + constraints: BoxConstraints.tight(Size(200, 40)), + ), + ], ), - Text(responseBody), + SizedBox(height: 10), + buildDataTable(context), ], ); } + + Widget buildDataTable(BuildContext context) { + List linkDatas = searchContent.isNotEmpty + ? allLinkDatas + .where((linkData) => linkData.searchLabel.contains(searchContent)) + .toList() + : allLinkDatas; +//bundleByDomain + if (true) { + final Map bundleByDomainMap = {}; + for (var linkData in linkDatas) { + bundleByDomainMap[linkData.domain] = + linkData.mergeByDomain(bundleByDomainMap[linkData.domain]); + } + linkDatas = bundleByDomainMap.values.toList(); + } + + final List rows = [ + for (var i = 0; i < linkDatas.length; i++) + linkDatas[i].buildRow( + context, + color: i % 2 == 0 + ? MaterialStateProperty.all( + Theme.of(context).colorScheme.onSurface.withOpacity(0.2), + ) + : null, + ), + ]; + + return DataTable( + columns: [ + DataColumn( + label: Text('OS'), + onSort: (_, __) {}, + ), + DataColumn(label: Text('Scheme')), + DataColumn(label: Text('Domain')), + DataColumn(label: Text('Path')), + ], + rows: rows, + ); + } +} + +Widget buildTitle(String text, BuildContext context) { + final textTheme = Theme.of(context).textTheme; + return Text( + text, + style: textTheme.bodyLarge, + ); } + + + + // Widget buildBundledTable(BuildContext context) { + // List paths = [ + // '/shoes/..*', + // '/Clothes/..*', + // '/Toys/..*', + // '/Jewelry/..*', + // '/Watches/..* ', + // '/Glasses/..*', + // ]; + + // List domains = [ + // 'm.shopping.com', + // 'm.french.shopping.com', + // 'm.chinese.shopping.com', + // ]; + + // String allPath = ''; + // for (var j = 0; j < 6; j++) allPath += paths[j] + '\n'; + // print(allPath); + + // List rows = [ + // for (var i = 0; i < 3; i++) + // buildRow( + // domain: domains[i], + // path: allPath, //for (var j = 0; j < 6; j++)paths[j], + // color: i % 2 == 0 + // ? MaterialStateProperty.all( + // Theme.of(context).colorScheme.onSurface.withOpacity(0.3)) + // : null, + // ), + // ]; + // List searchList = [ + // for (var i = 0; i < 3; i++) + // for (var j = 0; j < 6; j++) + // 'AndroidIOSHttp://, https://${domains[i]}${paths[j]}' + // ]; + + // if (searchContent.isNotEmpty) { + // rows = rows + // .whereIndexed( + // (index, element) => searchList[index].contains(searchContent), + // ) + // .toList(); + // } + + // return DataTable( + // columns: [ + // DataColumn( + // label: Text('OS'), + // onSort: (_, __) {}, + // ), + // DataColumn(label: Text('Scheme')), + // DataColumn(label: Text('Domain')), + // DataColumn(label: Text('Path')), + // DataColumn(label: Text('issues')), + // ], + // rows: rows, + // dataRowMinHeight: 150, + // ); + // } \ No newline at end of file diff --git a/packages/devtools_app/lib/src/screens/deep_link/one_link_screen.dart b/packages/devtools_app/lib/src/screens/deep_link/one_link_screen.dart new file mode 100644 index 00000000000..7d2634a8d71 --- /dev/null +++ b/packages/devtools_app/lib/src/screens/deep_link/one_link_screen.dart @@ -0,0 +1,196 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +import '../../shared/analytics/analytics.dart' as ga; +import '../../shared/globals.dart'; +import '../../shared/primitives/auto_dispose.dart'; +import '../../shared/primitives/simple_items.dart'; +import '../../shared/screen.dart'; +import '../../shared/ui/icons.dart'; + +import 'dart:convert'; +import 'package:http/http.dart' as http; + +class OneLinkPage extends StatelessWidget { + const OneLinkPage({ + super.key, + required this.onBack, + }); + +// @override +// State createState() => _OneLinkPageState(); +// } + +// class _OneLinkPageState extends State { +// String responseBody = 'empty response'; + +// void setResponse(String response) { +// setState(() { +// responseBody = response; +// }); +// } + final VoidCallback onBack; + + @override + Widget build(BuildContext context) { + // return Column( + // children: [ + // //v1/packageName/com.deeplinkexperiment.android:validateAppLinkDomain + // FilledButton( + // onPressed: () async { + // var url = Uri.parse( + // 'https://autopush-deeplinkassistant-pa.sandbox.googleapis.com/android/validation/v1/domain:validate?key=AIzaSyDVE6FP3GpwxgS4q8rbS7qaf6cAbxc_elc'); + // var headers = {'Content-Type': 'application/json'}; + // var payload = { + // 'package_name': 'com.deeplinkexperiment.android', + // 'app_link_domain': 'android.deeplink.store', + // 'supplemental_sha256_cert_fingerprints': [ + // '5A:33:EA:64:09:97:F2:F0:24:21:0F:B6:7A:A8:18:1C:18:A9:83:03:20:21:8F:9B:0B:98:BF:43:69:C2:AF:4A' + // ] + // }; + + // var response = await http.post( + // url, + // headers: headers, + // body: jsonEncode(payload), + // ); + // setResponse(response.body); + // print(response.body); + // }, + // child: Text('validate'), + // ), + // Text(responseBody), + // ], + // ); + return ListView( + children: [ + Align( + alignment: Alignment.centerLeft, + child: IconButton(onPressed: onBack, icon: Icon(Icons.arrow_back)), + ), + buildTitle('Overview', context), + buildDataTable(context), + Row( + children: [ + buildCard(context), + SizedBox(width: 8), + buildCard(context), + ], + ), + buildTitle('Checks for your website', context), + buildWebcheckDataTable(context), + buildTitle('Checks for your app', context), + buildAppcheckDataTable(context), + ], + ); + } + + Widget buildDataTable(BuildContext context) { + final myRow = DataRow(cells: [ + DataCell(Text('Android, IOS')), + DataCell(Text('Http://, https://')), + DataCell(Text('Domain.com')), + DataCell(Text('/path/.*')), + DataCell(Text('0')), + ]); + + return DataTable( + columns: [ + DataColumn( + label: Text('OS'), + onSort: (_, __) {}, + ), + DataColumn(label: Text('Scheme')), + DataColumn(label: Text('Domain')), + DataColumn(label: Text('Path')), + DataColumn(label: Text('issues')), + ], + rows: [ + myRow, + ], + ); + } + + Widget buildWebcheckDataTable(BuildContext context) { + final myRow = DataRow(cells: [ + DataCell(Text('Android')), + DataCell(Text('Digital asset link file')), + DataCell(Text('2 check failed')), + ]); + + return DataTable( + columns: [ + DataColumn(label: Text('OS')), + DataColumn(label: Text('Checks')), + DataColumn(label: Text('issues')), + ], + rows: [ + myRow, + ], + ); + } + + Widget buildAppcheckDataTable(BuildContext context) { + final myRow = DataRow(cells: [ + DataCell(Text('Android')), + DataCell(Text('Intent filter')), + DataCell(Text('1 check failed')), + ]); + + return DataTable( + columns: [ + DataColumn(label: Text('OS')), + DataColumn(label: Text('Checks')), + DataColumn(label: Text('issues')), + ], + rows: [ + myRow, + ], + ); + } +} + +Widget buildTitle(String text, BuildContext context) { + final textTheme = Theme.of(context).textTheme; + return Text( + text, + style: textTheme.bodyLarge, + ); +} + +Widget buildCard(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + return Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.error, + color: Colors.red, //colorScheme.error, + ), + SizedBox(width: 8), + SizedBox( + width: 206, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('(Placeholder) 20 checks failed in total for this app'), + Text( + '(Placeholder) Auto fix and manual fix are both provided', + style: textTheme.bodyMedium, + ), + TextButton(onPressed: () {}, child: Text('Fix all issues')), + ], + ), + ), + ], + ), + ), + ); +} From 80160f1d836a61e85d7a9a96473c033e280f0d4f Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Wed, 2 Aug 2023 18:03:15 -0700 Subject: [PATCH 04/47] 1 --- packages/devtools_app/lib/src/app.dart | 6 +- .../screens/deep_link/deep_link_screen.dart | 300 ------------------ .../screens/deep_link/one_link_screen.dart | 196 ------------ .../deep_links_screen.dart | 223 ++++++++++++- 4 files changed, 218 insertions(+), 507 deletions(-) delete mode 100644 packages/devtools_app/lib/src/screens/deep_link/deep_link_screen.dart delete mode 100644 packages/devtools_app/lib/src/screens/deep_link/one_link_screen.dart diff --git a/packages/devtools_app/lib/src/app.dart b/packages/devtools_app/lib/src/app.dart index 5dab09447e9..f02e15e1e86 100644 --- a/packages/devtools_app/lib/src/app.dart +++ b/packages/devtools_app/lib/src/app.dart @@ -21,7 +21,6 @@ import 'screens/app_size/app_size_controller.dart'; import 'screens/app_size/app_size_screen.dart'; import 'screens/debugger/debugger_controller.dart'; import 'screens/debugger/debugger_screen.dart'; -import 'screens/deep_link/deep_link_screen.dart'; import 'screens/deep_link_validation/deep_links_controller.dart'; import 'screens/deep_link_validation/deep_links_screen.dart'; import 'screens/inspector/inspector_controller.dart'; @@ -497,10 +496,7 @@ List defaultScreens({ List sampleData = const [], }) { return devtoolsScreens ??= [ - DevToolsScreen( - DeepLinkScreen(), - createController: (_) {}, - ), + DevToolsScreen(HomeScreen(sampleData: sampleData)), DevToolsScreen( InspectorScreen(), diff --git a/packages/devtools_app/lib/src/screens/deep_link/deep_link_screen.dart b/packages/devtools_app/lib/src/screens/deep_link/deep_link_screen.dart deleted file mode 100644 index c0172b3c4d4..00000000000 --- a/packages/devtools_app/lib/src/screens/deep_link/deep_link_screen.dart +++ /dev/null @@ -1,300 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/material.dart'; - -import '../../shared/primitives/simple_items.dart'; -import '../../shared/screen.dart'; -import '../../shared/ui/icons.dart'; - -import 'one_link_screen.dart'; - -const List paths = [ - '/shoes/..*', - '/Clothes/..*', - '/Toys/..*', - '/Jewelry/..*', - '/Watches/..* ', - '/Glasses/..*', -]; - -List allLinkDatas = [ - for (var path in paths) - LinkData( - os: 'Android, iOS', - domain: 'm.shopping.com', - path: path, - domainError: true, - pathError: path.contains('shoe'), - ), - for (var path in paths) - LinkData( - os: 'iOS', - domain: 'm.french.shopping.com', - path: path, - pathError: path.contains('shoe'), - ), - for (var path in paths) - LinkData( - os: 'Android', - domain: 'm.chinese.shopping.com', - path: path, - pathError: path.contains('shoe'), - ), -]; - -class LinkData { - LinkData({ - required this.os, - required this.domain, - required this.path, - this.scheme = 'Http://, Https://', - this.domainError = false, - this.pathError = false, - }); - - String os; - String path; - String domain; - String scheme; - bool domainError; - bool pathError; - String get searchLabel => os + path + domain + scheme; - - DataRow buildRow( - BuildContext context, { - MaterialStateProperty? color, - }) { - return DataRow( - color: color, - cells: [ - DataCell(Text(os)), - DataCell(Text(scheme)), - DataCell( - domainError - ? Row( - children: [ - Icon(Icons.error, - color: Theme.of(context).colorScheme.error), - SizedBox(width: 10), - Text(domain), - ], - ) - : Text(domain), - ), - DataCell( - pathError - ? Row( - children: [ - Icon(Icons.error, - color: Theme.of(context).colorScheme.error), - SizedBox(width: 10), - Text(path), - ], - ) - : Text(path), - ), - ], - ); - } - - LinkData mergeByDomain(LinkData? linkData) { - if (linkData == null) { - return this; - } - - return LinkData(os: os, domain: domain, path: '${linkData.path}\n$path'); - } -} - -class DeepLinkScreen extends Screen { - DeepLinkScreen() - : super.conditional( - id: id, - worksOffline: true, - title: ScreenMetaData.deepLink.title, - icon: Octicons.link, - ); - - static final id = ScreenMetaData.deepLink.id; - - @override - String get docPageId => id; - - @override - Widget build(BuildContext context) { - return MyHomePage(); - } -} - -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key}); - - @override - State createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - String responseBody = 'empty response'; - - int? selectedRowIndex; - String searchContent = ''; - bool bundleByDomain = false; - - void setResponse(String response) { - setState(() { - responseBody = response; - }); - } - - @override - Widget build(BuildContext context) { - if (selectedRowIndex != null) { - return OneLinkPage( - onBack: () { - setState(() { - selectedRowIndex = null; - }); - }, - ); - } - - return ListView( - children: [ - Row( - //mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded(child: buildTitle('All deep links', context)), - const Text('Bundle by domain'), - const SizedBox(width: 10), - SearchBar( - leading: const Icon(Icons.search), - onChanged: (value) { - setState(() { - searchContent = value; - }); - }, - constraints: BoxConstraints.tight(Size(200, 40)), - ), - ], - ), - SizedBox(height: 10), - buildDataTable(context), - ], - ); - } - - Widget buildDataTable(BuildContext context) { - List linkDatas = searchContent.isNotEmpty - ? allLinkDatas - .where((linkData) => linkData.searchLabel.contains(searchContent)) - .toList() - : allLinkDatas; -//bundleByDomain - if (true) { - final Map bundleByDomainMap = {}; - for (var linkData in linkDatas) { - bundleByDomainMap[linkData.domain] = - linkData.mergeByDomain(bundleByDomainMap[linkData.domain]); - } - linkDatas = bundleByDomainMap.values.toList(); - } - - final List rows = [ - for (var i = 0; i < linkDatas.length; i++) - linkDatas[i].buildRow( - context, - color: i % 2 == 0 - ? MaterialStateProperty.all( - Theme.of(context).colorScheme.onSurface.withOpacity(0.2), - ) - : null, - ), - ]; - - return DataTable( - columns: [ - DataColumn( - label: Text('OS'), - onSort: (_, __) {}, - ), - DataColumn(label: Text('Scheme')), - DataColumn(label: Text('Domain')), - DataColumn(label: Text('Path')), - ], - rows: rows, - ); - } -} - -Widget buildTitle(String text, BuildContext context) { - final textTheme = Theme.of(context).textTheme; - return Text( - text, - style: textTheme.bodyLarge, - ); -} - - - - // Widget buildBundledTable(BuildContext context) { - // List paths = [ - // '/shoes/..*', - // '/Clothes/..*', - // '/Toys/..*', - // '/Jewelry/..*', - // '/Watches/..* ', - // '/Glasses/..*', - // ]; - - // List domains = [ - // 'm.shopping.com', - // 'm.french.shopping.com', - // 'm.chinese.shopping.com', - // ]; - - // String allPath = ''; - // for (var j = 0; j < 6; j++) allPath += paths[j] + '\n'; - // print(allPath); - - // List rows = [ - // for (var i = 0; i < 3; i++) - // buildRow( - // domain: domains[i], - // path: allPath, //for (var j = 0; j < 6; j++)paths[j], - // color: i % 2 == 0 - // ? MaterialStateProperty.all( - // Theme.of(context).colorScheme.onSurface.withOpacity(0.3)) - // : null, - // ), - // ]; - // List searchList = [ - // for (var i = 0; i < 3; i++) - // for (var j = 0; j < 6; j++) - // 'AndroidIOSHttp://, https://${domains[i]}${paths[j]}' - // ]; - - // if (searchContent.isNotEmpty) { - // rows = rows - // .whereIndexed( - // (index, element) => searchList[index].contains(searchContent), - // ) - // .toList(); - // } - - // return DataTable( - // columns: [ - // DataColumn( - // label: Text('OS'), - // onSort: (_, __) {}, - // ), - // DataColumn(label: Text('Scheme')), - // DataColumn(label: Text('Domain')), - // DataColumn(label: Text('Path')), - // DataColumn(label: Text('issues')), - // ], - // rows: rows, - // dataRowMinHeight: 150, - // ); - // } \ No newline at end of file diff --git a/packages/devtools_app/lib/src/screens/deep_link/one_link_screen.dart b/packages/devtools_app/lib/src/screens/deep_link/one_link_screen.dart deleted file mode 100644 index 7d2634a8d71..00000000000 --- a/packages/devtools_app/lib/src/screens/deep_link/one_link_screen.dart +++ /dev/null @@ -1,196 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/material.dart'; - -import '../../shared/analytics/analytics.dart' as ga; -import '../../shared/globals.dart'; -import '../../shared/primitives/auto_dispose.dart'; -import '../../shared/primitives/simple_items.dart'; -import '../../shared/screen.dart'; -import '../../shared/ui/icons.dart'; - -import 'dart:convert'; -import 'package:http/http.dart' as http; - -class OneLinkPage extends StatelessWidget { - const OneLinkPage({ - super.key, - required this.onBack, - }); - -// @override -// State createState() => _OneLinkPageState(); -// } - -// class _OneLinkPageState extends State { -// String responseBody = 'empty response'; - -// void setResponse(String response) { -// setState(() { -// responseBody = response; -// }); -// } - final VoidCallback onBack; - - @override - Widget build(BuildContext context) { - // return Column( - // children: [ - // //v1/packageName/com.deeplinkexperiment.android:validateAppLinkDomain - // FilledButton( - // onPressed: () async { - // var url = Uri.parse( - // 'https://autopush-deeplinkassistant-pa.sandbox.googleapis.com/android/validation/v1/domain:validate?key=AIzaSyDVE6FP3GpwxgS4q8rbS7qaf6cAbxc_elc'); - // var headers = {'Content-Type': 'application/json'}; - // var payload = { - // 'package_name': 'com.deeplinkexperiment.android', - // 'app_link_domain': 'android.deeplink.store', - // 'supplemental_sha256_cert_fingerprints': [ - // '5A:33:EA:64:09:97:F2:F0:24:21:0F:B6:7A:A8:18:1C:18:A9:83:03:20:21:8F:9B:0B:98:BF:43:69:C2:AF:4A' - // ] - // }; - - // var response = await http.post( - // url, - // headers: headers, - // body: jsonEncode(payload), - // ); - // setResponse(response.body); - // print(response.body); - // }, - // child: Text('validate'), - // ), - // Text(responseBody), - // ], - // ); - return ListView( - children: [ - Align( - alignment: Alignment.centerLeft, - child: IconButton(onPressed: onBack, icon: Icon(Icons.arrow_back)), - ), - buildTitle('Overview', context), - buildDataTable(context), - Row( - children: [ - buildCard(context), - SizedBox(width: 8), - buildCard(context), - ], - ), - buildTitle('Checks for your website', context), - buildWebcheckDataTable(context), - buildTitle('Checks for your app', context), - buildAppcheckDataTable(context), - ], - ); - } - - Widget buildDataTable(BuildContext context) { - final myRow = DataRow(cells: [ - DataCell(Text('Android, IOS')), - DataCell(Text('Http://, https://')), - DataCell(Text('Domain.com')), - DataCell(Text('/path/.*')), - DataCell(Text('0')), - ]); - - return DataTable( - columns: [ - DataColumn( - label: Text('OS'), - onSort: (_, __) {}, - ), - DataColumn(label: Text('Scheme')), - DataColumn(label: Text('Domain')), - DataColumn(label: Text('Path')), - DataColumn(label: Text('issues')), - ], - rows: [ - myRow, - ], - ); - } - - Widget buildWebcheckDataTable(BuildContext context) { - final myRow = DataRow(cells: [ - DataCell(Text('Android')), - DataCell(Text('Digital asset link file')), - DataCell(Text('2 check failed')), - ]); - - return DataTable( - columns: [ - DataColumn(label: Text('OS')), - DataColumn(label: Text('Checks')), - DataColumn(label: Text('issues')), - ], - rows: [ - myRow, - ], - ); - } - - Widget buildAppcheckDataTable(BuildContext context) { - final myRow = DataRow(cells: [ - DataCell(Text('Android')), - DataCell(Text('Intent filter')), - DataCell(Text('1 check failed')), - ]); - - return DataTable( - columns: [ - DataColumn(label: Text('OS')), - DataColumn(label: Text('Checks')), - DataColumn(label: Text('issues')), - ], - rows: [ - myRow, - ], - ); - } -} - -Widget buildTitle(String text, BuildContext context) { - final textTheme = Theme.of(context).textTheme; - return Text( - text, - style: textTheme.bodyLarge, - ); -} - -Widget buildCard(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final textTheme = Theme.of(context).textTheme; - return Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon( - Icons.error, - color: Colors.red, //colorScheme.error, - ), - SizedBox(width: 8), - SizedBox( - width: 206, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('(Placeholder) 20 checks failed in total for this app'), - Text( - '(Placeholder) Auto fix and manual fix are both provided', - style: textTheme.bodyMedium, - ), - TextButton(onPressed: () {}, child: Text('Fix all issues')), - ], - ), - ), - ], - ), - ), - ); -} diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart index 93a504876d1..604616c7ebb 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart @@ -3,9 +3,116 @@ // found in the LICENSE file. import 'package:flutter/material.dart'; - import '../../shared/screen.dart'; +const List paths = [ + '/shoes/..*', + '/Clothes/..*', + '/Toys/..*', + '/Jewelry/..*', + '/Watches/..* ', + '/Glasses/..*', +]; + +List allLinkDatas = [ + for (var path in paths) + LinkData( + os: 'Android, iOS', + domain: 'm.shopping.com', + path: path, + domainError: true, + pathError: path.contains('shoe'), + ), + for (var path in paths) + LinkData( + os: 'iOS', + domain: 'm.french.shopping.com', + path: path, + pathError: path.contains('shoe'), + ), + for (var path in paths) + LinkData( + os: 'Android', + domain: 'm.chinese.shopping.com', + path: path, + pathError: path.contains('shoe'), + ), +]; + +class LinkData { + LinkData({ + required this.os, + required this.domain, + required this.path, + this.scheme = 'Http://, Https://', + this.domainError = false, + this.pathError = false, + }); + + String os; + String path; + String domain; + String scheme; + bool domainError; + bool pathError; + String get searchLabel => (os + path + domain + scheme).toLowerCase(); + + DataRow buildRow( + BuildContext context, { + MaterialStateProperty? color, + }) { + return DataRow( + color: color, + cells: [ + DataCell(Text(os)), + DataCell(Text(scheme)), + DataCell( + domainError + ? Row( + children: [ + Icon( + Icons.error, + color: Theme.of(context).colorScheme.error, + ), + const SizedBox(width: 10), + Text(domain), + ], + ) + : Text(domain), + ), + DataCell( + pathError + ? Row( + children: [ + Icon( + Icons.error, + color: Theme.of(context).colorScheme.error, + ), + const SizedBox(width: 10), + Text(path), + ], + ) + : Text(path), + ), + ], + ); + } + + LinkData mergeByDomain(LinkData? linkData) { + if (linkData == null) { + return this; + } + + return LinkData( + os: os, + domain: domain, + path: '${linkData.path}\n$path', + domainError: domainError || linkData.domainError, + // pathError: pathError || linkData.pathError, + ); + } +} + class DeepLinksScreen extends Screen { DeepLinksScreen() : super.conditional( @@ -18,14 +125,118 @@ class DeepLinksScreen extends Screen { static final id = ScreenMetaData.deepLinks.id; - // TODO(https://github.com/flutter/devtools/issues/6013): write documentation. - // @override - // String get docPageId => id; + @override + String get docPageId => id; @override Widget build(BuildContext context) { - return const Center( - child: Text('TODO: build deep link validation tool'), + return const DeepLinkPage(); + } +} + +class DeepLinkPage extends StatefulWidget { + const DeepLinkPage({super.key}); + + @override + State createState() => _DeepLinkPageState(); +} + +class _DeepLinkPageState extends State { + String responseBody = 'empty response'; + + int? selectedRowIndex; + String searchContent = ''; + bool bundleByDomain = true; + + void setBundleByDomain(bool shouldBundleByDomain) { + setState(() { + bundleByDomain = shouldBundleByDomain; + }); + } + + void setResponse(String response) { + setState(() { + responseBody = response; + }); + } + + @override + Widget build(BuildContext context) { + return ListView( + children: [ + Row( + children: [ + Expanded(child: buildTitle('All deep links', context)), + InkWell( + onTap: () { + setBundleByDomain(!bundleByDomain); + }, + child: const Text('Bundle by domain'), + ), + const SizedBox(width: 10), + SearchBar( + leading: const Icon(Icons.search), + onChanged: (value) { + setState(() { + searchContent = value.toLowerCase(); + }); + }, + constraints: BoxConstraints.tight(const Size(200, 40)), + ), + ], + ), + const SizedBox(height: 10), + buildDataTable(context), + ], ); } + + Widget buildDataTable(BuildContext context) { + List linkDatas = searchContent.isNotEmpty + ? allLinkDatas + .where((linkData) => linkData.searchLabel.contains(searchContent)) + .toList() + : allLinkDatas; + + if (bundleByDomain) { + final Map bundleByDomainMap = {}; + for (var linkData in linkDatas) { + bundleByDomainMap[linkData.domain] = + linkData.mergeByDomain(bundleByDomainMap[linkData.domain]); + } + linkDatas = bundleByDomainMap.values.toList(); + } + + final List rows = [ + for (var i = 0; i < linkDatas.length; i++) + linkDatas[i].buildRow( + context, + color: i % 2 == 0 + ? MaterialStateProperty.all( + Theme.of(context).colorScheme.onSurface.withOpacity(0.2), + ) + : null, + ), + ]; + + return DataTable( + columns: const [ + DataColumn(label: Text('OS')), + DataColumn(label: Text('Scheme')), + DataColumn(label: Text('Domain')), + DataColumn(label: Text('Path')), + ], + dataRowMinHeight: bundleByDomain ? 150 : null, + dataRowMaxHeight: bundleByDomain ? 150 : null, + rows: rows, + ); + } +} + +Widget buildTitle(String text, BuildContext context) { + final textTheme = Theme.of(context).textTheme; + return Text( + text, + style: textTheme.bodyLarge, + ); } From 9d9f5f990ae6c0403c84b66b0e96ac335aaed0d5 Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Wed, 2 Aug 2023 18:12:17 -0700 Subject: [PATCH 05/47] 2 --- packages/devtools_app/lib/src/app.dart | 1 - .../screens/deep_link_validation/deep_links_screen.dart | 8 ++++---- packages/devtools_app/lib/src/shared/ui/icons.dart | 1 - 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/devtools_app/lib/src/app.dart b/packages/devtools_app/lib/src/app.dart index f02e15e1e86..6c76807f860 100644 --- a/packages/devtools_app/lib/src/app.dart +++ b/packages/devtools_app/lib/src/app.dart @@ -496,7 +496,6 @@ List defaultScreens({ List sampleData = const [], }) { return devtoolsScreens ??= [ - DevToolsScreen(HomeScreen(sampleData: sampleData)), DevToolsScreen( InspectorScreen(), diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart index 604616c7ebb..80076b95c66 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart @@ -125,8 +125,9 @@ class DeepLinksScreen extends Screen { static final id = ScreenMetaData.deepLinks.id; - @override - String get docPageId => id; + // TODO(https://github.com/flutter/devtools/issues/6013): write documentation. + // @override + // String get docPageId => id; @override Widget build(BuildContext context) { @@ -143,10 +144,9 @@ class DeepLinkPage extends StatefulWidget { class _DeepLinkPageState extends State { String responseBody = 'empty response'; - int? selectedRowIndex; String searchContent = ''; - bool bundleByDomain = true; + bool bundleByDomain = false; void setBundleByDomain(bool shouldBundleByDomain) { setState(() { diff --git a/packages/devtools_app/lib/src/shared/ui/icons.dart b/packages/devtools_app/lib/src/shared/ui/icons.dart index 28711d318a8..9b8568109e3 100644 --- a/packages/devtools_app/lib/src/shared/ui/icons.dart +++ b/packages/devtools_app/lib/src/shared/ui/icons.dart @@ -330,5 +330,4 @@ class Octicons { static const IconData package = IconData(61812, fontFamily: 'Octicons'); static const IconData dashboard = IconData(61733, fontFamily: 'Octicons'); static const IconData pulse = IconData(61823, fontFamily: 'Octicons'); - static const IconData link = IconData(128279, fontFamily: 'Octicons'); } From 5b641494caddb4fe7f891a308ccaef99d7c0978a Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Wed, 2 Aug 2023 18:14:35 -0700 Subject: [PATCH 06/47] Update deep_links_screen.dart --- .../lib/src/screens/deep_link_validation/deep_links_screen.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart index 80076b95c66..298a50588b8 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart @@ -108,7 +108,6 @@ class LinkData { domain: domain, path: '${linkData.path}\n$path', domainError: domainError || linkData.domainError, - // pathError: pathError || linkData.pathError, ); } } From de2ecc629c1741d53487020ca06069aade498db3 Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Thu, 10 Aug 2023 13:09:49 -0700 Subject: [PATCH 07/47] 2 --- .../deep_links_controller.dart | 49 +++- .../deep_links_model.dart | 86 +++++++ .../deep_links_screen.dart | 225 ++++-------------- .../deep_link_validation/fake_data.dart | 35 +++ 4 files changed, 220 insertions(+), 175 deletions(-) create mode 100644 packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart create mode 100644 packages/devtools_app/lib/src/screens/deep_link_validation/fake_data.dart diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart index 2cc26a4416e..69fb10ecf94 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart @@ -2,4 +2,51 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -class DeepLinksController {} +import 'package:flutter/foundation.dart'; + +import 'deep_links_model.dart'; +import 'fake_data.dart'; + + +class DeepLinksController { + + final _linkDatasNotifier = ValueNotifier>(allLinkDatas); + ValueListenable> get linkDatasNotifier => _linkDatasNotifier; + List get linkDatas => _linkDatasNotifier.value; + + final _searchContentNotifier = ValueNotifier(''); + ValueListenable get searchContentNotifier => _searchContentNotifier; + set searchContent(String content) { + _searchContentNotifier.value = content; + _updateLinks(); + } + + final _bundleByDomainNotifier = ValueNotifier(false); + ValueListenable get bundleByDomainNotifier => _bundleByDomainNotifier; + set bundleByDomain(bool value) { + _bundleByDomainNotifier.value = value; + _updateLinks(); + } + bool get bundleByDomain => _bundleByDomainNotifier.value; + + void _updateLinks() { + final searchContent = _searchContentNotifier.value; + List linkDatas = searchContent.isNotEmpty + ? allLinkDatas + .where( + (linkData) => linkData.searchLabel.contains(searchContent), + ) + .toList() + : allLinkDatas; + + if (bundleByDomain) { + final Map bundleByDomainMap = {}; + for (var linkData in linkDatas) { + bundleByDomainMap[linkData.domain] = + linkData.mergeByDomain(bundleByDomainMap[linkData.domain]); + } + linkDatas = bundleByDomainMap.values.toList(); + } + _linkDatasNotifier.value = linkDatas; + } +} diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart new file mode 100644 index 00000000000..75ea100e2d9 --- /dev/null +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart @@ -0,0 +1,86 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +import '../../shared/theme.dart'; + +/// Contains all data relevant to a deep link. +class LinkData { + LinkData({ + required this.os, + required this.domain, + required this.paths, + this.scheme = 'Http://, Https://', + this.domainError = false, + this.pathError = false, + }); + + final String os; + final List paths; + final String domain; + final String scheme; + final bool domainError; + final bool pathError; + + String get searchLabel => (os + paths.join() + domain + scheme).toLowerCase(); + + LinkData mergeByDomain(LinkData? linkData) { + if (linkData == null) { + return this; + } + + return LinkData( + os: os, + domain: domain, + paths: [...paths, ...linkData.paths], + domainError: domainError || linkData.domainError, + ); + } +} + +DataRow buildRow( + BuildContext context, + LinkData data, { + MaterialStateProperty? color, +}) { + return DataRow( + color: color, + cells: [ + DataCell(Text(data.os)), + DataCell(Text(data.scheme)), + DataCell( + Row( + children: [ + if (data.domainError) + Padding( + padding: const EdgeInsets.only(right: denseSpacing), + child: Icon( + Icons.error, + color: Theme.of(context).colorScheme.error, + ), + ), + Text(data.domain), + ], + ), + ), + DataCell( + Row( + children: [ + if (data.pathError) + Padding( + padding: const EdgeInsets.only(right: denseSpacing), + child: Icon( + Icons.error, + color: Theme.of(context).colorScheme.error, + ), + ), + const SizedBox(width: 10), + Text(data.paths.join('\n')), + ], + ), + ), + ], + ); +} diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart index 298a50588b8..a61c9d1f07f 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart @@ -3,114 +3,12 @@ // found in the LICENSE file. import 'package:flutter/material.dart'; -import '../../shared/screen.dart'; -const List paths = [ - '/shoes/..*', - '/Clothes/..*', - '/Toys/..*', - '/Jewelry/..*', - '/Watches/..* ', - '/Glasses/..*', -]; +import '../../../devtools_app.dart'; +import 'deep_links_controller.dart'; +import 'deep_links_model.dart'; -List allLinkDatas = [ - for (var path in paths) - LinkData( - os: 'Android, iOS', - domain: 'm.shopping.com', - path: path, - domainError: true, - pathError: path.contains('shoe'), - ), - for (var path in paths) - LinkData( - os: 'iOS', - domain: 'm.french.shopping.com', - path: path, - pathError: path.contains('shoe'), - ), - for (var path in paths) - LinkData( - os: 'Android', - domain: 'm.chinese.shopping.com', - path: path, - pathError: path.contains('shoe'), - ), -]; - -class LinkData { - LinkData({ - required this.os, - required this.domain, - required this.path, - this.scheme = 'Http://, Https://', - this.domainError = false, - this.pathError = false, - }); - - String os; - String path; - String domain; - String scheme; - bool domainError; - bool pathError; - String get searchLabel => (os + path + domain + scheme).toLowerCase(); - - DataRow buildRow( - BuildContext context, { - MaterialStateProperty? color, - }) { - return DataRow( - color: color, - cells: [ - DataCell(Text(os)), - DataCell(Text(scheme)), - DataCell( - domainError - ? Row( - children: [ - Icon( - Icons.error, - color: Theme.of(context).colorScheme.error, - ), - const SizedBox(width: 10), - Text(domain), - ], - ) - : Text(domain), - ), - DataCell( - pathError - ? Row( - children: [ - Icon( - Icons.error, - color: Theme.of(context).colorScheme.error, - ), - const SizedBox(width: 10), - Text(path), - ], - ) - : Text(path), - ), - ], - ); - } - - LinkData mergeByDomain(LinkData? linkData) { - if (linkData == null) { - return this; - } - - return LinkData( - os: os, - domain: domain, - path: '${linkData.path}\n$path', - domainError: domainError || linkData.domainError, - ); - } -} +const bundledDataRowHeight = 150.0; class DeepLinksScreen extends Screen { DeepLinksScreen() @@ -141,22 +39,15 @@ class DeepLinkPage extends StatefulWidget { State createState() => _DeepLinkPageState(); } -class _DeepLinkPageState extends State { - String responseBody = 'empty response'; - int? selectedRowIndex; - String searchContent = ''; - bool bundleByDomain = false; - - void setBundleByDomain(bool shouldBundleByDomain) { - setState(() { - bundleByDomain = shouldBundleByDomain; - }); - } - - void setResponse(String response) { - setState(() { - responseBody = response; - }); +class _DeepLinkPageState extends State + with + AutoDisposeMixin, + SingleTickerProviderStateMixin, + ProvidedControllerMixin { + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (!initController()) return; } @override @@ -165,77 +56,63 @@ class _DeepLinkPageState extends State { children: [ Row( children: [ - Expanded(child: buildTitle('All deep links', context)), + Expanded( + child: Text( + 'All deep links', + style: Theme.of(context).textTheme.bodyLarge, + ), + ), InkWell( onTap: () { - setBundleByDomain(!bundleByDomain); + controller.bundleByDomain = !controller.bundleByDomain; }, child: const Text('Bundle by domain'), ), - const SizedBox(width: 10), + const SizedBox(width: denseSpacing), SearchBar( leading: const Icon(Icons.search), onChanged: (value) { - setState(() { - searchContent = value.toLowerCase(); - }); + controller.searchContent = value; }, constraints: BoxConstraints.tight(const Size(200, 40)), ), ], ), - const SizedBox(height: 10), + const SizedBox(height: denseSpacing), buildDataTable(context), ], ); } Widget buildDataTable(BuildContext context) { - List linkDatas = searchContent.isNotEmpty - ? allLinkDatas - .where((linkData) => linkData.searchLabel.contains(searchContent)) - .toList() - : allLinkDatas; - - if (bundleByDomain) { - final Map bundleByDomainMap = {}; - for (var linkData in linkDatas) { - bundleByDomainMap[linkData.domain] = - linkData.mergeByDomain(bundleByDomainMap[linkData.domain]); - } - linkDatas = bundleByDomainMap.values.toList(); - } - - final List rows = [ - for (var i = 0; i < linkDatas.length; i++) - linkDatas[i].buildRow( - context, - color: i % 2 == 0 - ? MaterialStateProperty.all( - Theme.of(context).colorScheme.onSurface.withOpacity(0.2), - ) - : null, - ), - ]; - - return DataTable( - columns: const [ - DataColumn(label: Text('OS')), - DataColumn(label: Text('Scheme')), - DataColumn(label: Text('Domain')), - DataColumn(label: Text('Path')), - ], - dataRowMinHeight: bundleByDomain ? 150 : null, - dataRowMaxHeight: bundleByDomain ? 150 : null, - rows: rows, + return ValueListenableBuilder>( + valueListenable: controller.linkDatasNotifier, + builder: (context, linkDatas, _) { + final bool bundleByDomain = controller.bundleByDomain; + + final List rows = [ + for (var i = 0; i < linkDatas.length; i++) + buildRow( + context, + linkDatas[i], + color: MaterialStateProperty.all( + alternatingColorForIndex(i, Theme.of(context).colorScheme), + ), + ), + ]; + + return DataTable( + columns: const [ + DataColumn(label: Text('OS')), + DataColumn(label: Text('Scheme')), + DataColumn(label: Text('Domain')), + DataColumn(label: Text('Path')), + ], + dataRowMinHeight: bundleByDomain ? bundledDataRowHeight : null, + dataRowMaxHeight: bundleByDomain ? bundledDataRowHeight : null, + rows: rows, + ); + }, ); } } - -Widget buildTitle(String text, BuildContext context) { - final textTheme = Theme.of(context).textTheme; - return Text( - text, - style: textTheme.bodyLarge, - ); -} diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/fake_data.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/fake_data.dart new file mode 100644 index 00000000000..d1cf2ac70ae --- /dev/null +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/fake_data.dart @@ -0,0 +1,35 @@ +import 'deep_links_model.dart'; + +const paths = [ + '/shoes/..*', + '/Clothes/..*', + '/Toys/..*', + '/Jewelry/..*', + '/Watches/..* ', + '/Glasses/..*', +]; + +final allLinkDatas = [ + for (var path in paths) + LinkData( + os: 'Android, iOS', + domain: 'm.shopping.com', + paths: [path], + domainError: true, + pathError: path.contains('shoe'), + ), + for (var path in paths) + LinkData( + os: 'iOS', + domain: 'm.french.shopping.com', + paths: [path], + pathError: path.contains('shoe'), + ), + for (var path in paths) + LinkData( + os: 'Android', + domain: 'm.chinese.shopping.com', + paths: [path], + pathError: path.contains('shoe'), + ), +]; From 9d2892b1bc9eb3497fae45d39efc269eed0ab0d4 Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Thu, 10 Aug 2023 13:29:25 -0700 Subject: [PATCH 08/47] 3 --- .../deep_links_model.dart | 49 ------------------- .../deep_links_screen.dart | 49 ++++++++++++++++++- .../deep_link_validation/fake_data.dart | 5 ++ 3 files changed, 53 insertions(+), 50 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart index 75ea100e2d9..b7c76206205 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart @@ -2,10 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/material.dart'; - -import '../../shared/theme.dart'; - /// Contains all data relevant to a deep link. class LinkData { LinkData({ @@ -39,48 +35,3 @@ class LinkData { ); } } - -DataRow buildRow( - BuildContext context, - LinkData data, { - MaterialStateProperty? color, -}) { - return DataRow( - color: color, - cells: [ - DataCell(Text(data.os)), - DataCell(Text(data.scheme)), - DataCell( - Row( - children: [ - if (data.domainError) - Padding( - padding: const EdgeInsets.only(right: denseSpacing), - child: Icon( - Icons.error, - color: Theme.of(context).colorScheme.error, - ), - ), - Text(data.domain), - ], - ), - ), - DataCell( - Row( - children: [ - if (data.pathError) - Padding( - padding: const EdgeInsets.only(right: denseSpacing), - child: Icon( - Icons.error, - color: Theme.of(context).colorScheme.error, - ), - ), - const SizedBox(width: 10), - Text(data.paths.join('\n')), - ], - ), - ), - ], - ); -} diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart index a61c9d1f07f..5101ca314c4 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart @@ -74,7 +74,9 @@ class _DeepLinkPageState extends State onChanged: (value) { controller.searchContent = value; }, - constraints: BoxConstraints.tight(const Size(200, 40)), + constraints: BoxConstraints.tight( + Size(defaultSearchFieldWidth, defaultTextFieldHeight), + ), ), ], ), @@ -116,3 +118,48 @@ class _DeepLinkPageState extends State ); } } + +DataRow buildRow( + BuildContext context, + LinkData data, { + MaterialStateProperty? color, +}) { + return DataRow( + color: color, + cells: [ + DataCell(Text(data.os)), + DataCell(Text(data.scheme)), + DataCell( + Row( + children: [ + if (data.domainError) + Padding( + padding: const EdgeInsets.only(right: denseSpacing), + child: Icon( + Icons.error, + color: Theme.of(context).colorScheme.error, + ), + ), + Text(data.domain), + ], + ), + ), + DataCell( + Row( + children: [ + if (data.pathError) + Padding( + padding: const EdgeInsets.only(right: denseSpacing), + child: Icon( + Icons.error, + color: Theme.of(context).colorScheme.error, + ), + ), + const SizedBox(width: 10), + Text(data.paths.join('\n')), + ], + ), + ), + ], + ); +} diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/fake_data.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/fake_data.dart index d1cf2ac70ae..49a4da544f4 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/fake_data.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/fake_data.dart @@ -1,5 +1,10 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + import 'deep_links_model.dart'; +/// Fake data for demo usage. Will replece this file with real deep link data. const paths = [ '/shoes/..*', '/Clothes/..*', From d849f14b3b1758aef15a66a5561553427d6c0d22 Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Thu, 10 Aug 2023 15:44:02 -0700 Subject: [PATCH 09/47] update datatable --- .../deep_links_model.dart | 2 + .../deep_links_screen.dart | 142 +++++++++--------- 2 files changed, 76 insertions(+), 68 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart index b7c76206205..52b427aa811 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart @@ -27,6 +27,8 @@ class LinkData { return this; } + assert(domain == linkData.domain); + return LinkData( os: os, domain: domain, diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart index 5101ca314c4..abb23faeebc 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart @@ -81,85 +81,91 @@ class _DeepLinkPageState extends State ], ), const SizedBox(height: denseSpacing), - buildDataTable(context), + ValueListenableBuilder>( + valueListenable: controller.linkDatasNotifier, + builder: (context, linkDatas, _) => _DataTable( + linkDatas: linkDatas, + bundleByDomain: controller.bundleByDomain, + ), + ), ], ); } +} - Widget buildDataTable(BuildContext context) { - return ValueListenableBuilder>( - valueListenable: controller.linkDatasNotifier, - builder: (context, linkDatas, _) { - final bool bundleByDomain = controller.bundleByDomain; +class _DataTable extends StatelessWidget { + const _DataTable({required this.bundleByDomain, required this.linkDatas}); + final bool bundleByDomain; + final List linkDatas; - final List rows = [ - for (var i = 0; i < linkDatas.length; i++) - buildRow( - context, - linkDatas[i], - color: MaterialStateProperty.all( - alternatingColorForIndex(i, Theme.of(context).colorScheme), - ), - ), - ]; + @override + Widget build(BuildContext context) { + final List rows = [ + for (var i = 0; i < linkDatas.length; i++) + _buildRow( + context, + linkDatas[i], + color: MaterialStateProperty.all( + alternatingColorForIndex(i, Theme.of(context).colorScheme), + ), + ), + ]; - return DataTable( - columns: const [ - DataColumn(label: Text('OS')), - DataColumn(label: Text('Scheme')), - DataColumn(label: Text('Domain')), - DataColumn(label: Text('Path')), - ], - dataRowMinHeight: bundleByDomain ? bundledDataRowHeight : null, - dataRowMaxHeight: bundleByDomain ? bundledDataRowHeight : null, - rows: rows, - ); - }, + return DataTable( + columns: const [ + DataColumn(label: Text('OS')), + DataColumn(label: Text('Scheme')), + DataColumn(label: Text('Domain')), + DataColumn(label: Text('Path')), + ], + dataRowMinHeight: bundleByDomain ? bundledDataRowHeight : null, + dataRowMaxHeight: bundleByDomain ? bundledDataRowHeight : null, + rows: rows, ); } -} -DataRow buildRow( - BuildContext context, - LinkData data, { - MaterialStateProperty? color, -}) { - return DataRow( - color: color, - cells: [ - DataCell(Text(data.os)), - DataCell(Text(data.scheme)), - DataCell( - Row( - children: [ - if (data.domainError) - Padding( - padding: const EdgeInsets.only(right: denseSpacing), - child: Icon( - Icons.error, - color: Theme.of(context).colorScheme.error, + DataRow _buildRow( + BuildContext context, + LinkData data, { + MaterialStateProperty? color, + }) { + return DataRow( + color: color, + cells: [ + DataCell(Text(data.os)), + DataCell(Text(data.scheme)), + DataCell( + Row( + children: [ + if (data.domainError) + Padding( + padding: const EdgeInsets.only(right: denseSpacing), + child: Icon( + Icons.error, + color: Theme.of(context).colorScheme.error, + ), ), - ), - Text(data.domain), - ], + Text(data.domain), + ], + ), ), - ), - DataCell( - Row( - children: [ - if (data.pathError) - Padding( - padding: const EdgeInsets.only(right: denseSpacing), - child: Icon( - Icons.error, - color: Theme.of(context).colorScheme.error, + DataCell( + Row( + children: [ + if (data.pathError) + Padding( + padding: const EdgeInsets.only(right: denseSpacing), + child: Icon( + Icons.error, + color: Theme.of(context).colorScheme.error, + ), ), - ), - const SizedBox(width: 10), - Text(data.paths.join('\n')), - ], + const SizedBox(width: 10), + Text(data.paths.join('\n')), + ], + ), ), - ), - ], - ); + ], + ); + } } From cfea32d88f7952f1bb7941f8c78d86114ff658c6 Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Thu, 21 Sep 2023 16:00:19 -0700 Subject: [PATCH 10/47] Update deep_links_model.dart 2 Update deep_links_controller.dart new mock --- .../deep_links_controller.dart | 61 ++-- .../deep_links_model.dart | 210 +++++++++++- .../deep_links_screen.dart | 316 +++++++++++++----- .../deep_link_validation/fake_data.dart | 12 +- .../devtools_app/lib/src/shared/theme.dart | 1 + 5 files changed, 467 insertions(+), 133 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart index 69fb10ecf94..2686d2414c6 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart @@ -7,46 +7,63 @@ import 'package:flutter/foundation.dart'; import 'deep_links_model.dart'; import 'fake_data.dart'; - class DeepLinksController { - - final _linkDatasNotifier = ValueNotifier>(allLinkDatas); ValueListenable> get linkDatasNotifier => _linkDatasNotifier; + ValueListenable get searchContentNotifier => _searchContentNotifier; + ValueListenable get showSpitScreenNotifier => _showSpitScreenNotifier; + List get linkDatas => _linkDatasNotifier.value; + bool get showSpitScreen => _showSpitScreenNotifier.value; + + final selectedLink = ValueNotifier(null); + + final _linkDatasNotifier = ValueNotifier>(allLinkDatas); final _searchContentNotifier = ValueNotifier(''); - ValueListenable get searchContentNotifier => _searchContentNotifier; - set searchContent(String content) { - _searchContentNotifier.value = content; - _updateLinks(); + final _showSpitScreenNotifier = ValueNotifier(false); + + set showSpitScreen(bool value) { + _showSpitScreenNotifier.value = value; } - final _bundleByDomainNotifier = ValueNotifier(false); - ValueListenable get bundleByDomainNotifier => _bundleByDomainNotifier; - set bundleByDomain(bool value) { - _bundleByDomainNotifier.value = value; + set searchContent(String content) { + _searchContentNotifier.value = content; _updateLinks(); } - bool get bundleByDomain => _bundleByDomainNotifier.value; void _updateLinks() { final searchContent = _searchContentNotifier.value; - List linkDatas = searchContent.isNotEmpty + final List linkDatas = searchContent.isNotEmpty ? allLinkDatas .where( - (linkData) => linkData.searchLabel.contains(searchContent), + (linkData) => linkData.matchesSearchToken( + RegExp( + searchContent, + caseSensitive: false, + ), + ), ) .toList() : allLinkDatas; - if (bundleByDomain) { - final Map bundleByDomainMap = {}; - for (var linkData in linkDatas) { - bundleByDomainMap[linkData.domain] = - linkData.mergeByDomain(bundleByDomainMap[linkData.domain]); - } - linkDatas = bundleByDomainMap.values.toList(); - } _linkDatasNotifier.value = linkDatas; } + + List get getLinkDatasByPath { + final linkDatasByPath = {}; + + for (var linkData in _linkDatasNotifier.value) { + linkDatasByPath[linkData.path] = linkData; + } + return linkDatasByPath.values.toList(); + } + + List get getLinkDatasByDomain { + final linkDatasByDomain = {}; + + for (var linkData in _linkDatasNotifier.value) { + linkDatasByDomain[linkData.domain] = linkData; + } + return linkDatasByDomain.values.toList(); + } } diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart index 52b427aa811..d702146bee3 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart @@ -3,37 +3,211 @@ // found in the LICENSE file. /// Contains all data relevant to a deep link. -class LinkData { +/// +import 'package:flutter/material.dart'; + +import '../../shared/primitives/utils.dart'; +import '../../shared/table/table.dart'; +import '../../shared/table/table_data.dart'; +import '../../shared/theme.dart'; +import '../../shared/ui/search.dart'; +import '../../shared/utils.dart'; + +const kDeeplinkTableCellDefaultWidth = 200.0; + +class LinkData with SearchableDataMixin { LinkData({ - required this.os, required this.domain, - required this.paths, - this.scheme = 'Http://, Https://', + required this.path, + required this.os, + this.scheme = const ['Http://', 'Https://'], this.domainError = false, this.pathError = false, }); - final String os; - final List paths; + final String path; final String domain; - final String scheme; + final List os; + final List scheme; final bool domainError; final bool pathError; - String get searchLabel => (os + paths.join() + domain + scheme).toLowerCase(); + @override + bool matchesSearchToken(RegExp regExpSearch) { + return (domain.caseInsensitiveContains(regExpSearch) == true) || + (path.caseInsensitiveContains(regExpSearch) == true); + } - LinkData mergeByDomain(LinkData? linkData) { - if (linkData == null) { - return this; - } + @override + String toString() => 'LinkData($domain $path)'; +} - assert(domain == linkData.domain); +class _ErrorAwareText extends StatelessWidget { + const _ErrorAwareText({ + required this.text, + required this.isError, + }); + final String text; + final bool isError; - return LinkData( - os: os, - domain: domain, - paths: [...paths, ...linkData.paths], - domainError: domainError || linkData.domainError, + @override + Widget build(BuildContext context) { + return Row( + children: [ + if (isError) + Padding( + padding: const EdgeInsets.only(right: denseSpacing), + child: Icon( + Icons.error, + color: Theme.of(context).colorScheme.error, + size: defaultIconSize, + ), + ), + const SizedBox(width: denseSpacing), + Text( + text, + overflow: TextOverflow.ellipsis, + ), + ], ); } } + +class DomainColumn extends ColumnData + implements ColumnRenderer { + DomainColumn() + : super( + 'Domain', + fixedWidthPx: scaleByFontFactor(kDeeplinkTableCellDefaultWidth), + ); + + @override + bool get supportsSorting => true; + + @override + String getValue(LinkData dataObject) => dataObject.domain; + + @override + Widget build( + BuildContext context, + LinkData dataObject, { + bool isRowSelected = false, + VoidCallback? onPressed, + }) { + return _ErrorAwareText( + isError: dataObject.domainError, text: dataObject.domain); + } +} + +class PathColumn extends ColumnData + implements ColumnRenderer { + PathColumn() + : super( + 'Path', + fixedWidthPx: scaleByFontFactor(kDeeplinkTableCellDefaultWidth), + ); + + @override + bool get supportsSorting => true; + + @override + String getValue(LinkData dataObject) => dataObject.path; + + @override + Widget build( + BuildContext context, + LinkData dataObject, { + bool isRowSelected = false, + VoidCallback? onPressed, + }) { + return _ErrorAwareText( + isError: dataObject.pathError, text: dataObject.path); + } +} + +class SchemeColumn extends ColumnData { + SchemeColumn() + : super( + 'Scheme', + fixedWidthPx: scaleByFontFactor(kDeeplinkTableCellDefaultWidth), + ); + + @override + String getValue(LinkData dataObject) => dataObject.scheme.join(','); +} + +class OSColumn extends ColumnData { + OSColumn() + : super( + 'OS', + fixedWidthPx: scaleByFontFactor(kDeeplinkTableCellDefaultWidth), + ); + + @override + String getValue(LinkData dataObject) => dataObject.os.join(','); +} + +class StatusColumn extends ColumnData + implements ColumnRenderer { + StatusColumn() + : super( + 'Status', + fixedWidthPx: scaleByFontFactor(kDeeplinkTableCellDefaultWidth), + ); + + @override + String getValue(LinkData dataObject) { + if (dataObject.domainError) { + return 'Failed domain checks'; + } else if (dataObject.pathError) { + return 'Failed path checks'; + } else { + return 'No issues found'; + } + } + + @override + Widget build( + BuildContext context, + LinkData dataObject, { + bool isRowSelected = false, + VoidCallback? onPressed, + }) { + if (dataObject.domainError || dataObject.pathError) { + return Text( + getValue(dataObject), + overflow: TextOverflow.ellipsis, + style: TextStyle(color: Theme.of(context).colorScheme.error), + ); + } else { + return const Text( + 'No issues found', + style: TextStyle(color: Color.fromARGB(255, 156, 233, 195)), + overflow: TextOverflow.ellipsis, + ); + } + } +} + +// TODO: implement this column. +class NavigationColumn extends ColumnData + implements ColumnRenderer { + NavigationColumn() + : super( + '', + fixedWidthPx: scaleByFontFactor(40), + ); + + @override + String getValue(LinkData dataObject) => ''; + + @override + Widget build( + BuildContext context, + LinkData dataObject, { + bool isRowSelected = false, + VoidCallback? onPressed, + }) { + return const Icon(Icons.arrow_forward); + } +} diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart index abb23faeebc..2bdd5281b0a 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart @@ -4,11 +4,21 @@ import 'package:flutter/material.dart'; -import '../../../devtools_app.dart'; +import '../../shared/primitives/auto_dispose.dart'; +import '../../shared/primitives/utils.dart'; +import '../../shared/screen.dart'; +import '../../shared/table/table.dart'; +import '../../shared/table/table_data.dart'; +import '../../shared/theme.dart'; +import '../../shared/utils.dart'; import 'deep_links_controller.dart'; import 'deep_links_model.dart'; -const bundledDataRowHeight = 150.0; +enum TableView { + domainView, + pathView, + singleUrlView, +} class DeepLinksScreen extends Screen { DeepLinksScreen() @@ -52,119 +62,251 @@ class _DeepLinkPageState extends State @override Widget build(BuildContext context) { - return ListView( - children: [ - Row( - children: [ - Expanded( - child: Text( - 'All deep links', - style: Theme.of(context).textTheme.bodyLarge, + return DefaultTabController( + length: TableView.values.length, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + 'All deep links', + style: Theme.of(context).textTheme.bodyLarge, + ), ), - ), - InkWell( - onTap: () { - controller.bundleByDomain = !controller.bundleByDomain; - }, - child: const Text('Bundle by domain'), - ), - const SizedBox(width: denseSpacing), - SearchBar( - leading: const Icon(Icons.search), - onChanged: (value) { - controller.searchContent = value; - }, - constraints: BoxConstraints.tight( - Size(defaultSearchFieldWidth, defaultTextFieldHeight), + const SizedBox(width: denseSpacing), + SearchBar( + leading: const Icon(Icons.search), + hintText: 'Search a URL, domain or path', + onChanged: (value) { + controller.searchContent = value; + }, + constraints: BoxConstraints.tight( + Size(wideSearchFieldWidth, defaultTextFieldHeight), + ), + ), + ], + ), + const SizedBox(height: denseSpacing), + const TabBar( + tabs: [ + Text('Domain view'), + Text('Path view'), + Text('Single URL view'), + ], + tabAlignment: TabAlignment.start, + isScrollable: true, + ), + Expanded( + child: ValueListenableBuilder>( + valueListenable: controller.linkDatasNotifier, + builder: (context, linkDatas, _) => ValueListenableBuilder( + valueListenable: controller.showSpitScreenNotifier, + builder: (context, showSpitScreen, _) => TabBarView( + children: [ + _DataTableWithValidationDetails( + tableView: TableView.domainView, + linkDatas: controller.getLinkDatasByDomain, + controller: controller, + showSpitScreen: showSpitScreen, + ), + _DataTableWithValidationDetails( + tableView: TableView.pathView, + linkDatas: controller.getLinkDatasByPath, + controller: controller, + showSpitScreen: showSpitScreen, + ), + _DataTableWithValidationDetails( + tableView: TableView.singleUrlView, + linkDatas: linkDatas, + controller: controller, + showSpitScreen: showSpitScreen, + ), + ], + ), ), ), - ], - ), - const SizedBox(height: denseSpacing), - ValueListenableBuilder>( - valueListenable: controller.linkDatasNotifier, - builder: (context, linkDatas, _) => _DataTable( + ), + ], + ), + ); + } +} + +class _DataTableWithValidationDetails extends StatelessWidget { + const _DataTableWithValidationDetails({ + required this.linkDatas, + required this.tableView, + required this.controller, + required this.showSpitScreen, + }); + final List linkDatas; + final TableView tableView; + final DeepLinksController controller; + final bool showSpitScreen; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: _DataTable( + tableView: tableView, linkDatas: linkDatas, - bundleByDomain: controller.bundleByDomain, + controller: controller, ), ), + if (showSpitScreen) + Expanded( + child: ValueListenableBuilder( + valueListenable: controller.selectedLink, + builder: (context, selectedLink, _) => _ValidationDetailScreen( + tableView: tableView, + linkData: selectedLink!, + controller: controller, + ), + ), + ), ], ); } } class _DataTable extends StatelessWidget { - const _DataTable({required this.bundleByDomain, required this.linkDatas}); - final bool bundleByDomain; + const _DataTable({ + required this.linkDatas, + required this.tableView, + required this.controller, + }); final List linkDatas; + final TableView tableView; + final DeepLinksController controller; @override Widget build(BuildContext context) { - final List rows = [ - for (var i = 0; i < linkDatas.length; i++) - _buildRow( - context, - linkDatas[i], - color: MaterialStateProperty.all( - alternatingColorForIndex(i, Theme.of(context).colorScheme), - ), - ), - ]; + final ColumnData domain = DomainColumn(); + final ColumnData path = PathColumn(); - return DataTable( - columns: const [ - DataColumn(label: Text('OS')), - DataColumn(label: Text('Scheme')), - DataColumn(label: Text('Domain')), - DataColumn(label: Text('Path')), + return FlatTable( + keyFactory: (node) => ValueKey(node.toString), + data: linkDatas, + dataKey: 'deep-links', + autoScrollContent: true, + columns: [ + if (tableView != TableView.pathView) domain, + if (tableView != TableView.domainView) path, + SchemeColumn(), + OSColumn(), + if (!controller.showSpitScreen) ...[ + StatusColumn(), + NavigationColumn(), + ], ], - dataRowMinHeight: bundleByDomain ? bundledDataRowHeight : null, - dataRowMaxHeight: bundleByDomain ? bundledDataRowHeight : null, - rows: rows, + selectionNotifier: controller.selectedLink, + defaultSortColumn: tableView == TableView.pathView ? path : domain, + defaultSortDirection: SortDirection.ascending, + onItemSelected: (item) => controller.showSpitScreen = true, ); } +} + +class _ValidationDetailScreen extends StatelessWidget { + const _ValidationDetailScreen({ + required this.linkData, + required this.tableView, + required this.controller, + }); - DataRow _buildRow( - BuildContext context, - LinkData data, { - MaterialStateProperty? color, - }) { - return DataRow( - color: color, - cells: [ - DataCell(Text(data.os)), - DataCell(Text(data.scheme)), - DataCell( + final LinkData linkData; + final TableView tableView; + final DeepLinksController controller; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: largeSpacing), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - if (data.domainError) - Padding( - padding: const EdgeInsets.only(right: denseSpacing), - child: Icon( - Icons.error, - color: Theme.of(context).colorScheme.error, - ), - ), - Text(data.domain), + const Text('Selected Deep link validation details'), + IconButton( + onPressed: () => controller.showSpitScreen = false, + icon: const Icon(Icons.close), + ), ], ), - ), - DataCell( - Row( - children: [ - if (data.pathError) - Padding( - padding: const EdgeInsets.only(right: denseSpacing), - child: Icon( - Icons.error, - color: Theme.of(context).colorScheme.error, - ), + Text( + 'This tool assistants helps you diagnose Universal Links, App Links,' + ' and Custom Schemes in your app. Web check are done for the web association' + ' file on your website. App checks are done for the intent filters in' + ' the manifest and info.plist file, routing issues, URL format, etc.', + style: Theme.of(context).textTheme.bodySmall, + ), + const Text('Domain check'), + _DomainCheckTable(linkData: linkData), + ], + ), + ); + } +} + +class _DomainCheckTable extends StatelessWidget { + const _DomainCheckTable({ + required this.linkData, + }); + + final LinkData linkData; + + @override + Widget build(BuildContext context) { + return DataTable( + columns: const [ + DataColumn(label: Text('OS')), + DataColumn(label: Text('Issue type')), + DataColumn(label: Text('Status')), + ], + rows: [ + if (linkData.os.contains('Android')) + DataRow( + cells: [ + const DataCell(Text('Android')), + const DataCell(Text('Digital assets link file')), + DataCell( + linkData.domainError + ? Text( + 'Check failed', + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), + ) + : const Text( + 'No issues found', + style: TextStyle( + // TODO: Update devtool colorscheme and use color from there. + color: Color.fromARGB(255, 156, 233, 195), + ), + ), + ), + ], + ), + if (linkData.os.contains('iOS')) + const DataRow( + cells: [ + DataCell(Text('iOS')), + DataCell(Text('Apple-App-Site-Association file')), + DataCell( + Text( + 'No issues found', + // TODO: Update devtool colorscheme and use color from there. + style: TextStyle(color: Color.fromARGB(255, 156, 233, 195)), ), - const SizedBox(width: 10), - Text(data.paths.join('\n')), + ), ], ), - ), ], ); } diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/fake_data.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/fake_data.dart index 49a4da544f4..2e16a18489f 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/fake_data.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/fake_data.dart @@ -17,24 +17,24 @@ const paths = [ final allLinkDatas = [ for (var path in paths) LinkData( - os: 'Android, iOS', + os: ['Android', 'iOS'], domain: 'm.shopping.com', - paths: [path], + path: path, domainError: true, pathError: path.contains('shoe'), ), for (var path in paths) LinkData( - os: 'iOS', + os: ['iOS'], domain: 'm.french.shopping.com', - paths: [path], + path: path, pathError: path.contains('shoe'), ), for (var path in paths) LinkData( - os: 'Android', + os: ['Android'], domain: 'm.chinese.shopping.com', - paths: [path], + path: path, pathError: path.contains('shoe'), ), ]; diff --git a/packages/devtools_app/lib/src/shared/theme.dart b/packages/devtools_app/lib/src/shared/theme.dart index 7b57e8bd0b2..d42e0d63ea2 100644 --- a/packages/devtools_app/lib/src/shared/theme.dart +++ b/packages/devtools_app/lib/src/shared/theme.dart @@ -230,6 +230,7 @@ double get tableIconSize => scaleByFontFactor(12.0); const defaultIconSizeBeforeScaling = 16.0; const defaultActionsIconSizeBeforeScaling = 20.0; +const largeSpacing = 32.0; const defaultSpacing = 16.0; const tabBarSpacing = 14.0; const intermediateSpacing = 12.0; From 63f9a2d703eae62f95e752d043bf9c9dae05408d Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Thu, 21 Sep 2023 16:05:56 -0700 Subject: [PATCH 11/47] Update deep_links_screen.dart --- .../deep_links_screen.dart | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart index 2bdd5281b0a..ff4d3e4f25e 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart @@ -14,7 +14,7 @@ import '../../shared/utils.dart'; import 'deep_links_controller.dart'; import 'deep_links_model.dart'; -enum TableView { +enum TableViewType { domainView, pathView, singleUrlView, @@ -63,7 +63,7 @@ class _DeepLinkPageState extends State @override Widget build(BuildContext context) { return DefaultTabController( - length: TableView.values.length, + length: TableViewType.values.length, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -106,19 +106,19 @@ class _DeepLinkPageState extends State builder: (context, showSpitScreen, _) => TabBarView( children: [ _DataTableWithValidationDetails( - tableView: TableView.domainView, + tableView: TableViewType.domainView, linkDatas: controller.getLinkDatasByDomain, controller: controller, showSpitScreen: showSpitScreen, ), _DataTableWithValidationDetails( - tableView: TableView.pathView, + tableView: TableViewType.pathView, linkDatas: controller.getLinkDatasByPath, controller: controller, showSpitScreen: showSpitScreen, ), _DataTableWithValidationDetails( - tableView: TableView.singleUrlView, + tableView: TableViewType.singleUrlView, linkDatas: linkDatas, controller: controller, showSpitScreen: showSpitScreen, @@ -142,7 +142,7 @@ class _DataTableWithValidationDetails extends StatelessWidget { required this.showSpitScreen, }); final List linkDatas; - final TableView tableView; + final TableViewType tableView; final DeepLinksController controller; final bool showSpitScreen; @@ -180,7 +180,7 @@ class _DataTable extends StatelessWidget { required this.controller, }); final List linkDatas; - final TableView tableView; + final TableViewType tableView; final DeepLinksController controller; @override @@ -193,9 +193,9 @@ class _DataTable extends StatelessWidget { data: linkDatas, dataKey: 'deep-links', autoScrollContent: true, - columns: [ - if (tableView != TableView.pathView) domain, - if (tableView != TableView.domainView) path, + columns: [ + if (tableView != TableViewType.pathView) domain, + if (tableView != TableViewType.domainView) path, SchemeColumn(), OSColumn(), if (!controller.showSpitScreen) ...[ @@ -204,7 +204,7 @@ class _DataTable extends StatelessWidget { ], ], selectionNotifier: controller.selectedLink, - defaultSortColumn: tableView == TableView.pathView ? path : domain, + defaultSortColumn: tableView == TableViewType.pathView ? path : domain, defaultSortDirection: SortDirection.ascending, onItemSelected: (item) => controller.showSpitScreen = true, ); @@ -219,7 +219,7 @@ class _ValidationDetailScreen extends StatelessWidget { }); final LinkData linkData; - final TableView tableView; + final TableViewType tableView; final DeepLinksController controller; @override From d3b4ea80e6db182be732abc4e4c8f2d01c5ab47b Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Thu, 21 Sep 2023 16:21:50 -0700 Subject: [PATCH 12/47] update --- .../deep_links_controller.dart | 54 ++++++++----------- .../deep_links_screen.dart | 8 +-- 2 files changed, 25 insertions(+), 37 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart index 2686d2414c6..961e82be83e 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart @@ -8,28 +8,29 @@ import 'deep_links_model.dart'; import 'fake_data.dart'; class DeepLinksController { - ValueListenable> get linkDatasNotifier => _linkDatasNotifier; - ValueListenable get searchContentNotifier => _searchContentNotifier; - ValueListenable get showSpitScreenNotifier => _showSpitScreenNotifier; + bool get showSpitScreen => showSpitScreenNotifier.value; - List get linkDatas => _linkDatasNotifier.value; + List get getLinkDatasByPath { + final linkDatasByPath = {}; + for (var linkData in linkDatasNotifier.value) { + linkDatasByPath[linkData.path] = linkData; + } + return linkDatasByPath.values.toList(); + } - bool get showSpitScreen => _showSpitScreenNotifier.value; + List get getLinkDatasByDomain { + final linkDatasByDomain = {}; + for (var linkData in linkDatasNotifier.value) { + linkDatasByDomain[linkData.domain] = linkData; + } + return linkDatasByDomain.values.toList(); + } final selectedLink = ValueNotifier(null); + final linkDatasNotifier = ValueNotifier>(allLinkDatas); + final showSpitScreenNotifier = ValueNotifier(false); - final _linkDatasNotifier = ValueNotifier>(allLinkDatas); final _searchContentNotifier = ValueNotifier(''); - final _showSpitScreenNotifier = ValueNotifier(false); - - set showSpitScreen(bool value) { - _showSpitScreenNotifier.value = value; - } - - set searchContent(String content) { - _searchContentNotifier.value = content; - _updateLinks(); - } void _updateLinks() { final searchContent = _searchContentNotifier.value; @@ -46,24 +47,11 @@ class DeepLinksController { .toList() : allLinkDatas; - _linkDatasNotifier.value = linkDatas; - } - - List get getLinkDatasByPath { - final linkDatasByPath = {}; - - for (var linkData in _linkDatasNotifier.value) { - linkDatasByPath[linkData.path] = linkData; - } - return linkDatasByPath.values.toList(); + linkDatasNotifier.value = linkDatas; } - List get getLinkDatasByDomain { - final linkDatasByDomain = {}; - - for (var linkData in _linkDatasNotifier.value) { - linkDatasByDomain[linkData.domain] = linkData; - } - return linkDatasByDomain.values.toList(); + set searchContent(String content) { + _searchContentNotifier.value = content; + _updateLinks(); } } diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart index ff4d3e4f25e..53853680c13 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart @@ -206,7 +206,7 @@ class _DataTable extends StatelessWidget { selectionNotifier: controller.selectedLink, defaultSortColumn: tableView == TableViewType.pathView ? path : domain, defaultSortDirection: SortDirection.ascending, - onItemSelected: (item) => controller.showSpitScreen = true, + onItemSelected: (item) => controller.showSpitScreenNotifier.value = true, ); } } @@ -234,7 +234,7 @@ class _ValidationDetailScreen extends StatelessWidget { children: [ const Text('Selected Deep link validation details'), IconButton( - onPressed: () => controller.showSpitScreen = false, + onPressed: () => controller.showSpitScreenNotifier.value = false, icon: const Icon(Icons.close), ), ], @@ -247,7 +247,7 @@ class _ValidationDetailScreen extends StatelessWidget { style: Theme.of(context).textTheme.bodySmall, ), const Text('Domain check'), - _DomainCheckTable(linkData: linkData), + Expanded(child: _DomainCheckTable(linkData: linkData)), ], ), ); @@ -287,7 +287,7 @@ class _DomainCheckTable extends StatelessWidget { 'No issues found', style: TextStyle( // TODO: Update devtool colorscheme and use color from there. - color: Color.fromARGB(255, 156, 233, 195), + color: Color.fromARGB(255, 156, 233, 195), ), ), ), From 808e07b3d988e2ec82cbb4b152202ccff9bc1bf8 Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Thu, 21 Sep 2023 16:26:15 -0700 Subject: [PATCH 13/47] green --- .../deep_link_validation/deep_links_screen.dart | 15 ++++++++------- packages/devtools_app/lib/src/shared/theme.dart | 2 ++ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart index 53853680c13..8d1549bd952 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart @@ -234,7 +234,8 @@ class _ValidationDetailScreen extends StatelessWidget { children: [ const Text('Selected Deep link validation details'), IconButton( - onPressed: () => controller.showSpitScreenNotifier.value = false, + onPressed: () => + controller.showSpitScreenNotifier.value = false, icon: const Icon(Icons.close), ), ], @@ -283,26 +284,26 @@ class _DomainCheckTable extends StatelessWidget { color: Theme.of(context).colorScheme.error, ), ) - : const Text( + : Text( 'No issues found', style: TextStyle( // TODO: Update devtool colorscheme and use color from there. - color: Color.fromARGB(255, 156, 233, 195), + color: Theme.of(context).colorScheme.green, ), ), ), ], ), if (linkData.os.contains('iOS')) - const DataRow( + DataRow( cells: [ - DataCell(Text('iOS')), - DataCell(Text('Apple-App-Site-Association file')), + const DataCell(Text('iOS')), + const DataCell(Text('Apple-App-Site-Association file')), DataCell( Text( 'No issues found', // TODO: Update devtool colorscheme and use color from there. - style: TextStyle(color: Color.fromARGB(255, 156, 233, 195)), + style: TextStyle(color: Theme.of(context).colorScheme.green), ), ), ], diff --git a/packages/devtools_app/lib/src/shared/theme.dart b/packages/devtools_app/lib/src/shared/theme.dart index d42e0d63ea2..3862590461e 100644 --- a/packages/devtools_app/lib/src/shared/theme.dart +++ b/packages/devtools_app/lib/src/shared/theme.dart @@ -302,6 +302,8 @@ extension DevToolsColorScheme on ColorScheme { Color get grey => const Color.fromARGB(255, 128, 128, 128); + Color get green =>const Color.fromARGB(255, 156, 233, 195); + Color get breakpointColor => primary; /// Background colors for charts. From ad82412f862e9c94c5bd589d53d11da13c9c9424 Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Thu, 21 Sep 2023 16:36:42 -0700 Subject: [PATCH 14/47] Update deep_links_screen.dart --- .../deep_links_screen.dart | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart index 8d1549bd952..e5e7a56dbaf 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; +import '../../shared/common_widgets.dart'; import '../../shared/primitives/auto_dispose.dart'; import '../../shared/primitives/utils.dart'; import '../../shared/screen.dart'; @@ -67,16 +68,12 @@ class _DeepLinkPageState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - Expanded( - child: Text( - 'All deep links', - style: Theme.of(context).textTheme.bodyLarge, - ), - ), - const SizedBox(width: denseSpacing), - SearchBar( + AreaPaneHeader( + title: Text( + 'All deep links', + style: Theme.of(context).textTheme.bodyLarge, + ), + actions: [SearchBar( leading: const Icon(Icons.search), hintText: 'Search a URL, domain or path', onChanged: (value) { @@ -85,9 +82,9 @@ class _DeepLinkPageState extends State constraints: BoxConstraints.tight( Size(wideSearchFieldWidth, defaultTextFieldHeight), ), - ), - ], + ),], ), + const SizedBox(height: denseSpacing), const TabBar( tabs: [ From ce1dcc4e381a96566477b36e0d86ddb27f1a6f21 Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Wed, 27 Sep 2023 13:37:54 -0700 Subject: [PATCH 15/47] update --- .../deep_links_model.dart | 15 +++++++----- .../deep_links_screen.dart | 23 +++++++++++-------- .../lib/src/shared/common_widgets.dart | 3 +++ 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart index d702146bee3..856b7a34adf 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -/// Contains all data relevant to a deep link. -/// import 'package:flutter/material.dart'; import '../../shared/primitives/utils.dart'; @@ -15,6 +13,7 @@ import '../../shared/utils.dart'; const kDeeplinkTableCellDefaultWidth = 200.0; +/// Contains all data relevant to a deep link. class LinkData with SearchableDataMixin { LinkData({ required this.domain, @@ -95,7 +94,9 @@ class DomainColumn extends ColumnData VoidCallback? onPressed, }) { return _ErrorAwareText( - isError: dataObject.domainError, text: dataObject.domain); + isError: dataObject.domainError, + text: dataObject.domain, + ); } } @@ -121,7 +122,9 @@ class PathColumn extends ColumnData VoidCallback? onPressed, }) { return _ErrorAwareText( - isError: dataObject.pathError, text: dataObject.path); + isError: dataObject.pathError, + text: dataObject.path, + ); } } @@ -180,9 +183,9 @@ class StatusColumn extends ColumnData style: TextStyle(color: Theme.of(context).colorScheme.error), ); } else { - return const Text( + return Text( 'No issues found', - style: TextStyle(color: Color.fromARGB(255, 156, 233, 195)), + style: TextStyle(color: Theme.of(context).colorScheme.green), overflow: TextOverflow.ellipsis, ); } diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart index e5e7a56dbaf..33815ae59cb 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart @@ -11,6 +11,7 @@ import '../../shared/screen.dart'; import '../../shared/table/table.dart'; import '../../shared/table/table_data.dart'; import '../../shared/theme.dart'; +import '../../shared/ui/search.dart'; import '../../shared/utils.dart'; import 'deep_links_controller.dart'; import 'deep_links_model.dart'; @@ -73,18 +74,20 @@ class _DeepLinkPageState extends State 'All deep links', style: Theme.of(context).textTheme.bodyLarge, ), - actions: [SearchBar( - leading: const Icon(Icons.search), - hintText: 'Search a URL, domain or path', - onChanged: (value) { - controller.searchContent = value; - }, - constraints: BoxConstraints.tight( - Size(wideSearchFieldWidth, defaultTextFieldHeight), + actions: [ + SizedBox( + width: wideSearchFieldWidth, + child: DevToolsClearableTextField( + labelText: '', + hintText: 'Search a URL, domain or path', + prefixIcon: const Icon(Icons.search), + onChanged: (value) { + controller.searchContent = value; + }, ), - ),], + ), + ], ), - const SizedBox(height: denseSpacing), const TabBar( tabs: [ diff --git a/packages/devtools_app/lib/src/shared/common_widgets.dart b/packages/devtools_app/lib/src/shared/common_widgets.dart index d009ac82fb9..50eeaf2f055 100644 --- a/packages/devtools_app/lib/src/shared/common_widgets.dart +++ b/packages/devtools_app/lib/src/shared/common_widgets.dart @@ -1201,6 +1201,7 @@ class DevToolsClearableTextField extends StatelessWidget { required this.labelText, TextEditingController? controller, this.hintText, + this.prefixIcon, this.onChanged, this.autofocus = false, }) : controller = controller ?? TextEditingController(), @@ -1208,6 +1209,7 @@ class DevToolsClearableTextField extends StatelessWidget { final TextEditingController controller; final String? hintText; + final Widget? prefixIcon; final String labelText; final Function(String)? onChanged; final bool autofocus; @@ -1227,6 +1229,7 @@ class DevToolsClearableTextField extends StatelessWidget { border: const OutlineInputBorder(), labelText: labelText, hintText: hintText, + prefixIcon: prefixIcon, suffixIcon: IconButton( tooltip: 'Clear', icon: const Icon(Icons.clear), From d887fee1a6a208b0939d0b215e3e6fdf0aa009a1 Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Wed, 27 Sep 2023 13:42:29 -0700 Subject: [PATCH 16/47] Update deep_links_screen.dart --- .../lib/src/screens/deep_link_validation/deep_links_screen.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart index 33815ae59cb..d1ecb795a17 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart @@ -287,7 +287,6 @@ class _DomainCheckTable extends StatelessWidget { : Text( 'No issues found', style: TextStyle( - // TODO: Update devtool colorscheme and use color from there. color: Theme.of(context).colorScheme.green, ), ), @@ -302,7 +301,6 @@ class _DomainCheckTable extends StatelessWidget { DataCell( Text( 'No issues found', - // TODO: Update devtool colorscheme and use color from there. style: TextStyle(color: Theme.of(context).colorScheme.green), ), ), From 76daac30b835c4e33d2b4b12ad39b88dd932f80a Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Wed, 27 Sep 2023 13:42:59 -0700 Subject: [PATCH 17/47] 1 --- .../lib/src/screens/deep_link_validation/deep_links_screen.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart index d1ecb795a17..efffd83ef86 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart @@ -11,7 +11,6 @@ import '../../shared/screen.dart'; import '../../shared/table/table.dart'; import '../../shared/table/table_data.dart'; import '../../shared/theme.dart'; -import '../../shared/ui/search.dart'; import '../../shared/utils.dart'; import 'deep_links_controller.dart'; import 'deep_links_model.dart'; From ea119c634aa4ef1c9519a9e4dca629dfcdb714df Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Thu, 28 Sep 2023 16:16:19 -0700 Subject: [PATCH 18/47] 1 --- .../deep_links_controller.dart | 14 +++- .../deep_links_model.dart | 62 +++++++++++++--- .../deep_links_screen.dart | 74 ++++++++++++++++++- .../deep_link_validation/fake_data.dart | 12 +-- 4 files changed, 140 insertions(+), 22 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart index 961e82be83e..e24c5185d24 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart @@ -13,7 +13,8 @@ class DeepLinksController { List get getLinkDatasByPath { final linkDatasByPath = {}; for (var linkData in linkDatasNotifier.value) { - linkDatasByPath[linkData.path] = linkData; + linkDatasByPath[linkData.path.single] = + linkData.mergeDomain(linkDatasByPath[linkData.path.single]); } return linkDatasByPath.values.toList(); } @@ -21,14 +22,21 @@ class DeepLinksController { List get getLinkDatasByDomain { final linkDatasByDomain = {}; for (var linkData in linkDatasNotifier.value) { - linkDatasByDomain[linkData.domain] = linkData; + linkDatasByDomain[linkData.domain.single] = + linkData.mergePath(linkDatasByDomain[linkData.domain.single]); } - return linkDatasByDomain.values.toList(); + final List linkDatasByDomainValues = + linkDatasByDomain.values.toList(); + domainErrorCountNotifier.value = + linkDatasByDomainValues.where((element) => element.domainError).length; + return linkDatasByDomainValues; } + final selectedLink = ValueNotifier(null); final linkDatasNotifier = ValueNotifier>(allLinkDatas); final showSpitScreenNotifier = ValueNotifier(false); + final domainErrorCountNotifier = ValueNotifier(0); final _searchContentNotifier = ValueNotifier(''); diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart index 856b7a34adf..ae1ab4daa1c 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart @@ -24,8 +24,8 @@ class LinkData with SearchableDataMixin { this.pathError = false, }); - final String path; - final String domain; + final List path; + final List domain; final List os; final List scheme; final bool domainError; @@ -33,12 +33,34 @@ class LinkData with SearchableDataMixin { @override bool matchesSearchToken(RegExp regExpSearch) { - return (domain.caseInsensitiveContains(regExpSearch) == true) || - (path.caseInsensitiveContains(regExpSearch) == true); + return (domain.join().caseInsensitiveContains(regExpSearch) == true) || + (path.join().caseInsensitiveContains(regExpSearch) == true); } @override String toString() => 'LinkData($domain $path)'; + + LinkData mergePath(LinkData? linkdata) { + if (linkdata == null) return this; + assert(domain.single == linkdata.domain.single); + return LinkData( + domain: domain, + path: [...path, ...linkdata.path], + os: os, + domainError: domainError, + ); + } + + LinkData mergeDomain(LinkData? linkdata) { + if (linkdata == null) return this; + assert(path.single == linkdata.path.single); + return LinkData( + domain: [...domain, ...linkdata.domain], + path: path, + os: os, + domainError: domainError, + ); + } } class _ErrorAwareText extends StatelessWidget { @@ -84,7 +106,7 @@ class DomainColumn extends ColumnData bool get supportsSorting => true; @override - String getValue(LinkData dataObject) => dataObject.domain; + String getValue(LinkData dataObject) => dataObject.domain.single; @override Widget build( @@ -95,7 +117,7 @@ class DomainColumn extends ColumnData }) { return _ErrorAwareText( isError: dataObject.domainError, - text: dataObject.domain, + text: dataObject.domain.single, ); } } @@ -112,7 +134,7 @@ class PathColumn extends ColumnData bool get supportsSorting => true; @override - String getValue(LinkData dataObject) => dataObject.path; + String getValue(LinkData dataObject) => dataObject.path.first; @override Widget build( @@ -123,11 +145,33 @@ class PathColumn extends ColumnData }) { return _ErrorAwareText( isError: dataObject.pathError, - text: dataObject.path, + text: dataObject.path.first, ); } } +class NumberOfAssociatedPathColumn extends ColumnData { + NumberOfAssociatedPathColumn() + : super( + 'Number of associated path', + fixedWidthPx: scaleByFontFactor(kDeeplinkTableCellDefaultWidth), + ); + + @override + String getValue(LinkData dataObject) => dataObject.path.length.toString(); +} + +class NumberOfAssociatedDomainColumn extends ColumnData { + NumberOfAssociatedDomainColumn() + : super( + 'Number of associated domain', + fixedWidthPx: scaleByFontFactor(kDeeplinkTableCellDefaultWidth), + ); + + @override + String getValue(LinkData dataObject) => dataObject.domain.length.toString(); +} + class SchemeColumn extends ColumnData { SchemeColumn() : super( @@ -192,7 +236,7 @@ class StatusColumn extends ColumnData } } -// TODO: implement this column. +// TODO: Implement this column. class NavigationColumn extends ColumnData implements ColumnRenderer { NavigationColumn() diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart index efffd83ef86..98d9fe938e5 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart @@ -15,6 +15,8 @@ import '../../shared/utils.dart'; import 'deep_links_controller.dart'; import 'deep_links_model.dart'; +const double _kNotificationCardWidth = 475; + enum TableViewType { domainView, pathView, @@ -70,10 +72,24 @@ class _DeepLinkPageState extends State children: [ AreaPaneHeader( title: Text( - 'All deep links', + 'Validate and fix', style: Theme.of(context).textTheme.bodyLarge, ), - actions: [ + ), + ValueListenableBuilder( + valueListenable: controller.domainErrorCountNotifier, + builder: (context, domainErrorCount, _) => + _NotificationCard(domainErrorCount: domainErrorCount), + ), + Row( + children: [ + Expanded( + child: Text( + 'All deep links', + style: Theme.of(context).textTheme.bodyLarge, + ), + ), + const SizedBox(width: denseSpacing), SizedBox( width: wideSearchFieldWidth, child: DevToolsClearableTextField( @@ -193,8 +209,15 @@ class _DataTable extends StatelessWidget { dataKey: 'deep-links', autoScrollContent: true, columns: [ - if (tableView != TableViewType.pathView) domain, - if (tableView != TableViewType.domainView) path, + if (tableView == TableViewType.domainView) ...[ + domain, + NumberOfAssociatedPathColumn(), + ], + if (tableView == TableViewType.pathView) ...[ + path, + NumberOfAssociatedDomainColumn(), + ], + if (tableView == TableViewType.singleUrlView) ...[domain, path], SchemeColumn(), OSColumn(), if (!controller.showSpitScreen) ...[ @@ -309,3 +332,46 @@ class _DomainCheckTable extends StatelessWidget { ); } } + +class _NotificationCard extends StatelessWidget { + const _NotificationCard({ + required this.domainErrorCount, + }); + + final int domainErrorCount; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + return SizedBox( + width: _kNotificationCardWidth, + child: Card( + child: Padding( + padding: const EdgeInsets.all(defaultSpacing), + child: Wrap( + children: [ + Icon(Icons.error, color: colorScheme.error), + const SizedBox(width: denseSpacing), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('$domainErrorCount domain not verified'), + Text( + '(Placeholder) This affects all deep links. Fix issues to make users go directly to your app.', + style: textTheme.bodyMedium, + ), + TextButton( + // TODO: Implement this. + onPressed: () {}, + child: const Text('Fix domain'), + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/fake_data.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/fake_data.dart index 2e16a18489f..c953b4d2eda 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/fake_data.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/fake_data.dart @@ -18,23 +18,23 @@ final allLinkDatas = [ for (var path in paths) LinkData( os: ['Android', 'iOS'], - domain: 'm.shopping.com', - path: path, + domain: ['m.shopping.com'], + path: [path], domainError: true, pathError: path.contains('shoe'), ), for (var path in paths) LinkData( os: ['iOS'], - domain: 'm.french.shopping.com', - path: path, + domain: ['m.french.shopping.com'], + path: [path], pathError: path.contains('shoe'), ), for (var path in paths) LinkData( os: ['Android'], - domain: 'm.chinese.shopping.com', - path: path, + domain: ['m.chinese.shopping.com'], + path: [path], pathError: path.contains('shoe'), ), ]; From 95c26338d7a6f50b7636f230e5100f5a3351ae7a Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Tue, 17 Oct 2023 13:05:28 -0700 Subject: [PATCH 19/47] update deep link validation tool ui --- .../deep_links_controller.dart | 166 ++++-- .../deep_links_model.dart | 258 +++++++- .../deep_links_screen.dart | 564 +++++++++++++----- .../lib/src/shared/ui/colors.dart | 1 + 4 files changed, 793 insertions(+), 196 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart index e24c5185d24..0557ddfbe5a 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart @@ -7,59 +7,145 @@ import 'package:flutter/foundation.dart'; import 'deep_links_model.dart'; import 'fake_data.dart'; -class DeepLinksController { - bool get showSpitScreen => showSpitScreenNotifier.value; +enum SchemeFilterOption { + http, + custom, + showAll, +} - List get getLinkDatasByPath { - final linkDatasByPath = {}; - for (var linkData in linkDatasNotifier.value) { - linkDatasByPath[linkData.path.single] = - linkData.mergeDomain(linkDatasByPath[linkData.path.single]); - } - return linkDatasByPath.values.toList(); - } +enum OsFilterOption { + android, + ios, + showAll, +} - List get getLinkDatasByDomain { - final linkDatasByDomain = {}; - for (var linkData in linkDatasNotifier.value) { - linkDatasByDomain[linkData.domain.single] = - linkData.mergePath(linkDatasByDomain[linkData.domain.single]); - } - final List linkDatasByDomainValues = - linkDatasByDomain.values.toList(); - domainErrorCountNotifier.value = - linkDatasByDomainValues.where((element) => element.domainError).length; - return linkDatasByDomainValues; - } +enum StatusFilterOption { + noIssue, + failedDomainCheck, + failedPathCheck, + failedAllCheck, + showAll, +} + +class DeepLinksController { + bool get showSplitScreen => showSplitScreenNotifier.value; + List get getLinkDatasByPath => + _getFilterredLinks(linkDatasByPath, _searchContentNotifier.value); + List get getLinkDatasByDomain => + _getFilterredLinks(linkDatasByDomain, _searchContentNotifier.value); final selectedLink = ValueNotifier(null); final linkDatasNotifier = ValueNotifier>(allLinkDatas); - final showSpitScreenNotifier = ValueNotifier(false); + final showSplitScreenNotifier = ValueNotifier(false); final domainErrorCountNotifier = ValueNotifier(0); - + final pathErrorCountNotifier = ValueNotifier(0); final _searchContentNotifier = ValueNotifier(''); + final schemeFilterOptionNotifier = + ValueNotifier(SchemeFilterOption.showAll); + final osFilterOptionNotifier = + ValueNotifier(OsFilterOption.showAll); + final statusFilterOptionNotifier = + ValueNotifier(StatusFilterOption.showAll); - void _updateLinks() { - final searchContent = _searchContentNotifier.value; - final List linkDatas = searchContent.isNotEmpty - ? allLinkDatas - .where( - (linkData) => linkData.matchesSearchToken( - RegExp( - searchContent, - caseSensitive: false, - ), - ), - ) - .toList() - : allLinkDatas; + var linkDatasByDomain = []; + var linkDatasByPath = []; - linkDatasNotifier.value = linkDatas; + void initLinkDatas() { + linkDatasNotifier.value = allLinkDatas; + final linkDatasByDomainMap = {}; + for (var linkData in allLinkDatas) { + linkDatasByDomainMap[linkData.domain.single] = + linkData.mergebyDomain(linkDatasByDomainMap[linkData.domain.single]); + } + final List linkDatasByDomainValues = + linkDatasByDomainMap.values.toList(); + domainErrorCountNotifier.value = + linkDatasByDomainValues.where((element) => element.domainError).length; + linkDatasByDomain = linkDatasByDomainValues; + + final linkDatasByPathMap = {}; + for (var linkData in allLinkDatas) { + linkDatasByPathMap[linkData.path.single] = + linkData.mergebyPath(linkDatasByPathMap[linkData.path.single]); + } + final List linkDatasByPathValues = + linkDatasByPathMap.values.toList(); + pathErrorCountNotifier.value = + linkDatasByPathValues.where((element) => element.pathError).length; + linkDatasByPath = linkDatasByPathValues; } set searchContent(String content) { _searchContentNotifier.value = content; - _updateLinks(); + linkDatasNotifier.value = + _getFilterredLinks(allLinkDatas, _searchContentNotifier.value); + } + + void updateFilterOptions({ + SchemeFilterOption? schemeOption, + OsFilterOption? osOption, + StatusFilterOption? statusOption, + }) { + if (schemeOption != null) schemeFilterOptionNotifier.value = schemeOption; + if (osOption != null) osFilterOptionNotifier.value = osOption; + if (statusOption != null) statusFilterOptionNotifier.value = statusOption; + + linkDatasNotifier.value = + _getFilterredLinks(allLinkDatas, _searchContentNotifier.value); + } + + List _getFilterredLinks( + List linkDatas, + String searchContent, + ) { + if (searchContent.isNotEmpty) { + linkDatas = linkDatas + .where( + (linkData) => linkData.matchesSearchToken( + RegExp( + searchContent, + caseSensitive: false, + ), + ), + ) + .toList(); + } + switch (statusFilterOptionNotifier.value) { + case StatusFilterOption.failedAllCheck: + linkDatas = linkDatas + .where((linkData) => linkData.domainError && linkData.pathError) + .toList(); + break; + case StatusFilterOption.failedDomainCheck: + linkDatas = + linkDatas.where((linkData) => linkData.domainError).toList(); + break; + case StatusFilterOption.failedPathCheck: + linkDatas = linkDatas.where((linkData) => linkData.pathError).toList(); + break; + case StatusFilterOption.noIssue: + linkDatas = linkDatas + .where((linkData) => !linkData.pathError && !linkData.domainError) + .toList(); + break; + case StatusFilterOption.showAll: + break; + } + switch (osFilterOptionNotifier.value) { + case OsFilterOption.android: + linkDatas = linkDatas + .where((linkData) => linkData.os.contains('Android')) + .toList(); + break; + case OsFilterOption.ios: + linkDatas = + linkDatas.where((linkData) => linkData.os.contains('iOS')).toList(); + break; + case OsFilterOption.showAll: + break; + } + + return linkDatas; } } diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart index 8923c67db1d..bb0ce1796be 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart @@ -2,13 +2,17 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:devtools_app_shared/ui.dart'; import 'package:flutter/material.dart'; import '../../shared/primitives/utils.dart'; import '../../shared/table/table.dart'; import '../../shared/table/table_data.dart'; +import '../../shared/ui/colors.dart'; import '../../shared/ui/search.dart'; -import '../../shared/utils.dart'; + +import 'deep_links_controller.dart'; +import 'deep_links_screen.dart'; const kDeeplinkTableCellDefaultWidth = 200.0; @@ -39,23 +43,25 @@ class LinkData with SearchableDataMixin { @override String toString() => 'LinkData($domain $path)'; - LinkData mergePath(LinkData? linkdata) { + // Used for [TableViewType.pathView]. + LinkData mergebyPath(LinkData? linkdata) { if (linkdata == null) return this; - assert(domain.single == linkdata.domain.single); + assert(path.single == linkdata.path.single); return LinkData( - domain: domain, - path: [...path, ...linkdata.path], + domain: [...domain, ...linkdata.domain], + path: path, os: os, - domainError: domainError, + pathError: pathError, ); } - LinkData mergeDomain(LinkData? linkdata) { + // Used for [TableViewType.domainView]. + LinkData mergebyDomain(LinkData? linkdata) { if (linkdata == null) return this; - assert(path.single == linkdata.path.single); + assert(domain.single == linkdata.domain.single); return LinkData( - domain: [...domain, ...linkdata.domain], - path: path, + domain: domain, + path: [...path, ...linkdata.path], os: os, domainError: domainError, ); @@ -119,6 +125,14 @@ class DomainColumn extends ColumnData text: dataObject.domain.single, ); } + + // Shows result with error first. + @override + int compare(LinkData a, LinkData b) { + if (a.domainError) return -1; + if (b.domainError) return 1; + return getValue(a).compareTo(getValue(b)); + } } class PathColumn extends ColumnData @@ -147,6 +161,14 @@ class PathColumn extends ColumnData text: dataObject.path.first, ); } + + // Shows result with error first. + @override + int compare(LinkData a, LinkData b) { + if (a.pathError) return -1; + if (b.pathError) return 1; + return getValue(a).compareTo(getValue(b)); + } } class NumberOfAssociatedPathColumn extends ColumnData { @@ -171,36 +193,154 @@ class NumberOfAssociatedDomainColumn extends ColumnData { String getValue(LinkData dataObject) => dataObject.domain.length.toString(); } -class SchemeColumn extends ColumnData { - SchemeColumn() +class SchemeColumn extends ColumnData + implements ColumnHeaderRenderer { + SchemeColumn(this.controller) : super( 'Scheme', fixedWidthPx: scaleByFontFactor(kDeeplinkTableCellDefaultWidth), ); + DeepLinksController controller; + + @override + Widget? buildHeader( + BuildContext context, + Widget Function() defaultHeaderRenderer, + ) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Scheme'), + PopupMenuButton( + onSelected: (option) { + controller.updateFilterOptions(schemeOption: option); + }, + itemBuilder: (BuildContext context) { + return [SchemeFilterOption.http, SchemeFilterOption.custom] + .map( + (value) => PopupMenuItem( + value: value, + child: Row( + children: [ + ValueListenableBuilder( + valueListenable: + controller.schemeFilterOptionNotifier, + builder: (context, option, _) => Checkbox( + value: option == value, + onChanged: (bool? checked) { + if (checked!) { + controller.updateFilterOptions( + schemeOption: value); + } else { + controller.updateFilterOptions( + schemeOption: SchemeFilterOption.showAll, + ); + } + }, + ), + ), + if (value == SchemeFilterOption.http) + const Text('Http://, https://'), + if (value == SchemeFilterOption.custom) + const Text('Custom scheme'), + ], + ), + ), + ) + .toList(); + }, + child: Icon( + Icons.arrow_drop_down, + size: actionsIconSize, + ), + ), + ], + ); + } + @override String getValue(LinkData dataObject) => dataObject.scheme.join(','); } -class OSColumn extends ColumnData { - OSColumn() +class OSColumn extends ColumnData + implements ColumnHeaderRenderer { + OSColumn(this.controller) : super( 'OS', fixedWidthPx: scaleByFontFactor(kDeeplinkTableCellDefaultWidth), ); + DeepLinksController controller; + + @override + Widget? buildHeader( + BuildContext context, + Widget Function() defaultHeaderRenderer, + ) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('OS'), + PopupMenuButton( + onSelected: (option) { + controller.updateFilterOptions(osOption: option); + }, + itemBuilder: (BuildContext context) { + return OsFilterOption.values + .map( + (value) => PopupMenuItem( + value: value, + child: Row( + children: [ + ValueListenableBuilder( + valueListenable: controller.osFilterOptionNotifier, + builder: (context, option, _) => Checkbox( + value: option == value, + onChanged: (bool? checked) { + if (checked!) { + controller.updateFilterOptions(osOption: value); + } else { + controller.updateFilterOptions( + osOption: OsFilterOption.showAll, + ); + } + }, + ), + ), + if (value == OsFilterOption.showAll) + const Text('Android, iOS'), + if (value == OsFilterOption.android) + const Text('Android'), + if (value == OsFilterOption.ios) const Text('iOS'), + ], + ), + ), + ) + .toList(); + }, + child: Icon( + Icons.arrow_drop_down, + size: actionsIconSize, + ), + ), + ], + ); + } + @override String getValue(LinkData dataObject) => dataObject.os.join(','); } class StatusColumn extends ColumnData - implements ColumnRenderer { - StatusColumn() + implements ColumnRenderer, ColumnHeaderRenderer { + StatusColumn(this.controller, this.tableViewType) : super( 'Status', fixedWidthPx: scaleByFontFactor(kDeeplinkTableCellDefaultWidth), ); - + DeepLinksController controller; + TableViewType tableViewType; @override String getValue(LinkData dataObject) { if (dataObject.domainError) { @@ -212,6 +352,90 @@ class StatusColumn extends ColumnData } } + @override + Widget? buildHeader( + BuildContext context, + Widget Function() defaultHeaderRenderer, + ) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Status'), + PopupMenuButton( + onSelected: (option) { + controller.updateFilterOptions(statusOption: option); + }, + itemBuilder: (BuildContext context) { + var statusFilterOptions = []; + switch (tableViewType) { + case TableViewType.singleUrlView: + statusFilterOptions = [ + StatusFilterOption.failedAllCheck, + StatusFilterOption.failedDomainCheck, + StatusFilterOption.failedPathCheck, + StatusFilterOption.noIssue, + ]; + break; + case TableViewType.domainView: + statusFilterOptions = [ + StatusFilterOption.failedDomainCheck, + StatusFilterOption.noIssue, + ]; + break; + case TableViewType.pathView: + statusFilterOptions = [ + StatusFilterOption.failedPathCheck, + StatusFilterOption.noIssue, + ]; + break; + } + return statusFilterOptions + .map( + (value) => PopupMenuItem( + value: value, + child: Row( + children: [ + ValueListenableBuilder( + valueListenable: + controller.statusFilterOptionNotifier, + builder: (context, option, _) => Checkbox( + value: option == value, + onChanged: (bool? checked) { + if (checked!) { + controller.updateFilterOptions( + statusOption: value, + ); + } else { + controller.updateFilterOptions( + statusOption: StatusFilterOption.showAll, + ); + } + }, + ), + ), + if (value == StatusFilterOption.failedAllCheck) + const Text('Failed domain and path checks'), + if (value == StatusFilterOption.failedDomainCheck) + const Text('Failed domain checks'), + if (value == StatusFilterOption.failedPathCheck) + const Text('Failed path checks'), + if (value == StatusFilterOption.noIssue) + const Text('No issues found'), + ], + ), + ), + ) + .toList(); + }, + child: Icon( + Icons.arrow_drop_down, + size: actionsIconSize, + ), + ), + ], + ); + } + @override Widget build( BuildContext context, diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart index f6df58d498a..2686301bb61 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart @@ -11,11 +11,12 @@ import '../../shared/primitives/utils.dart'; import '../../shared/screen.dart'; import '../../shared/table/table.dart'; import '../../shared/table/table_data.dart'; +import '../../shared/ui/colors.dart'; import '../../shared/utils.dart'; import 'deep_links_controller.dart'; import 'deep_links_model.dart'; -const double _kNotificationCardWidth = 475; +const _kNotificationCardSize = Size(475, 132); enum TableViewType { domainView, @@ -54,32 +55,111 @@ class _DeepLinkPageState extends State void didChangeDependencies() { super.didChangeDependencies(); if (!initController()) return; + controller.initLinkDatas(); } @override Widget build(BuildContext context) { - return DefaultTabController( - length: TableViewType.values.length, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - AreaPaneHeader( - title: Text( - 'Validate and fix', - style: Theme.of(context).textTheme.bodyLarge, + final theme = Theme.of(context); + final textTheme = theme.textTheme; + return ValueListenableBuilder( + valueListenable: controller.showSplitScreenNotifier, + builder: (context, showSplitScreen, _) => DefaultTabController( + length: TableViewType.values.length, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AreaPaneHeader( + title: Text('Validate and fix', style: textTheme.bodyLarge), ), + showSplitScreen + ? const SizedBox.shrink() + : ValueListenableBuilder( + valueListenable: controller.domainErrorCountNotifier, + builder: (context, domainErrorCount, _) => + ValueListenableBuilder( + valueListenable: controller.pathErrorCountNotifier, + builder: (context, pathErrorCount, _) => + _NotificationCardSection( + domainErrorCount: domainErrorCount, + pathErrorCount: pathErrorCount, + controller: controller, + ), + ), + ), + Expanded( + child: Row( + children: [ + Expanded( + child: _AllDeepLinkDataTable(controller: controller), + ), + if (showSplitScreen) + Expanded( + child: ValueListenableBuilder( + valueListenable: controller.selectedLink, + builder: (context, selectedLink, _) => TabBarView( + children: [ + _ValidationDetailScreen( + linkData: selectedLink!, + controller: controller, + tableView: TableViewType.domainView, + ), + _ValidationDetailScreen( + linkData: selectedLink, + controller: controller, + tableView: TableViewType.pathView, + ), + _ValidationDetailScreen( + linkData: selectedLink, + controller: controller, + tableView: TableViewType.singleUrlView, + ), + ], + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +class _AllDeepLinkDataTable extends StatelessWidget { + const _AllDeepLinkDataTable({ + required this.controller, + }); + final DeepLinksController controller; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + final textTheme = theme.textTheme; + return Column( + children: [ + Container( + decoration: BoxDecoration( + border: Border.fromBorderSide( + defaultBorderSide(Theme.of(context)), + ), + color: colorScheme.surface, ), - ValueListenableBuilder( - valueListenable: controller.domainErrorCountNotifier, - builder: (context, domainErrorCount, _) => - _NotificationCard(domainErrorCount: domainErrorCount), + padding: const EdgeInsets.fromLTRB( + defaultSpacing, + denseSpacing, + denseSpacing, + denseSpacing, ), - Row( + child: Row( children: [ Expanded( child: Text( 'All deep links', - style: Theme.of(context).textTheme.bodyLarge, + style: textTheme.bodyLarge, ), ), const SizedBox(width: denseSpacing), @@ -96,86 +176,76 @@ class _DeepLinkPageState extends State ), ], ), - const SizedBox(height: denseSpacing), - const TabBar( - tabs: [ - Text('Domain view'), - Text('Path view'), - Text('Single URL view'), - ], - tabAlignment: TabAlignment.start, - isScrollable: true, - ), - Expanded( - child: ValueListenableBuilder>( - valueListenable: controller.linkDatasNotifier, - builder: (context, linkDatas, _) => ValueListenableBuilder( - valueListenable: controller.showSpitScreenNotifier, - builder: (context, showSpitScreen, _) => TabBarView( - children: [ - _DataTableWithValidationDetails( - tableView: TableViewType.domainView, - linkDatas: controller.getLinkDatasByDomain, - controller: controller, - showSpitScreen: showSpitScreen, - ), - _DataTableWithValidationDetails( - tableView: TableViewType.pathView, - linkDatas: controller.getLinkDatasByPath, - controller: controller, - showSpitScreen: showSpitScreen, - ), - _DataTableWithValidationDetails( - tableView: TableViewType.singleUrlView, - linkDatas: linkDatas, - controller: controller, - showSpitScreen: showSpitScreen, - ), - ], - ), - ), + ), + Container( + decoration: BoxDecoration( + border: Border.fromBorderSide( + defaultBorderSide(Theme.of(context)), ), + color: colorScheme.surface, ), - ], - ), - ); - } -} - -class _DataTableWithValidationDetails extends StatelessWidget { - const _DataTableWithValidationDetails({ - required this.linkDatas, - required this.tableView, - required this.controller, - required this.showSpitScreen, - }); - final List linkDatas; - final TableViewType tableView; - final DeepLinksController controller; - final bool showSpitScreen; + child: Row( + children: [ + TabBar( + tabs: [ + Text( + 'Domain view', + style: textTheme.bodyLarge, + ), + Text( + 'Path view', + style: textTheme.bodyLarge, + ), + Text( + 'Single URL view', + style: textTheme.bodyLarge, + ), + ], + tabAlignment: TabAlignment.start, + isScrollable: true, + ), + const Spacer(), - @override - Widget build(BuildContext context) { - return Row( - children: [ - Expanded( - child: _DataTable( - tableView: tableView, - linkDatas: linkDatas, - controller: controller, + // TODO: Add functions to these icons. + IconButton( + onPressed: () {}, + icon: Icon(Icons.restart_alt, size: actionsIconSize), + ), + IconButton( + onPressed: () {}, + icon: Icon(Icons.settings, size: actionsIconSize), + ), + IconButton( + onPressed: () {}, + icon: Icon(Icons.help_outline, size: actionsIconSize), + ), + ], ), ), - if (showSpitScreen) - Expanded( - child: ValueListenableBuilder( - valueListenable: controller.selectedLink, - builder: (context, selectedLink, _) => _ValidationDetailScreen( - tableView: tableView, - linkData: selectedLink!, - controller: controller, - ), + Expanded( + child: ValueListenableBuilder>( + valueListenable: controller.linkDatasNotifier, + builder: (context, linkDatas, _) => TabBarView( + children: [ + _DataTable( + tableView: TableViewType.domainView, + linkDatas: controller.getLinkDatasByDomain, + controller: controller, + ), + _DataTable( + tableView: TableViewType.pathView, + linkDatas: controller.getLinkDatasByPath, + controller: controller, + ), + _DataTable( + tableView: TableViewType.singleUrlView, + linkDatas: linkDatas, + controller: controller, + ), + ], ), ), + ), ], ); } @@ -196,32 +266,42 @@ class _DataTable extends StatelessWidget { final ColumnData domain = DomainColumn(); final ColumnData path = PathColumn(); - return FlatTable( - keyFactory: (node) => ValueKey(node.toString), - data: linkDatas, - dataKey: 'deep-links', - autoScrollContent: true, - columns: [ - if (tableView == TableViewType.domainView) ...[ - domain, - NumberOfAssociatedPathColumn(), - ], - if (tableView == TableViewType.pathView) ...[ - path, - NumberOfAssociatedDomainColumn(), - ], - if (tableView == TableViewType.singleUrlView) ...[domain, path], - SchemeColumn(), - OSColumn(), - if (!controller.showSpitScreen) ...[ - StatusColumn(), - NavigationColumn(), + return Container( + decoration: BoxDecoration( + border: Border.fromBorderSide( + defaultBorderSide(Theme.of(context)), + ), + color: Theme.of(context).colorScheme.surface, + ), + padding: const EdgeInsets.only(top: denseSpacing), + child: FlatTable( + keyFactory: (node) => ValueKey(node.toString), + data: linkDatas, + dataKey: 'deep-links', + autoScrollContent: true, + columns: [ + if (tableView == TableViewType.domainView) ...[ + domain, + NumberOfAssociatedPathColumn(), + ], + if (tableView == TableViewType.pathView) ...[ + path, + NumberOfAssociatedDomainColumn(), + ], + if (tableView == TableViewType.singleUrlView) ...[domain, path], + SchemeColumn(controller), + OSColumn(controller), + if (!controller.showSplitScreen) ...[ + StatusColumn(controller, tableView), + NavigationColumn(), + ], ], - ], - selectionNotifier: controller.selectedLink, - defaultSortColumn: tableView == TableViewType.pathView ? path : domain, - defaultSortDirection: SortDirection.ascending, - onItemSelected: (item) => controller.showSpitScreenNotifier.value = true, + selectionNotifier: controller.selectedLink, + defaultSortColumn: tableView == TableViewType.pathView ? path : domain, + defaultSortDirection: SortDirection.ascending, + onItemSelected: (item) => + controller.showSplitScreenNotifier.value = true, + ), ); } } @@ -239,6 +319,8 @@ class _ValidationDetailScreen extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; return Padding( padding: const EdgeInsets.symmetric(horizontal: largeSpacing), child: Column( @@ -247,10 +329,15 @@ class _ValidationDetailScreen extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text('Selected Deep link validation details'), + Text( + tableView == TableViewType.domainView + ? 'Selected domain validation details' + : 'Selected Deep link validation details', + style: textTheme.titleSmall, + ), IconButton( onPressed: () => - controller.showSpitScreenNotifier.value = false, + controller.showSplitScreenNotifier.value = false, icon: const Icon(Icons.close), ), ], @@ -260,10 +347,62 @@ class _ValidationDetailScreen extends StatelessWidget { ' and Custom Schemes in your app. Web check are done for the web association' ' file on your website. App checks are done for the intent filters in' ' the manifest and info.plist file, routing issues, URL format, etc.', - style: Theme.of(context).textTheme.bodySmall, + style: textTheme.bodyMedium!.copyWith( + color: colorScheme.onSurface.withOpacity(0.6), + ), ), - const Text('Domain check'), - Expanded(child: _DomainCheckTable(linkData: linkData)), + if (tableView != TableViewType.pathView) ...[ + const SizedBox(height: intermediateSpacing), + Text('Domain check', style: textTheme.titleSmall), + _DomainCheckTable(linkData: linkData), + ], + if (tableView != TableViewType.domainView) ...[ + const SizedBox(height: intermediateSpacing), + Text('Path check (coming soon)', style: textTheme.titleSmall), + _PathCheckTable(), + ], + Align( + alignment: Alignment.bottomRight, + child: FilledButton( + onPressed: () { + controller.initLinkDatas(); + }, + child: const Text('Recheck all'), + ), + ), + if (tableView == TableViewType.domainView) ...[ + Text('Associated deep link URL', style: textTheme.titleSmall), + Card( + color: colorScheme.surface, + shape: const RoundedRectangleBorder(), + child: Padding( + padding: const EdgeInsets.all(denseSpacing), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: linkData.path + .map( + (path) => Padding( + padding: const EdgeInsets.symmetric( + vertical: denseRowSpacing, + ), + child: Row( + children: [ + Icon( + Icons.error, + color: colorScheme.error, + size: defaultIconSize, + ), + const SizedBox(width: denseSpacing), + Text(path), + ], + ), + ), + ) + .toList(), + ), + ), + ), + ], ], ), ); @@ -280,6 +419,9 @@ class _DomainCheckTable extends StatelessWidget { @override Widget build(BuildContext context) { return DataTable( + dataRowColor: MaterialStateProperty.all( + Theme.of(context).colorScheme.alternatingBackgroundColor2, + ), columns: const [ DataColumn(label: Text('OS')), DataColumn(label: Text('Issue type')), @@ -326,40 +468,184 @@ class _DomainCheckTable extends StatelessWidget { } } -class _NotificationCard extends StatelessWidget { - const _NotificationCard({ +class _PathCheckTable extends StatelessWidget { + @override + Widget build(BuildContext context) { + final notAvailableCell = DataCell( + Text( + 'Not available', + style: TextStyle( + color: Theme.of(context).colorScheme.deeplinkUnavailableColor, + ), + ), + ); + return Opacity( + opacity: 0.5, + child: DataTable( + dataRowColor: MaterialStateProperty.all( + Theme.of(context).colorScheme.alternatingBackgroundColor2, + ), + columns: const [ + DataColumn(label: Text('OS')), + DataColumn(label: Text('Issue type')), + DataColumn(label: Text('Status')), + ], + rows: [ + DataRow( + cells: [ + const DataCell(Text('Android')), + const DataCell(Text('Intent filter')), + notAvailableCell, + ], + ), + DataRow( + cells: [ + const DataCell(Text('iOS')), + const DataCell(Text('Associated domain')), + notAvailableCell, + ], + ), + DataRow( + cells: [ + const DataCell(Text('Android, iOS')), + const DataCell(Text('URL format')), + notAvailableCell, + ], + ), + DataRow( + cells: [ + const DataCell(Text('Android, iOS')), + const DataCell(Text('Routing')), + notAvailableCell, + ], + ), + ], + ), + ); + } +} + +class _NotificationCardSection extends StatelessWidget { + const _NotificationCardSection({ required this.domainErrorCount, + required this.pathErrorCount, + required this.controller, }); final int domainErrorCount; + final int pathErrorCount; + final DeepLinksController controller; + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + if (domainErrorCount == 0 && domainErrorCount == 0) { + return const SizedBox.shrink(); + } + return Container( + decoration: BoxDecoration( + border: Border.fromBorderSide(defaultBorderSide(Theme.of(context))), + color: colorScheme.surface, + ), + padding: const EdgeInsets.all(defaultSpacing), + child: Row( + children: [ + if (domainErrorCount > 0) + _NotificationCard( + title: '$domainErrorCount domain not verified', + description: + 'This affects all deep links. Fix issues to make users go directly to your app.', + actionButton: TextButton( + onPressed: () { + // Switch to the domain view. Select the first link with domain error and show the split screen. + DefaultTabController.of(context).index = 0; + controller.selectedLink.value = controller + .getLinkDatasByDomain + .where((element) => element.domainError) + .first; + controller.showSplitScreenNotifier.value = true; + }, + child: const Text('Fix domain'), + ), + ), + if (domainErrorCount > 0 && pathErrorCount > 0) + const SizedBox(width: defaultSpacing), + if (pathErrorCount > 0) + _NotificationCard( + title: '$pathErrorCount path not working', + description: + 'Fix these path to make sure users are directed to your app', + actionButton: TextButton( + onPressed: () { + // Switch to the path view. Select the first link with path error and show the split screen. + DefaultTabController.of(context).index = 1; + controller.selectedLink.value = controller.getLinkDatasByPath + .where((element) => element.pathError) + .first; + controller.showSplitScreenNotifier.value = true; + }, + child: const Text('Fix path'), + ), + ), + ], + ), + ); + } +} + +class _NotificationCard extends StatelessWidget { + const _NotificationCard({ + required this.title, + required this.description, + required this.actionButton, + }); + + final String title; + final String description; + final Widget actionButton; @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; - return SizedBox( - width: _kNotificationCardWidth, + return SizedBox.fromSize( + size: _kNotificationCardSize, child: Card( + color: colorScheme.surface, child: Padding( - padding: const EdgeInsets.all(defaultSpacing), - child: Wrap( + padding: const EdgeInsets.fromLTRB( + defaultSpacing, + defaultSpacing, + defaultSpacing, + 0, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(Icons.error, color: colorScheme.error), const SizedBox(width: denseSpacing), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('$domainErrorCount domain not verified'), - Text( - '(Placeholder) This affects all deep links. Fix issues to make users go directly to your app.', - style: textTheme.bodyMedium, - ), - TextButton( - // TODO: Implement this. - onPressed: () {}, - child: const Text('Fix domain'), - ), - ], + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: textTheme.bodyMedium! + .copyWith(color: colorScheme.onSurface), + ), + Text( + description, + style: textTheme.bodyMedium!.copyWith( + color: colorScheme.onSurface.withOpacity(0.6), + ), + ), + Expanded( + child: Align( + alignment: Alignment.bottomRight, + child: actionButton, + ), + ), + ], + ), ), ], ), diff --git a/packages/devtools_app/lib/src/shared/ui/colors.dart b/packages/devtools_app/lib/src/shared/ui/colors.dart index 39f3b7ad156..e3c44ce819a 100644 --- a/packages/devtools_app/lib/src/shared/ui/colors.dart +++ b/packages/devtools_app/lib/src/shared/ui/colors.dart @@ -148,4 +148,5 @@ extension DevToolsColorExtension on ColorScheme { Color get green => const Color.fromARGB(255, 156, 233, 195); Color get overlayShadowColor => const Color.fromRGBO(0, 0, 0, 0.5); + Color get deeplinkUnavailableColor => const Color(0xFFFE7C04); } From d95318c2d770d442278d57de9a4fcb08da5519bf Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Tue, 24 Oct 2023 16:21:44 -0700 Subject: [PATCH 20/47] more ui --- packages/devtools_app/lib/src/app.dart | 2 +- .../deep_links_controller.dart | 198 ++++--- .../deep_links_model.dart | 189 +++---- .../deep_links_screen.dart | 515 +++++++++--------- .../lib/src/shared/table/table.dart | 22 +- .../lib/src/shared/ui/colors.dart | 3 +- 6 files changed, 453 insertions(+), 476 deletions(-) diff --git a/packages/devtools_app/lib/src/app.dart b/packages/devtools_app/lib/src/app.dart index 2905854fa27..e93c55be2d1 100644 --- a/packages/devtools_app/lib/src/app.dart +++ b/packages/devtools_app/lib/src/app.dart @@ -581,7 +581,7 @@ List defaultScreens({ AppSizeScreen(), createController: (_) => AppSizeController(), ), - if (FeatureFlags.deepLinkValidation) + //if (FeatureFlags.deepLinkValidation) DevToolsScreen( DeepLinksScreen(), createController: (_) => DeepLinksController(), diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart index 0557ddfbe5a..b7b4cae4e88 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart @@ -7,46 +7,82 @@ import 'package:flutter/foundation.dart'; import 'deep_links_model.dart'; import 'fake_data.dart'; -enum SchemeFilterOption { - http, - custom, - showAll, +enum FilterOption { + http('http://, https://'), + custom('Custom scheme'), + android('Android'), + ios('iOS'), + noIssue('No issues found'), + failedDomainCheck('Failed domain checks '), + failedPathCheck('Failed path checks'); + + const FilterOption(this.description); + final String description; } -enum OsFilterOption { - android, - ios, - showAll, -} +class DisplayOptions { + DisplayOptions({ + this.domainErrorCount = 0, + this.pathErrorCount = 0, + this.showSplitScreen = false, + this.filters = const { + FilterOption.http: true, + FilterOption.custom: true, + FilterOption.android: true, + FilterOption.ios: true, + FilterOption.noIssue: true, + FilterOption.failedDomainCheck: true, + FilterOption.failedPathCheck: true, + }, + }); + + int domainErrorCount = 0; + int pathErrorCount = 0; + bool showSplitScreen = false; + + Map filters = { + for (var item in FilterOption.values) item: true, + }; + + DisplayOptions updateFilter(FilterOption option, bool value) { + filters[option] = value; + return DisplayOptions( + domainErrorCount: domainErrorCount, + pathErrorCount: pathErrorCount, + showSplitScreen: showSplitScreen, + filters: filters, + ); + } -enum StatusFilterOption { - noIssue, - failedDomainCheck, - failedPathCheck, - failedAllCheck, - showAll, + DisplayOptions copyWith({ + int? domainErrorCount, + int? pathErrorCount, + bool? showSplitScreen, + }) { + return DisplayOptions( + domainErrorCount: domainErrorCount ?? this.domainErrorCount, + pathErrorCount: pathErrorCount ?? this.pathErrorCount, + showSplitScreen: showSplitScreen ?? this.showSplitScreen, + filters: filters, + ); + } } class DeepLinksController { - bool get showSplitScreen => showSplitScreenNotifier.value; - List get getLinkDatasByPath => _getFilterredLinks(linkDatasByPath, _searchContentNotifier.value); List get getLinkDatasByDomain => _getFilterredLinks(linkDatasByDomain, _searchContentNotifier.value); + DisplayOptions get displayOptions => displayOptionsNotifier.value; + final selectedLink = ValueNotifier(null); final linkDatasNotifier = ValueNotifier>(allLinkDatas); - final showSplitScreenNotifier = ValueNotifier(false); - final domainErrorCountNotifier = ValueNotifier(0); - final pathErrorCountNotifier = ValueNotifier(0); + + final displayOptionsNotifier = + ValueNotifier(DisplayOptions()); + final _searchContentNotifier = ValueNotifier(''); - final schemeFilterOptionNotifier = - ValueNotifier(SchemeFilterOption.showAll); - final osFilterOptionNotifier = - ValueNotifier(OsFilterOption.showAll); - final statusFilterOptionNotifier = - ValueNotifier(StatusFilterOption.showAll); var linkDatasByDomain = []; var linkDatasByPath = []; @@ -60,8 +96,6 @@ class DeepLinksController { } final List linkDatasByDomainValues = linkDatasByDomainMap.values.toList(); - domainErrorCountNotifier.value = - linkDatasByDomainValues.where((element) => element.domainError).length; linkDatasByDomain = linkDatasByDomainValues; final linkDatasByPathMap = {}; @@ -71,9 +105,15 @@ class DeepLinksController { } final List linkDatasByPathValues = linkDatasByPathMap.values.toList(); - pathErrorCountNotifier.value = - linkDatasByPathValues.where((element) => element.pathError).length; linkDatasByPath = linkDatasByPathValues; + + displayOptionsNotifier.value = displayOptionsNotifier.value.copyWith( + domainErrorCount: linkDatasByDomainValues + .where((element) => element.domainError) + .length, + pathErrorCount: + linkDatasByPathValues.where((element) => element.pathError).length, + ); } set searchContent(String content) { @@ -83,68 +123,64 @@ class DeepLinksController { } void updateFilterOptions({ - SchemeFilterOption? schemeOption, - OsFilterOption? osOption, - StatusFilterOption? statusOption, + required FilterOption option, + required bool value, }) { - if (schemeOption != null) schemeFilterOptionNotifier.value = schemeOption; - if (osOption != null) osFilterOptionNotifier.value = osOption; - if (statusOption != null) statusFilterOptionNotifier.value = statusOption; + displayOptionsNotifier.value = + displayOptionsNotifier.value.updateFilter(option, value); linkDatasNotifier.value = _getFilterredLinks(allLinkDatas, _searchContentNotifier.value); } + void updateDisplayOptions({ + int? domainErrorCount, + int? pathErrorCount, + bool? showSplitScreen, + }) { + displayOptionsNotifier.value = displayOptionsNotifier.value.copyWith( + domainErrorCount: domainErrorCount, + pathErrorCount: pathErrorCount, + showSplitScreen: showSplitScreen, + ); + linkDatasNotifier.value = + _getFilterredLinks(allLinkDatas, _searchContentNotifier.value); + } + List _getFilterredLinks( List linkDatas, String searchContent, ) { - if (searchContent.isNotEmpty) { - linkDatas = linkDatas - .where( - (linkData) => linkData.matchesSearchToken( - RegExp( - searchContent, - caseSensitive: false, - ), + linkDatas = linkDatas.where((linkData) { + if (searchContent.isNotEmpty && + !linkData.matchesSearchToken( + RegExp( + searchContent, + caseSensitive: false, ), - ) - .toList(); - } - switch (statusFilterOptionNotifier.value) { - case StatusFilterOption.failedAllCheck: - linkDatas = linkDatas - .where((linkData) => linkData.domainError && linkData.pathError) - .toList(); - break; - case StatusFilterOption.failedDomainCheck: - linkDatas = - linkDatas.where((linkData) => linkData.domainError).toList(); - break; - case StatusFilterOption.failedPathCheck: - linkDatas = linkDatas.where((linkData) => linkData.pathError).toList(); - break; - case StatusFilterOption.noIssue: - linkDatas = linkDatas - .where((linkData) => !linkData.pathError && !linkData.domainError) - .toList(); - break; - case StatusFilterOption.showAll: - break; - } - switch (osFilterOptionNotifier.value) { - case OsFilterOption.android: - linkDatas = linkDatas - .where((linkData) => linkData.os.contains('Android')) - .toList(); - break; - case OsFilterOption.ios: - linkDatas = - linkDatas.where((linkData) => linkData.os.contains('iOS')).toList(); - break; - case OsFilterOption.showAll: - break; - } + )) { + return false; + } + + if (!((linkData.os.contains('Android') && + displayOptions.filters[FilterOption.android]!) || + (linkData.os.contains('iOS') && + displayOptions.filters[FilterOption.ios]!))) { + return false; + } + + if (!((linkData.domainError && + displayOptions.filters[FilterOption.failedDomainCheck]!) || + (linkData.pathError && + displayOptions.filters[FilterOption.failedPathCheck]!) || + (!linkData.domainError && + !linkData.pathError && + displayOptions.filters[FilterOption.noIssue]!))) { + return false; + } + + return true; + }).toList(); return linkDatas; } diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart index 2aa8ab58df2..69188e0cdcc 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart @@ -22,7 +22,7 @@ class LinkData with SearchableDataMixin { required this.domain, required this.path, required this.os, - this.scheme = const ['Http://', 'Https://'], + this.scheme = const ['http://', 'https://'], this.domainError = false, this.pathError = false, }); @@ -212,43 +212,12 @@ class SchemeColumn extends ColumnData mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Scheme'), - PopupMenuButton( - onSelected: (option) { - controller.updateFilterOptions(schemeOption: option); - }, + PopupMenuButton( itemBuilder: (BuildContext context) { - return [SchemeFilterOption.http, SchemeFilterOption.custom] - .map( - (value) => PopupMenuItem( - value: value, - child: Row( - children: [ - ValueListenableBuilder( - valueListenable: - controller.schemeFilterOptionNotifier, - builder: (context, option, _) => Checkbox( - value: option == value, - onChanged: (bool? checked) { - if (checked!) { - controller.updateFilterOptions( - schemeOption: value); - } else { - controller.updateFilterOptions( - schemeOption: SchemeFilterOption.showAll, - ); - } - }, - ), - ), - if (value == SchemeFilterOption.http) - const Text('Http://, https://'), - if (value == SchemeFilterOption.custom) - const Text('Custom scheme'), - ], - ), - ), - ) - .toList(); + return [ + _buildPopupMenuEntry(controller, FilterOption.http), + _buildPopupMenuEntry(controller, FilterOption.custom), + ]; }, child: Icon( Icons.arrow_drop_down, @@ -282,42 +251,12 @@ class OSColumn extends ColumnData mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('OS'), - PopupMenuButton( - onSelected: (option) { - controller.updateFilterOptions(osOption: option); - }, + PopupMenuButton( itemBuilder: (BuildContext context) { - return OsFilterOption.values - .map( - (value) => PopupMenuItem( - value: value, - child: Row( - children: [ - ValueListenableBuilder( - valueListenable: controller.osFilterOptionNotifier, - builder: (context, option, _) => Checkbox( - value: option == value, - onChanged: (bool? checked) { - if (checked!) { - controller.updateFilterOptions(osOption: value); - } else { - controller.updateFilterOptions( - osOption: OsFilterOption.showAll, - ); - } - }, - ), - ), - if (value == OsFilterOption.showAll) - const Text('Android, iOS'), - if (value == OsFilterOption.android) - const Text('Android'), - if (value == OsFilterOption.ios) const Text('iOS'), - ], - ), - ), - ) - .toList(); + return [ + _buildPopupMenuEntry(controller, FilterOption.android), + _buildPopupMenuEntry(controller, FilterOption.ios), + ]; }, child: Icon( Icons.arrow_drop_down, @@ -339,8 +278,11 @@ class StatusColumn extends ColumnData 'Status', fixedWidthPx: scaleByFontFactor(kDeeplinkTableCellDefaultWidth), ); + DeepLinksController controller; + TableViewType tableViewType; + @override String getValue(LinkData dataObject) { if (dataObject.domainError) { @@ -361,71 +303,38 @@ class StatusColumn extends ColumnData mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Status'), - PopupMenuButton( - onSelected: (option) { - controller.updateFilterOptions(statusOption: option); - }, + PopupMenuButton( itemBuilder: (BuildContext context) { - var statusFilterOptions = []; switch (tableViewType) { case TableViewType.singleUrlView: - statusFilterOptions = [ - StatusFilterOption.failedAllCheck, - StatusFilterOption.failedDomainCheck, - StatusFilterOption.failedPathCheck, - StatusFilterOption.noIssue, + return [ + _buildPopupMenuEntry( + controller, + FilterOption.failedDomainCheck, + ), + _buildPopupMenuEntry( + controller, + FilterOption.failedPathCheck, + ), + _buildPopupMenuEntry(controller, FilterOption.noIssue), ]; - break; case TableViewType.domainView: - statusFilterOptions = [ - StatusFilterOption.failedDomainCheck, - StatusFilterOption.noIssue, + return [ + _buildPopupMenuEntry( + controller, + FilterOption.failedPathCheck, + ), + _buildPopupMenuEntry(controller, FilterOption.noIssue), ]; - break; case TableViewType.pathView: - statusFilterOptions = [ - StatusFilterOption.failedPathCheck, - StatusFilterOption.noIssue, + return [ + _buildPopupMenuEntry( + controller, + FilterOption.failedDomainCheck, + ), + _buildPopupMenuEntry(controller, FilterOption.noIssue), ]; - break; } - return statusFilterOptions - .map( - (value) => PopupMenuItem( - value: value, - child: Row( - children: [ - ValueListenableBuilder( - valueListenable: - controller.statusFilterOptionNotifier, - builder: (context, option, _) => Checkbox( - value: option == value, - onChanged: (bool? checked) { - if (checked!) { - controller.updateFilterOptions( - statusOption: value, - ); - } else { - controller.updateFilterOptions( - statusOption: StatusFilterOption.showAll, - ); - } - }, - ), - ), - if (value == StatusFilterOption.failedAllCheck) - const Text('Failed domain and path checks'), - if (value == StatusFilterOption.failedDomainCheck) - const Text('Failed domain checks'), - if (value == StatusFilterOption.failedPathCheck) - const Text('Failed path checks'), - if (value == StatusFilterOption.noIssue) - const Text('No issues found'), - ], - ), - ), - ) - .toList(); }, child: Icon( Icons.arrow_drop_down, @@ -481,3 +390,27 @@ class NavigationColumn extends ColumnData return const Icon(Icons.arrow_forward); } } + +PopupMenuEntry _buildPopupMenuEntry( + DeepLinksController controller, + FilterOption filterOption, +) { + return PopupMenuItem( + value: filterOption, + child: Row( + children: [ + ValueListenableBuilder( + valueListenable: controller.displayOptionsNotifier, + builder: (context, option, _) => Checkbox( + value: option.filters[filterOption], + onChanged: (bool? checked) => controller.updateFilterOptions( + option: filterOption, + value: checked!, + ), + ), + ), + Text(filterOption.description), + ], + ), + ); +} diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart index 2686301bb61..2fd505e511d 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart @@ -62,66 +62,60 @@ class _DeepLinkPageState extends State Widget build(BuildContext context) { final theme = Theme.of(context); final textTheme = theme.textTheme; - return ValueListenableBuilder( - valueListenable: controller.showSplitScreenNotifier, - builder: (context, showSplitScreen, _) => DefaultTabController( + return ValueListenableBuilder( + valueListenable: controller.displayOptionsNotifier, + builder: (context, displayOptions, _) => DefaultTabController( length: TableViewType.values.length, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - AreaPaneHeader( - title: Text('Validate and fix', style: textTheme.bodyLarge), - ), - showSplitScreen - ? const SizedBox.shrink() - : ValueListenableBuilder( - valueListenable: controller.domainErrorCountNotifier, - builder: (context, domainErrorCount, _) => - ValueListenableBuilder( - valueListenable: controller.pathErrorCountNotifier, - builder: (context, pathErrorCount, _) => - _NotificationCardSection( - domainErrorCount: domainErrorCount, - pathErrorCount: pathErrorCount, - controller: controller, - ), + child: RoundedOutlinedBorder( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AreaPaneHeader( + title: Text('Validate and fix', style: textTheme.bodyLarge), + ), + displayOptions.showSplitScreen + ? const SizedBox.shrink() + : _NotificationCardSection( + domainErrorCount: displayOptions.domainErrorCount, + pathErrorCount: displayOptions.pathErrorCount, + controller: controller, ), - ), - Expanded( - child: Row( - children: [ - Expanded( - child: _AllDeepLinkDataTable(controller: controller), - ), - if (showSplitScreen) + Expanded( + child: Row( + children: [ Expanded( - child: ValueListenableBuilder( - valueListenable: controller.selectedLink, - builder: (context, selectedLink, _) => TabBarView( - children: [ - _ValidationDetailScreen( - linkData: selectedLink!, - controller: controller, - tableView: TableViewType.domainView, - ), - _ValidationDetailScreen( - linkData: selectedLink, - controller: controller, - tableView: TableViewType.pathView, - ), - _ValidationDetailScreen( - linkData: selectedLink, - controller: controller, - tableView: TableViewType.singleUrlView, - ), - ], + child: _AllDeepLinkDataTable(controller: controller), + ), + if (displayOptions.showSplitScreen) + Expanded( + child: ValueListenableBuilder( + valueListenable: controller.selectedLink, + builder: (context, selectedLink, _) => TabBarView( + children: [ + _ValidationDetailScreen( + linkData: selectedLink!, + controller: controller, + tableView: TableViewType.domainView, + ), + _ValidationDetailScreen( + linkData: selectedLink, + controller: controller, + tableView: TableViewType.pathView, + ), + _ValidationDetailScreen( + linkData: selectedLink, + controller: controller, + tableView: TableViewType.singleUrlView, + ), + ], + ), ), ), - ), - ], + ], + ), ), - ), - ], + ], + ), ), ), ); @@ -137,90 +131,77 @@ class _AllDeepLinkDataTable extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); - final colorScheme = theme.colorScheme; final textTheme = theme.textTheme; return Column( children: [ - Container( - decoration: BoxDecoration( - border: Border.fromBorderSide( - defaultBorderSide(Theme.of(context)), + OutlineDecoration( + child: Padding( + padding: const EdgeInsets.fromLTRB( + defaultSpacing, + denseSpacing, + denseSpacing, + denseSpacing, ), - color: colorScheme.surface, - ), - padding: const EdgeInsets.fromLTRB( - defaultSpacing, - denseSpacing, - denseSpacing, - denseSpacing, - ), - child: Row( - children: [ - Expanded( - child: Text( - 'All deep links', - style: textTheme.bodyLarge, + child: Row( + children: [ + Expanded( + child: Text( + 'All deep links', + style: textTheme.bodyLarge, + ), ), - ), - const SizedBox(width: denseSpacing), - SizedBox( - width: wideSearchFieldWidth, - child: DevToolsClearableTextField( - labelText: '', - hintText: 'Search a URL, domain or path', - prefixIcon: const Icon(Icons.search), - onChanged: (value) { - controller.searchContent = value; - }, + const SizedBox(width: denseSpacing), + SizedBox( + width: wideSearchFieldWidth, + child: DevToolsClearableTextField( + labelText: '', + hintText: 'Search a URL, domain or path', + prefixIcon: const Icon(Icons.search), + onChanged: (value) { + controller.searchContent = value; + }, + ), ), - ), - ], + ], + ), ), ), - Container( - decoration: BoxDecoration( - border: Border.fromBorderSide( - defaultBorderSide(Theme.of(context)), + Row( + children: [ + TabBar( + tabs: [ + Text( + 'Domain view', + style: textTheme.bodyLarge, + ), + Text( + 'Path view', + style: textTheme.bodyLarge, + ), + Text( + 'Single URL view', + style: textTheme.bodyLarge, + ), + ], + tabAlignment: TabAlignment.start, + isScrollable: true, ), - color: colorScheme.surface, - ), - child: Row( - children: [ - TabBar( - tabs: [ - Text( - 'Domain view', - style: textTheme.bodyLarge, - ), - Text( - 'Path view', - style: textTheme.bodyLarge, - ), - Text( - 'Single URL view', - style: textTheme.bodyLarge, - ), - ], - tabAlignment: TabAlignment.start, - isScrollable: true, - ), - const Spacer(), + const Spacer(), - // TODO: Add functions to these icons. - IconButton( - onPressed: () {}, - icon: Icon(Icons.restart_alt, size: actionsIconSize), - ), - IconButton( - onPressed: () {}, - icon: Icon(Icons.settings, size: actionsIconSize), - ), - IconButton( - onPressed: () {}, - icon: Icon(Icons.help_outline, size: actionsIconSize), - ), - ], - ), + // TODO: Add functions to these icons. + IconButton( + onPressed: () {}, + icon: Icon(Icons.restart_alt, size: actionsIconSize), + ), + IconButton( + onPressed: () {}, + icon: Icon(Icons.settings, size: actionsIconSize), + ), + IconButton( + onPressed: () {}, + icon: Icon(Icons.help_outline, size: actionsIconSize), + ), + ], ), Expanded( child: ValueListenableBuilder>( @@ -266,19 +247,14 @@ class _DataTable extends StatelessWidget { final ColumnData domain = DomainColumn(); final ColumnData path = PathColumn(); - return Container( - decoration: BoxDecoration( - border: Border.fromBorderSide( - defaultBorderSide(Theme.of(context)), - ), - color: Theme.of(context).colorScheme.surface, - ), + return Padding( padding: const EdgeInsets.only(top: denseSpacing), child: FlatTable( keyFactory: (node) => ValueKey(node.toString), data: linkDatas, dataKey: 'deep-links', autoScrollContent: true, + headerColor: Theme.of(context).colorScheme.deeplinkTableHeaderColor, columns: [ if (tableView == TableViewType.domainView) ...[ domain, @@ -291,7 +267,7 @@ class _DataTable extends StatelessWidget { if (tableView == TableViewType.singleUrlView) ...[domain, path], SchemeColumn(controller), OSColumn(controller), - if (!controller.showSplitScreen) ...[ + if (!controller.displayOptionsNotifier.value.showSplitScreen) ...[ StatusColumn(controller, tableView), NavigationColumn(), ], @@ -300,7 +276,7 @@ class _DataTable extends StatelessWidget { defaultSortColumn: tableView == TableViewType.pathView ? path : domain, defaultSortDirection: SortDirection.ascending, onItemSelected: (item) => - controller.showSplitScreenNotifier.value = true, + controller.updateDisplayOptions(showSplitScreen: true), ), ); } @@ -321,90 +297,101 @@ class _ValidationDetailScreen extends StatelessWidget { Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; - return Padding( - padding: const EdgeInsets.symmetric(horizontal: largeSpacing), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + OutlineDecoration( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: largeSpacing), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + tableView == TableViewType.domainView + ? 'Selected domain validation details' + : 'Selected Deep link validation details', + style: textTheme.titleSmall, + ), + IconButton( + onPressed: () => + controller.updateDisplayOptions(showSplitScreen: false), + icon: const Icon(Icons.close), + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: largeSpacing, + vertical: defaultSpacing, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - tableView == TableViewType.domainView - ? 'Selected domain validation details' - : 'Selected Deep link validation details', - style: textTheme.titleSmall, + 'This tool assistants helps you diagnose Universal Links, App Links,' + ' and Custom Schemes in your app. Web check are done for the web association' + ' file on your website. App checks are done for the intent filters in' + ' the manifest and info.plist file, routing issues, URL format, etc.', + style: Theme.of(context).subtleTextStyle, ), - IconButton( - onPressed: () => - controller.showSplitScreenNotifier.value = false, - icon: const Icon(Icons.close), + if (tableView != TableViewType.pathView) ...[ + const SizedBox(height: intermediateSpacing), + Text('Domain check', style: textTheme.titleSmall), + _DomainCheckTable(linkData: linkData), + ], + if (tableView != TableViewType.domainView) ...[ + const SizedBox(height: intermediateSpacing), + Text('Path check (coming soon)', style: textTheme.titleSmall), + _PathCheckTable(), + ], + Align( + alignment: Alignment.bottomRight, + child: FilledButton( + onPressed: () { + controller.initLinkDatas(); + }, + child: const Text('Recheck all'), + ), ), - ], - ), - Text( - 'This tool assistants helps you diagnose Universal Links, App Links,' - ' and Custom Schemes in your app. Web check are done for the web association' - ' file on your website. App checks are done for the intent filters in' - ' the manifest and info.plist file, routing issues, URL format, etc.', - style: textTheme.bodyMedium!.copyWith( - color: colorScheme.onSurface.withOpacity(0.6), - ), - ), - if (tableView != TableViewType.pathView) ...[ - const SizedBox(height: intermediateSpacing), - Text('Domain check', style: textTheme.titleSmall), - _DomainCheckTable(linkData: linkData), - ], - if (tableView != TableViewType.domainView) ...[ - const SizedBox(height: intermediateSpacing), - Text('Path check (coming soon)', style: textTheme.titleSmall), - _PathCheckTable(), - ], - Align( - alignment: Alignment.bottomRight, - child: FilledButton( - onPressed: () { - controller.initLinkDatas(); - }, - child: const Text('Recheck all'), - ), - ), - if (tableView == TableViewType.domainView) ...[ - Text('Associated deep link URL', style: textTheme.titleSmall), - Card( - color: colorScheme.surface, - shape: const RoundedRectangleBorder(), - child: Padding( - padding: const EdgeInsets.all(denseSpacing), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: linkData.path - .map( - (path) => Padding( - padding: const EdgeInsets.symmetric( - vertical: denseRowSpacing, - ), - child: Row( - children: [ - Icon( - Icons.error, - color: colorScheme.error, - size: defaultIconSize, + if (tableView == TableViewType.domainView) ...[ + Text('Associated deep link URL', style: textTheme.titleSmall), + Card( + color: colorScheme.surface, + shape: const RoundedRectangleBorder(), + child: Padding( + padding: const EdgeInsets.all(denseSpacing), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: linkData.path + .map( + (path) => Padding( + padding: const EdgeInsets.symmetric( + vertical: denseRowSpacing, ), - const SizedBox(width: denseSpacing), - Text(path), - ], - ), - ), - ) - .toList(), + child: Row( + children: [ + Icon( + Icons.error, + color: colorScheme.error, + size: defaultIconSize, + ), + const SizedBox(width: denseSpacing), + Text(path), + ], + ), + ), + ) + .toList(), + ), + ), ), - ), - ), - ], - ], - ), + ], + ], + ), + ), + ], ); } } @@ -419,6 +406,9 @@ class _DomainCheckTable extends StatelessWidget { @override Widget build(BuildContext context) { return DataTable( + headingRowColor: MaterialStateProperty.all( + Theme.of(context).colorScheme.deeplinkTableHeaderColor, + ), dataRowColor: MaterialStateProperty.all( Theme.of(context).colorScheme.alternatingBackgroundColor2, ), @@ -482,6 +472,9 @@ class _PathCheckTable extends StatelessWidget { return Opacity( opacity: 0.5, child: DataTable( + headingRowColor: MaterialStateProperty.all( + Theme.of(context).colorScheme.deeplinkTableHeaderColor, + ), dataRowColor: MaterialStateProperty.all( Theme.of(context).colorScheme.alternatingBackgroundColor2, ), @@ -537,56 +530,54 @@ class _NotificationCardSection extends StatelessWidget { final DeepLinksController controller; @override Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; if (domainErrorCount == 0 && domainErrorCount == 0) { return const SizedBox.shrink(); } - return Container( - decoration: BoxDecoration( - border: Border.fromBorderSide(defaultBorderSide(Theme.of(context))), - color: colorScheme.surface, - ), - padding: const EdgeInsets.all(defaultSpacing), - child: Row( - children: [ - if (domainErrorCount > 0) - _NotificationCard( - title: '$domainErrorCount domain not verified', - description: - 'This affects all deep links. Fix issues to make users go directly to your app.', - actionButton: TextButton( - onPressed: () { - // Switch to the domain view. Select the first link with domain error and show the split screen. - DefaultTabController.of(context).index = 0; - controller.selectedLink.value = controller - .getLinkDatasByDomain - .where((element) => element.domainError) - .first; - controller.showSplitScreenNotifier.value = true; - }, - child: const Text('Fix domain'), + return OutlineDecoration( + child: Padding( + padding: const EdgeInsets.all(defaultSpacing), + child: Row( + children: [ + if (domainErrorCount > 0) + _NotificationCard( + title: '$domainErrorCount domain not verified', + description: + 'This affects all deep links. Fix issues to make users go directly to your app.', + actionButton: TextButton( + onPressed: () { + // Switch to the domain view. Select the first link with domain error and show the split screen. + DefaultTabController.of(context).index = 0; + controller.selectedLink.value = controller + .getLinkDatasByDomain + .where((element) => element.domainError) + .first; + controller.updateDisplayOptions(showSplitScreen: true); + }, + child: const Text('Fix domain'), + ), ), - ), - if (domainErrorCount > 0 && pathErrorCount > 0) - const SizedBox(width: defaultSpacing), - if (pathErrorCount > 0) - _NotificationCard( - title: '$pathErrorCount path not working', - description: - 'Fix these path to make sure users are directed to your app', - actionButton: TextButton( - onPressed: () { - // Switch to the path view. Select the first link with path error and show the split screen. - DefaultTabController.of(context).index = 1; - controller.selectedLink.value = controller.getLinkDatasByPath - .where((element) => element.pathError) - .first; - controller.showSplitScreenNotifier.value = true; - }, - child: const Text('Fix path'), + if (domainErrorCount > 0 && pathErrorCount > 0) + const SizedBox(width: defaultSpacing), + if (pathErrorCount > 0) + _NotificationCard( + title: '$pathErrorCount path not working', + description: + 'Fix these path to make sure users are directed to your app', + actionButton: TextButton( + onPressed: () { + // Switch to the path view. Select the first link with path error and show the split screen. + DefaultTabController.of(context).index = 1; + controller.selectedLink.value = controller + .getLinkDatasByPath + .where((element) => element.pathError) + .first; + controller.updateDisplayOptions(showSplitScreen: true); + }, + child: const Text('Fix path'), + ), ), - ), - ], + ], + ), ), ); } @@ -634,9 +625,7 @@ class _NotificationCard extends StatelessWidget { ), Text( description, - style: textTheme.bodyMedium!.copyWith( - color: colorScheme.onSurface.withOpacity(0.6), - ), + style: Theme.of(context).subtleTextStyle, ), Expanded( child: Align( diff --git a/packages/devtools_app/lib/src/shared/table/table.dart b/packages/devtools_app/lib/src/shared/table/table.dart index b1ba673d49f..15426c9b729 100644 --- a/packages/devtools_app/lib/src/shared/table/table.dart +++ b/packages/devtools_app/lib/src/shared/table/table.dart @@ -113,6 +113,7 @@ class FlatTable extends StatefulWidget { this.includeColumnGroupHeaders = true, this.tallHeaders = false, this.sizeColumnsToFit = true, + this.headerColor, ValueNotifier? selectionNotifier, }) : selectionNotifier = selectionNotifier ?? ValueNotifier(null), super(key: key); @@ -151,6 +152,11 @@ class FlatTable extends StatefulWidget { /// support multiline text. final bool tallHeaders; + /// The background color of the header. + /// + /// If null, defaults to `Theme.of(context).canvasColor`. + final Color? headerColor; + /// Data set to show as rows in this table. final List data; @@ -319,6 +325,7 @@ class FlatTableState extends State> with AutoDisposeMixin { rowItemExtent: defaultRowHeight, preserveVerticalScrollPosition: widget.preserveVerticalScrollPosition, tallHeaders: widget.tallHeaders, + headerColor: widget.headerColor, ); if (widget.sizeColumnsToFit || tableController.columnWidths == null) { return LayoutBuilder( @@ -424,6 +431,7 @@ class TreeTable> extends StatefulWidget { this.preserveVerticalScrollPosition = false, this.displayTreeGuidelines = false, this.tallHeaders = false, + this.headerColor, ValueNotifier>? selectionNotifier, }) : selectionNotifier = selectionNotifier ?? ValueNotifier>(Selection.empty()), @@ -498,6 +506,11 @@ class TreeTable> extends StatefulWidget { /// support multiline text. final bool tallHeaders; + /// The background color of the header. + /// + /// If null, defaults to `Theme.of(context).canvasColor`. + final Color? headerColor; + @override TreeTableState createState() => TreeTableState(); } @@ -658,6 +671,7 @@ class TreeTableState> extends State> selectionNotifier: widget.selectionNotifier, preserveVerticalScrollPosition: widget.preserveVerticalScrollPosition, tallHeaders: widget.tallHeaders, + headerColor: widget.headerColor, ); } @@ -837,6 +851,7 @@ class _Table extends StatefulWidget { this.activeSearchMatchNotifier, this.rowItemExtent, this.tallHeaders = false, + this.headerColor, }) : super(key: key); final TableControllerBase tableController; @@ -850,6 +865,7 @@ class _Table extends StatefulWidget { final ValueListenable? activeSearchMatchNotifier; final bool preserveVerticalScrollPosition; final bool tallHeaders; + final Color? headerColor; @override _TableState createState() => _TableState(); @@ -1048,6 +1064,7 @@ class _TableState extends State<_Table> with AutoDisposeMixin { widget.tableController.secondarySortColumn, onSortChanged: widget.tableController.sortDataAndNotify, tall: widget.tallHeaders, + backgroundColor: widget.headerColor, ), TableRow.tableColumnHeader( key: const Key('Table header'), @@ -1061,6 +1078,7 @@ class _TableState extends State<_Table> with AutoDisposeMixin { secondarySortColumn: widget.tableController.secondarySortColumn, onSortChanged: widget.tableController.sortDataAndNotify, tall: widget.tallHeaders, + backgroundColor: widget.headerColor, ), if (pinnedData.isNotEmpty) ...[ SizedBox( @@ -1195,13 +1213,13 @@ class TableRow extends StatefulWidget { this.secondarySortColumn, this.onPressed, this.tall = false, + this.backgroundColor, }) : node = null, isExpanded = false, isExpandable = false, isSelected = false, expandableColumn = null, isShown = true, - backgroundColor = null, searchMatchesNotifier = null, activeSearchMatchNotifier = null, displayTreeGuidelines = false, @@ -1221,6 +1239,7 @@ class TableRow extends StatefulWidget { this.secondarySortColumn, this.onPressed, this.tall = false, + this.backgroundColor, }) : node = null, isExpanded = false, isExpandable = false, @@ -1228,7 +1247,6 @@ class TableRow extends StatefulWidget { expandableColumn = null, columns = const [], isShown = true, - backgroundColor = null, searchMatchesNotifier = null, activeSearchMatchNotifier = null, displayTreeGuidelines = false, diff --git a/packages/devtools_app/lib/src/shared/ui/colors.dart b/packages/devtools_app/lib/src/shared/ui/colors.dart index e3c44ce819a..473aeccae35 100644 --- a/packages/devtools_app/lib/src/shared/ui/colors.dart +++ b/packages/devtools_app/lib/src/shared/ui/colors.dart @@ -148,5 +148,6 @@ extension DevToolsColorExtension on ColorScheme { Color get green => const Color.fromARGB(255, 156, 233, 195); Color get overlayShadowColor => const Color.fromRGBO(0, 0, 0, 0.5); - Color get deeplinkUnavailableColor => const Color(0xFFFE7C04); + Color get deeplinkUnavailableColor => const Color(0xFFFE7C04); + Color get deeplinkTableHeaderColor => Colors.black; } From 8c26270d90c916c741186e89ac8e8c36bae7c9a7 Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Tue, 24 Oct 2023 18:59:48 -0700 Subject: [PATCH 21/47] 2 --- .../deep_link_list_view.dart | 639 ++++++++++++++---- .../deep_links_controller.dart | 83 ++- .../deep_links_model.dart | 91 +-- .../deep_links_screen.dart | 192 ------ 4 files changed, 577 insertions(+), 428 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart index 8f1508c22a4..1321692a8be 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart @@ -17,6 +17,8 @@ import '../../shared/utils.dart'; import 'deep_links_controller.dart'; import 'deep_links_model.dart'; +const _kNotificationCardSize = Size(475, 132); + enum TableViewType { domainView, pathView, @@ -47,6 +49,7 @@ class _DeepLinkListViewState extends State // If not found, default to 0. releaseVariantIndex = max(releaseVariantIndex, 0); controller.selectedVariantIndex.value = releaseVariantIndex; + controller.initLinkDatas(); }); } @@ -54,13 +57,15 @@ class _DeepLinkListViewState extends State Widget build(BuildContext context) { return DefaultTabController( length: TableViewType.values.length, - child: const Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _DeepLinkListViewTopPanel(), - SizedBox(height: denseSpacing), - Expanded(child: _DeepLinkListViewMainPanel()), - ], + child: const RoundedOutlinedBorder( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _DeepLinkListViewTopPanel(), + SizedBox(height: denseSpacing), + Expanded(child: _DeepLinkListViewMainPanel()), + ], + ), ), ); } @@ -72,112 +77,68 @@ class _DeepLinkListViewMainPanel extends StatelessWidget { @override Widget build(BuildContext context) { final controller = Provider.of(context); - return ValueListenableBuilder?>( - valueListenable: controller.linkDatasNotifier, - builder: (context, linkDatas, _) { - if (linkDatas == null) { - return const CenteredCircularProgressIndicator(); - } - return Column( - children: [ - AreaPaneHeader( - title: Text( - 'All deep links', - style: Theme.of(context).textTheme.bodyLarge, + final textTheme = Theme.of(context).textTheme; + + return ValueListenableBuilder( + valueListenable: controller.displayOptionsNotifier, + builder: (context, displayOptions, _) => + ValueListenableBuilder?>( + valueListenable: controller.linkDatasNotifier, + builder: (context, linkDatas, _) { + if (linkDatas == null) { + return const CenteredCircularProgressIndicator(); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AreaPaneHeader( + title: Text('Validate and fix', style: textTheme.bodyLarge), ), - actions: [ - SizedBox( - width: wideSearchFieldWidth, - child: DevToolsClearableTextField( - labelText: '', - hintText: 'Search a URL, domain or path', - prefixIcon: const Icon(Icons.search), - onChanged: (value) { - controller.searchContent = value; - }, - ), - ), - ], - ), - const SizedBox(height: denseSpacing), - const TabBar( - tabs: [ - Text('Domain view'), - Text('Path view'), - Text('Single URL view'), - ], - tabAlignment: TabAlignment.start, - isScrollable: true, - ), - Expanded( - child: ValueListenableBuilder( - valueListenable: controller.showSpitScreenNotifier, - builder: (context, showSpitScreen, _) => TabBarView( - children: [ - _DataTableWithValidationDetails( - tableView: TableViewType.domainView, - linkDatas: controller.getLinkDatasByDomain, + displayOptions.showSplitScreen + ? const SizedBox.shrink() + : _NotificationCardSection( + domainErrorCount: displayOptions.domainErrorCount, + pathErrorCount: displayOptions.pathErrorCount, controller: controller, - showSpitScreen: showSpitScreen, ), - _DataTableWithValidationDetails( - tableView: TableViewType.pathView, - linkDatas: controller.getLinkDatasByPath, - controller: controller, - showSpitScreen: showSpitScreen, - ), - _DataTableWithValidationDetails( - tableView: TableViewType.singleUrlView, - linkDatas: linkDatas, - controller: controller, - showSpitScreen: showSpitScreen, + Expanded( + child: Row( + children: [ + Expanded( + child: _AllDeepLinkDataTable(controller: controller), ), + if (displayOptions.showSplitScreen) + Expanded( + child: ValueListenableBuilder( + valueListenable: controller.selectedLink, + builder: (context, selectedLink, _) => TabBarView( + children: [ + _ValidationDetailScreen( + linkData: selectedLink!, + controller: controller, + tableView: TableViewType.domainView, + ), + _ValidationDetailScreen( + linkData: selectedLink, + controller: controller, + tableView: TableViewType.pathView, + ), + _ValidationDetailScreen( + linkData: selectedLink, + controller: controller, + tableView: TableViewType.singleUrlView, + ), + ], + ), + ), + ), ], ), ), - ), - ], - ); - }, - ); - } -} - -class _DataTableWithValidationDetails extends StatelessWidget { - const _DataTableWithValidationDetails({ - required this.linkDatas, - required this.tableView, - required this.controller, - required this.showSpitScreen, - }); - final List linkDatas; - final TableViewType tableView; - final DeepLinksController controller; - final bool showSpitScreen; - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Expanded( - child: _DataTable( - tableView: tableView, - linkDatas: linkDatas, - controller: controller, - ), - ), - if (showSpitScreen) - Expanded( - child: ValueListenableBuilder( - valueListenable: controller.selectedLink, - builder: (context, selectedLink, _) => _ValidationDetailScreen( - tableView: tableView, - linkData: selectedLink!, - controller: controller, - ), - ), - ), - ], + ], + ); + }, + ), ); } } @@ -197,25 +158,37 @@ class _DataTable extends StatelessWidget { final ColumnData domain = DomainColumn(); final ColumnData path = PathColumn(); - return FlatTable( - keyFactory: (node) => ValueKey(node.toString), - data: linkDatas, - dataKey: 'deep-links', - autoScrollContent: true, - columns: [ - if (tableView != TableViewType.pathView) domain, - if (tableView != TableViewType.domainView) path, - SchemeColumn(), - OSColumn(), - if (!controller.showSpitScreen) ...[ - StatusColumn(), - NavigationColumn(), + return Padding( + padding: const EdgeInsets.only(top: denseSpacing), + child: FlatTable( + keyFactory: (node) => ValueKey(node.toString), + data: linkDatas, + dataKey: 'deep-links', + autoScrollContent: true, + headerColor: Theme.of(context).colorScheme.deeplinkTableHeaderColor, + columns: [ + if (tableView == TableViewType.domainView) ...[ + domain, + NumberOfAssociatedPathColumn(), + ], + if (tableView == TableViewType.pathView) ...[ + path, + NumberOfAssociatedDomainColumn(), + ], + if (tableView == TableViewType.singleUrlView) ...[domain, path], + SchemeColumn(controller), + OSColumn(controller), + if (!controller.displayOptionsNotifier.value.showSplitScreen) ...[ + StatusColumn(controller, tableView), + NavigationColumn(), + ], ], - ], - selectionNotifier: controller.selectedLink, - defaultSortColumn: tableView == TableViewType.pathView ? path : domain, - defaultSortDirection: SortDirection.ascending, - onItemSelected: (item) => controller.showSpitScreenNotifier.value = true, + selectionNotifier: controller.selectedLink, + defaultSortColumn: tableView == TableViewType.pathView ? path : domain, + defaultSortDirection: SortDirection.ascending, + onItemSelected: (item) => + controller.updateDisplayOptions(showSplitScreen: true), + ), ); } } @@ -233,33 +206,103 @@ class _ValidationDetailScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: largeSpacing), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + final colorScheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + OutlineDecoration( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: largeSpacing), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + tableView == TableViewType.domainView + ? 'Selected domain validation details' + : 'Selected Deep link validation details', + style: textTheme.titleSmall, + ), + IconButton( + onPressed: () => + controller.updateDisplayOptions(showSplitScreen: false), + icon: const Icon(Icons.close), + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: largeSpacing, + vertical: defaultSpacing, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text('Selected deep link validation details'), - IconButton( - onPressed: () => - controller.showSpitScreenNotifier.value = false, - icon: const Icon(Icons.close), + Text( + 'This tool assistants helps you diagnose Universal Links, App Links,' + ' and Custom Schemes in your app. Web check are done for the web association' + ' file on your website. App checks are done for the intent filters in' + ' the manifest and info.plist file, routing issues, URL format, etc.', + style: Theme.of(context).subtleTextStyle, ), + if (tableView != TableViewType.pathView) ...[ + const SizedBox(height: intermediateSpacing), + Text('Domain check', style: textTheme.titleSmall), + _DomainCheckTable(linkData: linkData), + ], + if (tableView != TableViewType.domainView) ...[ + const SizedBox(height: intermediateSpacing), + Text('Path check (coming soon)', style: textTheme.titleSmall), + _PathCheckTable(), + ], + Align( + alignment: Alignment.bottomRight, + child: FilledButton( + onPressed: () { + controller.initLinkDatas(); + }, + child: const Text('Recheck all'), + ), + ), + if (tableView == TableViewType.domainView) ...[ + Text('Associated deep link URL', style: textTheme.titleSmall), + Card( + color: colorScheme.surface, + shape: const RoundedRectangleBorder(), + child: Padding( + padding: const EdgeInsets.all(denseSpacing), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: linkData.associatedPath + .map( + (path) => Padding( + padding: const EdgeInsets.symmetric( + vertical: denseRowSpacing, + ), + child: Row( + children: [ + Icon( + Icons.error, + color: colorScheme.error, + size: defaultIconSize, + ), + const SizedBox(width: denseSpacing), + Text(path), + ], + ), + ), + ) + .toList(), + ), + ), + ), + ], ], ), - Text( - 'This tool helps you diagnose Universal Links, App Links,' - ' and Custom Schemes in your app. Web checks are done for the web association' - ' files on your website. App checks are done for the intent filters in' - ' the manifest and info.plist file, routing issues, URL format, etc.', - style: Theme.of(context).textTheme.bodySmall, - ), - const Text('Domain check'), - Expanded(child: _DomainCheckTable(linkData: linkData)), - ], - ), + ), + ], ); } } @@ -274,6 +317,12 @@ class _DomainCheckTable extends StatelessWidget { @override Widget build(BuildContext context) { return DataTable( + headingRowColor: MaterialStateProperty.all( + Theme.of(context).colorScheme.deeplinkTableHeaderColor, + ), + dataRowColor: MaterialStateProperty.all( + Theme.of(context).colorScheme.alternatingBackgroundColor2, + ), columns: const [ DataColumn(label: Text('OS')), DataColumn(label: Text('Issue type')), @@ -382,3 +431,299 @@ class _AndroidVariantDropdown extends StatelessWidget { ); } } + +class _AllDeepLinkDataTable extends StatelessWidget { + const _AllDeepLinkDataTable({ + required this.controller, + }); + final DeepLinksController controller; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final textTheme = theme.textTheme; + return Column( + children: [ + OutlineDecoration( + child: Padding( + padding: const EdgeInsets.fromLTRB( + defaultSpacing, + denseSpacing, + denseSpacing, + denseSpacing, + ), + child: Row( + children: [ + Expanded( + child: Text( + 'All deep links', + style: textTheme.bodyLarge, + ), + ), + const SizedBox(width: denseSpacing), + SizedBox( + width: wideSearchFieldWidth, + child: DevToolsClearableTextField( + labelText: '', + hintText: 'Search a URL, domain or path', + prefixIcon: const Icon(Icons.search), + onChanged: (value) { + controller.searchContent = value; + }, + ), + ), + ], + ), + ), + ), + Row( + children: [ + TabBar( + tabs: [ + Text( + 'Domain view', + style: textTheme.bodyLarge, + ), + Text( + 'Path view', + style: textTheme.bodyLarge, + ), + Text( + 'Single URL view', + style: textTheme.bodyLarge, + ), + ], + tabAlignment: TabAlignment.start, + isScrollable: true, + ), + const Spacer(), + + // TODO: Add functions to these icons. + IconButton( + onPressed: () {}, + icon: Icon(Icons.restart_alt, size: actionsIconSize), + ), + IconButton( + onPressed: () {}, + icon: Icon(Icons.settings, size: actionsIconSize), + ), + IconButton( + onPressed: () {}, + icon: Icon(Icons.help_outline, size: actionsIconSize), + ), + ], + ), + Expanded( + child: ValueListenableBuilder?>( + valueListenable: controller.linkDatasNotifier, + builder: (context, linkDatas, _) => TabBarView( + children: [ + _DataTable( + tableView: TableViewType.domainView, + linkDatas: controller.getLinkDatasByDomain, + controller: controller, + ), + _DataTable( + tableView: TableViewType.pathView, + linkDatas: controller.getLinkDatasByPath, + controller: controller, + ), + _DataTable( + tableView: TableViewType.singleUrlView, + linkDatas: linkDatas!, + controller: controller, + ), + ], + ), + ), + ), + ], + ); + } +} + +class _PathCheckTable extends StatelessWidget { + @override + Widget build(BuildContext context) { + final notAvailableCell = DataCell( + Text( + 'Not available', + style: TextStyle( + color: Theme.of(context).colorScheme.deeplinkUnavailableColor, + ), + ), + ); + return Opacity( + opacity: 0.5, + child: DataTable( + headingRowColor: MaterialStateProperty.all( + Theme.of(context).colorScheme.deeplinkTableHeaderColor, + ), + dataRowColor: MaterialStateProperty.all( + Theme.of(context).colorScheme.alternatingBackgroundColor2, + ), + columns: const [ + DataColumn(label: Text('OS')), + DataColumn(label: Text('Issue type')), + DataColumn(label: Text('Status')), + ], + rows: [ + DataRow( + cells: [ + const DataCell(Text('Android')), + const DataCell(Text('Intent filter')), + notAvailableCell, + ], + ), + DataRow( + cells: [ + const DataCell(Text('iOS')), + const DataCell(Text('Associated domain')), + notAvailableCell, + ], + ), + DataRow( + cells: [ + const DataCell(Text('Android, iOS')), + const DataCell(Text('URL format')), + notAvailableCell, + ], + ), + DataRow( + cells: [ + const DataCell(Text('Android, iOS')), + const DataCell(Text('Routing')), + notAvailableCell, + ], + ), + ], + ), + ); + } +} + +class _NotificationCardSection extends StatelessWidget { + const _NotificationCardSection({ + required this.domainErrorCount, + required this.pathErrorCount, + required this.controller, + }); + + final int domainErrorCount; + final int pathErrorCount; + final DeepLinksController controller; + @override + Widget build(BuildContext context) { + if (domainErrorCount == 0 && domainErrorCount == 0) { + return const SizedBox.shrink(); + } + return OutlineDecoration( + child: Padding( + padding: const EdgeInsets.all(defaultSpacing), + child: Row( + children: [ + if (domainErrorCount > 0) + _NotificationCard( + title: '$domainErrorCount domain not verified', + description: + 'This affects all deep links. Fix issues to make users go directly to your app.', + actionButton: TextButton( + onPressed: () { + // Switch to the domain view. Select the first link with domain error and show the split screen. + DefaultTabController.of(context).index = 0; + controller.selectedLink.value = controller + .getLinkDatasByDomain + .where((element) => element.domainError) + .first; + controller.updateDisplayOptions(showSplitScreen: true); + }, + child: const Text('Fix domain'), + ), + ), + if (domainErrorCount > 0 && pathErrorCount > 0) + const SizedBox(width: defaultSpacing), + if (pathErrorCount > 0) + _NotificationCard( + title: '$pathErrorCount path not working', + description: + 'Fix these path to make sure users are directed to your app', + actionButton: TextButton( + onPressed: () { + // Switch to the path view. Select the first link with path error and show the split screen. + DefaultTabController.of(context).index = 1; + controller.selectedLink.value = controller + .getLinkDatasByPath + .where((element) => element.pathError) + .first; + controller.updateDisplayOptions(showSplitScreen: true); + }, + child: const Text('Fix path'), + ), + ), + ], + ), + ), + ); + } +} + +class _NotificationCard extends StatelessWidget { + const _NotificationCard({ + required this.title, + required this.description, + required this.actionButton, + }); + + final String title; + final String description; + final Widget actionButton; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + return SizedBox.fromSize( + size: _kNotificationCardSize, + child: Card( + color: colorScheme.surface, + child: Padding( + padding: const EdgeInsets.fromLTRB( + defaultSpacing, + defaultSpacing, + defaultSpacing, + 0, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.error, color: colorScheme.error), + const SizedBox(width: denseSpacing), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: textTheme.bodyMedium! + .copyWith(color: colorScheme.onSurface), + ), + Text( + description, + style: Theme.of(context).subtleTextStyle, + ), + Expanded( + child: Align( + alignment: Alignment.bottomRight, + child: actionButton, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart index b3cd0396178..60cc78dd9cf 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart @@ -11,6 +11,7 @@ import '../../shared/analytics/analytics.dart' as ga; import '../../shared/analytics/constants.dart' as gac; import '../../shared/config_specific/server/server.dart' as server; import 'deep_links_model.dart'; +import 'fake_data.dart'; typedef _DomainAndPath = ({String domain, String path}); @@ -41,23 +42,27 @@ class DisplayOptions { FilterOption.failedDomainCheck: true, FilterOption.failedPathCheck: true, }, + this.searchContent = '', }); int domainErrorCount = 0; int pathErrorCount = 0; bool showSplitScreen = false; + String searchContent; - Map filters = { - for (var item in FilterOption.values) item: true, - }; + Map filters; DisplayOptions updateFilter(FilterOption option, bool value) { - filters[option] = value; + final Map newFilters = + Map.from(filters); + newFilters[option] = value; + return DisplayOptions( domainErrorCount: domainErrorCount, pathErrorCount: pathErrorCount, showSplitScreen: showSplitScreen, - filters: filters, + filters: newFilters, + searchContent: searchContent, ); } @@ -65,12 +70,14 @@ class DisplayOptions { int? domainErrorCount, int? pathErrorCount, bool? showSplitScreen, + String? searchContent, }) { return DisplayOptions( domainErrorCount: domainErrorCount ?? this.domainErrorCount, pathErrorCount: pathErrorCount ?? this.pathErrorCount, showSplitScreen: showSplitScreen ?? this.showSplitScreen, filters: filters, + searchContent: searchContent ?? '', ); } } @@ -85,17 +92,46 @@ class DeepLinksController { List get getLinkDatasByPath { final linkDatasByPath = {}; for (var linkData in linkDatasNotifier.value!) { - linkDatasByPath[linkData.path] = linkData; + final prevoisRecord = linkDatasByPath[linkData.path]; + linkDatasByPath[linkData.path] = LinkData( + domain: linkData.domain, + path: linkData.path, + os: [ + if (prevoisRecord?.os.contains(PlatformOS.android) ?? + false || linkData.os.contains(PlatformOS.android)) + PlatformOS.android, + if (prevoisRecord?.os.contains(PlatformOS.ios) ?? + false || linkData.os.contains(PlatformOS.ios)) + PlatformOS.ios, + ], + associatedDomains: [ + ...prevoisRecord?.associatedDomains ?? [], + linkData.domain, + ], + pathError: linkData.pathError, + ); } - return linkDatasByPath.values.toList(); + + return _getFilterredLinks(linkDatasByPath.values.toList()); } List get getLinkDatasByDomain { final linkDatasByDomain = {}; + for (var linkData in linkDatasNotifier.value!) { - linkDatasByDomain[linkData.domain] = linkData; + final prevoisRecord = linkDatasByDomain[linkData.domain]; + linkDatasByDomain[linkData.domain] = LinkData( + domain: linkData.domain, + path: linkData.path, + os: linkData.os, + associatedPath: [ + ...prevoisRecord?.associatedPath ?? [], + linkData.path, + ], + domainError: linkData.domainError, + ); } - return linkDatasByDomain.values.toList(); + return _getFilterredLinks(linkDatasByDomain.values.toList()); } final Map _androidAppLinks = {}; @@ -152,14 +188,12 @@ class DeepLinksController { final selectedProject = ValueNotifier(null); final selectedLink = ValueNotifier(null); - final linkDatasNotifier = ValueNotifier?>(); + final linkDatasNotifier = ValueNotifier?>(null); final displayOptionsNotifier = ValueNotifier(DisplayOptions()); - final _searchContentNotifier = ValueNotifier(''); - - void initLinkDatas() { + void _updateLinks() { linkDatasNotifier.value = _allLinkDatas; displayOptionsNotifier.value = displayOptionsNotifier.value.copyWith( @@ -170,15 +204,10 @@ class DeepLinksController { ); } - void _updateLinks() { - final searchContent = _searchContentNotifier.value; - linkDatasNotifier.value = _getFilterredLinks(_allLinkDatas, searchContent); - } - set searchContent(String content) { - _searchContentNotifier.value = content; - linkDatasNotifier.value = - _getFilterredLinks(_allLinkDatas, _searchContentNotifier.value); + displayOptionsNotifier.value = + displayOptionsNotifier.value.copyWith(searchContent: content); + linkDatasNotifier.value = _getFilterredLinks(_allLinkDatas); } void updateFilterOptions({ @@ -188,8 +217,7 @@ class DeepLinksController { displayOptionsNotifier.value = displayOptionsNotifier.value.updateFilter(option, value); - linkDatasNotifier.value = - _getFilterredLinks(_allLinkDatas, _searchContentNotifier.value); + linkDatasNotifier.value = _getFilterredLinks(_allLinkDatas); } void updateDisplayOptions({ @@ -202,14 +230,11 @@ class DeepLinksController { pathErrorCount: pathErrorCount, showSplitScreen: showSplitScreen, ); - linkDatasNotifier.value = - _getFilterredLinks(_allLinkDatas, _searchContentNotifier.value); + linkDatasNotifier.value = _getFilterredLinks(_allLinkDatas); } - List _getFilterredLinks( - List linkDatas, - String searchContent, - ) { + List _getFilterredLinks(List linkDatas) { + final String searchContent = displayOptions.searchContent; linkDatas = linkDatas.where((linkData) { if (searchContent.isNotEmpty && !linkData.matchesSearchToken( diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart index a413ead7dbe..c34c0c75f74 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart @@ -12,13 +12,16 @@ import '../../shared/ui/colors.dart'; import '../../shared/ui/search.dart'; import 'deep_links_controller.dart'; -import 'deep_links_screen.dart'; +import 'deep_link_list_view.dart'; const kDeeplinkTableCellDefaultWidth = 200.0; enum PlatformOS { - android, - ios, + android('Android'), + ios('iOS'); + + const PlatformOS(this.description); + final String description; } /// Contains all data relevant to a deep link. @@ -30,6 +33,8 @@ class LinkData with SearchableDataMixin { this.scheme = const ['http://', 'https://'], this.domainError = false, this.pathError = false, + this.associatedPath = const [], + this.associatedDomains = const [], }); final String path; @@ -39,6 +44,9 @@ class LinkData with SearchableDataMixin { final bool domainError; final bool pathError; + final List associatedPath; + final List associatedDomains; + @override bool matchesSearchToken(RegExp regExpSearch) { return domain.caseInsensitiveContains(regExpSearch) || @@ -47,30 +55,6 @@ class LinkData with SearchableDataMixin { @override String toString() => 'LinkData($domain $path)'; - - // // Used for [TableViewType.pathView]. - // LinkData mergebyPath(LinkData? linkdata) { - // if (linkdata == null) return this; - // assert(path == linkdata.path); - // return LinkData( - // domain: [...domain, ...linkdata.domain], - // path: path, - // os: os, - // pathError: pathError, - // ); - // } - - // // Used for [TableViewType.domainView]. - // LinkData mergebyDomain(LinkData? linkdata) { - // if (linkdata == null) return this; - // assert(domain.single == linkdata.domain.single); - // return LinkData( - // domain: domain, - // path: [...path, ...linkdata.path], - // os: os, - // domainError: domainError, - // ); - // } } class _ErrorAwareText extends StatelessWidget { @@ -184,7 +168,8 @@ class NumberOfAssociatedPathColumn extends ColumnData { ); @override - String getValue(LinkData dataObject) => dataObject.path.length.toString(); + String getValue(LinkData dataObject) => + dataObject.associatedPath.length.toString(); } class NumberOfAssociatedDomainColumn extends ColumnData { @@ -195,7 +180,8 @@ class NumberOfAssociatedDomainColumn extends ColumnData { ); @override - String getValue(LinkData dataObject) => dataObject.domain.length.toString(); + String getValue(LinkData dataObject) => + dataObject.associatedDomains.length.toString(); } class SchemeColumn extends ColumnData @@ -273,7 +259,8 @@ class OSColumn extends ColumnData } @override - String getValue(LinkData dataObject) => dataObject.os.join(','); + String getValue(LinkData dataObject) => + dataObject.os.map((e) => e.description).toList().join(','); } class StatusColumn extends ColumnData @@ -310,36 +297,19 @@ class StatusColumn extends ColumnData const Text('Status'), PopupMenuButton( itemBuilder: (BuildContext context) { - switch (tableViewType) { - case TableViewType.singleUrlView: - return [ - _buildPopupMenuEntry( - controller, - FilterOption.failedDomainCheck, - ), - _buildPopupMenuEntry( - controller, - FilterOption.failedPathCheck, - ), - _buildPopupMenuEntry(controller, FilterOption.noIssue), - ]; - case TableViewType.domainView: - return [ - _buildPopupMenuEntry( - controller, - FilterOption.failedPathCheck, - ), - _buildPopupMenuEntry(controller, FilterOption.noIssue), - ]; - case TableViewType.pathView: - return [ - _buildPopupMenuEntry( - controller, - FilterOption.failedDomainCheck, - ), - _buildPopupMenuEntry(controller, FilterOption.noIssue), - ]; - } + return [ + if (tableViewType != TableViewType.domainView) + _buildPopupMenuEntry( + controller, + FilterOption.failedPathCheck, + ), + if (tableViewType != TableViewType.pathView) + _buildPopupMenuEntry( + controller, + FilterOption.failedDomainCheck, + ), + _buildPopupMenuEntry(controller, FilterOption.noIssue), + ]; }, child: Icon( Icons.arrow_drop_down, @@ -419,6 +389,7 @@ PopupMenuEntry _buildPopupMenuEntry( ), ); } + class FlutterProject { FlutterProject({ required this.path, diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart index 268b3211667..b4becdce884 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart @@ -15,13 +15,7 @@ import 'deep_links_model.dart'; import 'select_project_view.dart'; import 'package:devtools_app_shared/ui.dart'; -const _kNotificationCardSize = Size(475, 132); -enum TableViewType { - domainView, - pathView, - singleUrlView, -} class DeepLinksScreen extends Screen { DeepLinksScreen() : super.fromMetaData(ScreenMetaData.deepLinks); @@ -57,7 +51,6 @@ class _DeepLinkPageState extends State void didChangeDependencies() { super.didChangeDependencies(); if (!initController()) return; - controller.initLinkDatas(); } @override @@ -74,188 +67,3 @@ class _DeepLinkPageState extends State } } -class _PathCheckTable extends StatelessWidget { - @override - Widget build(BuildContext context) { - final notAvailableCell = DataCell( - Text( - 'Not available', - style: TextStyle( - color: Theme.of(context).colorScheme.deeplinkUnavailableColor, - ), - ), - ); - return Opacity( - opacity: 0.5, - child: DataTable( - headingRowColor: MaterialStateProperty.all( - Theme.of(context).colorScheme.deeplinkTableHeaderColor, - ), - dataRowColor: MaterialStateProperty.all( - Theme.of(context).colorScheme.alternatingBackgroundColor2, - ), - columns: const [ - DataColumn(label: Text('OS')), - DataColumn(label: Text('Issue type')), - DataColumn(label: Text('Status')), - ], - rows: [ - DataRow( - cells: [ - const DataCell(Text('Android')), - const DataCell(Text('Intent filter')), - notAvailableCell, - ], - ), - DataRow( - cells: [ - const DataCell(Text('iOS')), - const DataCell(Text('Associated domain')), - notAvailableCell, - ], - ), - DataRow( - cells: [ - const DataCell(Text('Android, iOS')), - const DataCell(Text('URL format')), - notAvailableCell, - ], - ), - DataRow( - cells: [ - const DataCell(Text('Android, iOS')), - const DataCell(Text('Routing')), - notAvailableCell, - ], - ), - ], - ), - ); - } -} - -class _NotificationCardSection extends StatelessWidget { - const _NotificationCardSection({ - required this.domainErrorCount, - required this.pathErrorCount, - required this.controller, - }); - - final int domainErrorCount; - final int pathErrorCount; - final DeepLinksController controller; - @override - Widget build(BuildContext context) { - if (domainErrorCount == 0 && domainErrorCount == 0) { - return const SizedBox.shrink(); - } - return OutlineDecoration( - child: Padding( - padding: const EdgeInsets.all(defaultSpacing), - child: Row( - children: [ - if (domainErrorCount > 0) - _NotificationCard( - title: '$domainErrorCount domain not verified', - description: - 'This affects all deep links. Fix issues to make users go directly to your app.', - actionButton: TextButton( - onPressed: () { - // Switch to the domain view. Select the first link with domain error and show the split screen. - DefaultTabController.of(context).index = 0; - controller.selectedLink.value = controller - .getLinkDatasByDomain - .where((element) => element.domainError) - .first; - controller.updateDisplayOptions(showSplitScreen: true); - }, - child: const Text('Fix domain'), - ), - ), - if (domainErrorCount > 0 && pathErrorCount > 0) - const SizedBox(width: defaultSpacing), - if (pathErrorCount > 0) - _NotificationCard( - title: '$pathErrorCount path not working', - description: - 'Fix these path to make sure users are directed to your app', - actionButton: TextButton( - onPressed: () { - // Switch to the path view. Select the first link with path error and show the split screen. - DefaultTabController.of(context).index = 1; - controller.selectedLink.value = controller - .getLinkDatasByPath - .where((element) => element.pathError) - .first; - controller.updateDisplayOptions(showSplitScreen: true); - }, - child: const Text('Fix path'), - ), - ), - ], - ), - ), - ); - } -} - -class _NotificationCard extends StatelessWidget { - const _NotificationCard({ - required this.title, - required this.description, - required this.actionButton, - }); - - final String title; - final String description; - final Widget actionButton; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final textTheme = Theme.of(context).textTheme; - return SizedBox.fromSize( - size: _kNotificationCardSize, - child: Card( - color: colorScheme.surface, - child: Padding( - padding: const EdgeInsets.fromLTRB( - defaultSpacing, - defaultSpacing, - defaultSpacing, - 0, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon(Icons.error, color: colorScheme.error), - const SizedBox(width: denseSpacing), - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: textTheme.bodyMedium! - .copyWith(color: colorScheme.onSurface), - ), - Text( - description, - style: Theme.of(context).subtleTextStyle, - ), - Expanded( - child: Align( - alignment: Alignment.bottomRight, - child: actionButton, - ), - ), - ], - ), - ), - ], - ), - ), - ), - ); - } -} From 94325b5cc3cd49646ceb88ffb1d6a8bd394edf95 Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Tue, 24 Oct 2023 19:01:00 -0700 Subject: [PATCH 22/47] Update app.dart --- packages/devtools_app/lib/src/app.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devtools_app/lib/src/app.dart b/packages/devtools_app/lib/src/app.dart index cf498078643..2160835611f 100644 --- a/packages/devtools_app/lib/src/app.dart +++ b/packages/devtools_app/lib/src/app.dart @@ -581,7 +581,7 @@ List defaultScreens({ AppSizeScreen(), createController: (_) => AppSizeController(), ), - //if (FeatureFlags.deepLinkValidation) + if (FeatureFlags.deepLinkValidation) DevToolsScreen( DeepLinksScreen(), createController: (_) => DeepLinksController(), From 4b2591d6b48b5c62e1b73bafdce3bd822978a2bb Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Tue, 24 Oct 2023 19:02:13 -0700 Subject: [PATCH 23/47] Update deep_links_screen.dart --- .../src/screens/deep_link_validation/deep_links_screen.dart | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart index b4becdce884..e19be3b7fff 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart @@ -15,8 +15,6 @@ import 'deep_links_model.dart'; import 'select_project_view.dart'; import 'package:devtools_app_shared/ui.dart'; - - class DeepLinksScreen extends Screen { DeepLinksScreen() : super.fromMetaData(ScreenMetaData.deepLinks); @@ -55,7 +53,6 @@ class _DeepLinkPageState extends State @override Widget build(BuildContext context) { - return ValueListenableBuilder( valueListenable: controller.selectedProject, builder: (_, FlutterProject? project, __) { @@ -66,4 +63,3 @@ class _DeepLinkPageState extends State ); } } - From 2917aae81051ad7bf49ab5eab9e61d59e7f2aa2c Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Wed, 25 Oct 2023 11:16:16 -0700 Subject: [PATCH 24/47] small fix --- .../deep_link_list_view.dart | 19 ++++++++++--------- .../deep_links_controller.dart | 5 +---- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart index 1321692a8be..65df0ea060d 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart @@ -167,15 +167,16 @@ class _DataTable extends StatelessWidget { autoScrollContent: true, headerColor: Theme.of(context).colorScheme.deeplinkTableHeaderColor, columns: [ - if (tableView == TableViewType.domainView) ...[ - domain, - NumberOfAssociatedPathColumn(), - ], - if (tableView == TableViewType.pathView) ...[ - path, - NumberOfAssociatedDomainColumn(), - ], - if (tableView == TableViewType.singleUrlView) ...[domain, path], + ...(() { + switch (tableView) { + case TableViewType.domainView: + return [domain, NumberOfAssociatedPathColumn()]; + case TableViewType.pathView: + return [path, NumberOfAssociatedDomainColumn()]; + case TableViewType.singleUrlView: + return [domain, path]; + } + })(), SchemeColumn(controller), OSColumn(controller), if (!controller.displayOptionsNotifier.value.showSplitScreen) ...[ diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart index 60cc78dd9cf..39bd9c2861d 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart @@ -238,10 +238,7 @@ class DeepLinksController { linkDatas = linkDatas.where((linkData) { if (searchContent.isNotEmpty && !linkData.matchesSearchToken( - RegExp( - searchContent, - caseSensitive: false, - ), + RegExp(searchContent, caseSensitive: false), )) { return false; } From ef2f30cb6de94da082e8c84b83224875cb1eda95 Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Wed, 25 Oct 2023 11:19:17 -0700 Subject: [PATCH 25/47] Update deep_link_list_view.dart --- .../screens/deep_link_validation/deep_link_list_view.dart | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart index 65df0ea060d..3d73cb552a8 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart @@ -104,6 +104,7 @@ class _DeepLinkListViewMainPanel extends StatelessWidget { Expanded( child: Row( children: [ + const SizedBox(width: denseSpacing), Expanded( child: _AllDeepLinkDataTable(controller: controller), ), @@ -447,12 +448,7 @@ class _AllDeepLinkDataTable extends StatelessWidget { children: [ OutlineDecoration( child: Padding( - padding: const EdgeInsets.fromLTRB( - defaultSpacing, - denseSpacing, - denseSpacing, - denseSpacing, - ), + padding: const EdgeInsets.all(denseSpacing), child: Row( children: [ Expanded( From eda157f7abed88e4523fba49f7283fb46b54c3b0 Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Wed, 25 Oct 2023 11:20:51 -0700 Subject: [PATCH 26/47] fix --- .../src/screens/deep_link_validation/deep_link_list_view.dart | 3 +-- .../screens/deep_link_validation/deep_links_controller.dart | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart index 3d73cb552a8..d1c082cf2c6 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart @@ -49,7 +49,6 @@ class _DeepLinkListViewState extends State // If not found, default to 0. releaseVariantIndex = max(releaseVariantIndex, 0); controller.selectedVariantIndex.value = releaseVariantIndex; - controller.initLinkDatas(); }); } @@ -263,7 +262,7 @@ class _ValidationDetailScreen extends StatelessWidget { alignment: Alignment.bottomRight, child: FilledButton( onPressed: () { - controller.initLinkDatas(); + controller.updateLinks(); }, child: const Text('Recheck all'), ), diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart index 39bd9c2861d..8de074b038b 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart @@ -158,7 +158,7 @@ class DeepLinksController { }, ); } - _updateLinks(); + updateLinks(); } List get _allLinkDatas { @@ -193,7 +193,7 @@ class DeepLinksController { final displayOptionsNotifier = ValueNotifier(DisplayOptions()); - void _updateLinks() { + void updateLinks() { linkDatasNotifier.value = _allLinkDatas; displayOptionsNotifier.value = displayOptionsNotifier.value.copyWith( From 4cca4e3aebc91e1afa6f7ed80a09d3a7b52c3df4 Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Wed, 25 Oct 2023 11:42:19 -0700 Subject: [PATCH 27/47] lint --- .../screens/deep_link_validation/deep_links_controller.dart | 1 - .../lib/src/screens/deep_link_validation/deep_links_model.dart | 3 +-- .../src/screens/deep_link_validation/deep_links_screen.dart | 2 -- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart index 8de074b038b..66d06e1ad21 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart @@ -11,7 +11,6 @@ import '../../shared/analytics/analytics.dart' as ga; import '../../shared/analytics/constants.dart' as gac; import '../../shared/config_specific/server/server.dart' as server; import 'deep_links_model.dart'; -import 'fake_data.dart'; typedef _DomainAndPath = ({String domain, String path}); diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart index c34c0c75f74..04db92776bb 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart @@ -10,9 +10,8 @@ import '../../shared/table/table.dart'; import '../../shared/table/table_data.dart'; import '../../shared/ui/colors.dart'; import '../../shared/ui/search.dart'; - -import 'deep_links_controller.dart'; import 'deep_link_list_view.dart'; +import 'deep_links_controller.dart'; const kDeeplinkTableCellDefaultWidth = 200.0; diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart index e19be3b7fff..4e8fb23b941 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart @@ -7,13 +7,11 @@ import 'package:flutter/material.dart'; import '../../shared/analytics/analytics.dart' as ga; import '../../shared/analytics/constants.dart' as gac; import '../../shared/screen.dart'; -import '../../shared/ui/colors.dart'; import '../../shared/utils.dart'; import 'deep_link_list_view.dart'; import 'deep_links_controller.dart'; import 'deep_links_model.dart'; import 'select_project_view.dart'; -import 'package:devtools_app_shared/ui.dart'; class DeepLinksScreen extends Screen { DeepLinksScreen() : super.fromMetaData(ScreenMetaData.deepLinks); From d2f1a73b3512ec5a1b6caa9b6760fdd3e6909fe7 Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Tue, 31 Oct 2023 21:22:34 -0700 Subject: [PATCH 28/47] ui updates --- packages/devtools_app/lib/src/app.dart | 2 +- .../deep_link_list_view.dart | 343 +++--------------- .../deep_links_controller.dart | 159 ++++---- .../deep_links_model.dart | 260 +++++++++++-- .../validation_details_view.dart | 249 +++++++++++++ 5 files changed, 613 insertions(+), 400 deletions(-) create mode 100644 packages/devtools_app/lib/src/screens/deep_link_validation/validation_details_view.dart diff --git a/packages/devtools_app/lib/src/app.dart b/packages/devtools_app/lib/src/app.dart index 2160835611f..cf498078643 100644 --- a/packages/devtools_app/lib/src/app.dart +++ b/packages/devtools_app/lib/src/app.dart @@ -581,7 +581,7 @@ List defaultScreens({ AppSizeScreen(), createController: (_) => AppSizeController(), ), - if (FeatureFlags.deepLinkValidation) + //if (FeatureFlags.deepLinkValidation) DevToolsScreen( DeepLinksScreen(), createController: (_) => DeepLinksController(), diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart index d1c082cf2c6..42bb0b42f33 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart @@ -16,8 +16,11 @@ import '../../shared/ui/colors.dart'; import '../../shared/utils.dart'; import 'deep_links_controller.dart'; import 'deep_links_model.dart'; +import 'validation_details_view.dart'; const _kNotificationCardSize = Size(475, 132); +const _kSearchFieldFullWidth = 314.0; +const _kSearchFieldSplitScreenWidth = 280.0; enum TableViewType { domainView, @@ -49,6 +52,7 @@ class _DeepLinkListViewState extends State // If not found, default to 0. releaseVariantIndex = max(releaseVariantIndex, 0); controller.selectedVariantIndex.value = releaseVariantIndex; + controller.updateLinks(); }); } @@ -103,7 +107,6 @@ class _DeepLinkListViewMainPanel extends StatelessWidget { Expanded( child: Row( children: [ - const SizedBox(width: denseSpacing), Expanded( child: _AllDeepLinkDataTable(controller: controller), ), @@ -113,20 +116,20 @@ class _DeepLinkListViewMainPanel extends StatelessWidget { valueListenable: controller.selectedLink, builder: (context, selectedLink, _) => TabBarView( children: [ - _ValidationDetailScreen( + ValidationDetailScreen( linkData: selectedLink!, controller: controller, - tableView: TableViewType.domainView, + viewType: TableViewType.domainView, ), - _ValidationDetailScreen( + ValidationDetailScreen( linkData: selectedLink, controller: controller, - tableView: TableViewType.pathView, + viewType: TableViewType.pathView, ), - _ValidationDetailScreen( + ValidationDetailScreen( linkData: selectedLink, controller: controller, - tableView: TableViewType.singleUrlView, + viewType: TableViewType.singleUrlView, ), ], ), @@ -146,17 +149,17 @@ class _DeepLinkListViewMainPanel extends StatelessWidget { class _DataTable extends StatelessWidget { const _DataTable({ required this.linkDatas, - required this.tableView, + required this.viewType, required this.controller, }); final List linkDatas; - final TableViewType tableView; + final TableViewType viewType; final DeepLinksController controller; @override Widget build(BuildContext context) { - final ColumnData domain = DomainColumn(); - final ColumnData path = PathColumn(); + final ColumnData domain = DomainColumn(controller); + final ColumnData path = PathColumn(controller); return Padding( padding: const EdgeInsets.only(top: denseSpacing), @@ -168,7 +171,7 @@ class _DataTable extends StatelessWidget { headerColor: Theme.of(context).colorScheme.deeplinkTableHeaderColor, columns: [ ...(() { - switch (tableView) { + switch (viewType) { case TableViewType.domainView: return [domain, NumberOfAssociatedPathColumn()]; case TableViewType.pathView: @@ -180,12 +183,12 @@ class _DataTable extends StatelessWidget { SchemeColumn(controller), OSColumn(controller), if (!controller.displayOptionsNotifier.value.showSplitScreen) ...[ - StatusColumn(controller, tableView), + StatusColumn(controller, viewType), NavigationColumn(), ], ], selectionNotifier: controller.selectedLink, - defaultSortColumn: tableView == TableViewType.pathView ? path : domain, + defaultSortColumn: viewType == TableViewType.pathView ? path : domain, defaultSortDirection: SortDirection.ascending, onItemSelected: (item) => controller.updateDisplayOptions(showSplitScreen: true), @@ -194,182 +197,6 @@ class _DataTable extends StatelessWidget { } } -class _ValidationDetailScreen extends StatelessWidget { - const _ValidationDetailScreen({ - required this.linkData, - required this.tableView, - required this.controller, - }); - - final LinkData linkData; - final TableViewType tableView; - final DeepLinksController controller; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final textTheme = Theme.of(context).textTheme; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - OutlineDecoration( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: largeSpacing), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - tableView == TableViewType.domainView - ? 'Selected domain validation details' - : 'Selected Deep link validation details', - style: textTheme.titleSmall, - ), - IconButton( - onPressed: () => - controller.updateDisplayOptions(showSplitScreen: false), - icon: const Icon(Icons.close), - ), - ], - ), - ), - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: largeSpacing, - vertical: defaultSpacing, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'This tool assistants helps you diagnose Universal Links, App Links,' - ' and Custom Schemes in your app. Web check are done for the web association' - ' file on your website. App checks are done for the intent filters in' - ' the manifest and info.plist file, routing issues, URL format, etc.', - style: Theme.of(context).subtleTextStyle, - ), - if (tableView != TableViewType.pathView) ...[ - const SizedBox(height: intermediateSpacing), - Text('Domain check', style: textTheme.titleSmall), - _DomainCheckTable(linkData: linkData), - ], - if (tableView != TableViewType.domainView) ...[ - const SizedBox(height: intermediateSpacing), - Text('Path check (coming soon)', style: textTheme.titleSmall), - _PathCheckTable(), - ], - Align( - alignment: Alignment.bottomRight, - child: FilledButton( - onPressed: () { - controller.updateLinks(); - }, - child: const Text('Recheck all'), - ), - ), - if (tableView == TableViewType.domainView) ...[ - Text('Associated deep link URL', style: textTheme.titleSmall), - Card( - color: colorScheme.surface, - shape: const RoundedRectangleBorder(), - child: Padding( - padding: const EdgeInsets.all(denseSpacing), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: linkData.associatedPath - .map( - (path) => Padding( - padding: const EdgeInsets.symmetric( - vertical: denseRowSpacing, - ), - child: Row( - children: [ - Icon( - Icons.error, - color: colorScheme.error, - size: defaultIconSize, - ), - const SizedBox(width: denseSpacing), - Text(path), - ], - ), - ), - ) - .toList(), - ), - ), - ), - ], - ], - ), - ), - ], - ); - } -} - -class _DomainCheckTable extends StatelessWidget { - const _DomainCheckTable({ - required this.linkData, - }); - - final LinkData linkData; - - @override - Widget build(BuildContext context) { - return DataTable( - headingRowColor: MaterialStateProperty.all( - Theme.of(context).colorScheme.deeplinkTableHeaderColor, - ), - dataRowColor: MaterialStateProperty.all( - Theme.of(context).colorScheme.alternatingBackgroundColor2, - ), - columns: const [ - DataColumn(label: Text('OS')), - DataColumn(label: Text('Issue type')), - DataColumn(label: Text('Status')), - ], - rows: [ - if (linkData.os.contains(PlatformOS.android)) - DataRow( - cells: [ - const DataCell(Text('Android')), - const DataCell(Text('Digital assets link file')), - DataCell( - linkData.domainError - ? Text( - 'Check failed', - style: TextStyle( - color: Theme.of(context).colorScheme.error, - ), - ) - : Text( - 'No issues found', - style: TextStyle( - color: Theme.of(context).colorScheme.green, - ), - ), - ), - ], - ), - if (linkData.os.contains(PlatformOS.ios)) - DataRow( - cells: [ - const DataCell(Text('iOS')), - const DataCell(Text('Apple-App-Site-Association file')), - DataCell( - Text( - 'No issues found', - style: TextStyle(color: Theme.of(context).colorScheme.green), - ), - ), - ], - ), - ], - ); - } -} - class _DeepLinkListViewTopPanel extends StatelessWidget { const _DeepLinkListViewTopPanel(); @@ -446,19 +273,24 @@ class _AllDeepLinkDataTable extends StatelessWidget { return Column( children: [ OutlineDecoration( - child: Padding( - padding: const EdgeInsets.all(denseSpacing), - child: Row( - children: [ - Expanded( + child: Row( + children: [ + Expanded( + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: defaultSpacing), child: Text( 'All deep links', style: textTheme.bodyLarge, ), ), - const SizedBox(width: denseSpacing), - SizedBox( - width: wideSearchFieldWidth, + ), + Padding( + padding: const EdgeInsets.all(denseSpacing), + child: SizedBox( + width: controller.displayOptions.showSplitScreen + ? _kSearchFieldSplitScreenWidth + : _kSearchFieldFullWidth, child: DevToolsClearableTextField( labelText: '', hintText: 'Search a URL, domain or path', @@ -468,46 +300,27 @@ class _AllDeepLinkDataTable extends StatelessWidget { }, ), ), - ], - ), + ), + ], ), ), - Row( - children: [ - TabBar( - tabs: [ - Text( - 'Domain view', - style: textTheme.bodyLarge, - ), - Text( - 'Path view', - style: textTheme.bodyLarge, - ), - Text( - 'Single URL view', - style: textTheme.bodyLarge, - ), - ], - tabAlignment: TabAlignment.start, - isScrollable: true, - ), - const Spacer(), - - // TODO: Add functions to these icons. - IconButton( - onPressed: () {}, - icon: Icon(Icons.restart_alt, size: actionsIconSize), + TabBar( + tabs: [ + Text( + 'Domain view', + style: textTheme.bodyLarge, ), - IconButton( - onPressed: () {}, - icon: Icon(Icons.settings, size: actionsIconSize), + Text( + 'Path view', + style: textTheme.bodyLarge, ), - IconButton( - onPressed: () {}, - icon: Icon(Icons.help_outline, size: actionsIconSize), + Text( + 'Single URL view', + style: textTheme.bodyLarge, ), ], + tabAlignment: TabAlignment.start, + isScrollable: true, ), Expanded( child: ValueListenableBuilder?>( @@ -515,17 +328,17 @@ class _AllDeepLinkDataTable extends StatelessWidget { builder: (context, linkDatas, _) => TabBarView( children: [ _DataTable( - tableView: TableViewType.domainView, + viewType: TableViewType.domainView, linkDatas: controller.getLinkDatasByDomain, controller: controller, ), _DataTable( - tableView: TableViewType.pathView, + viewType: TableViewType.pathView, linkDatas: controller.getLinkDatasByPath, controller: controller, ), _DataTable( - tableView: TableViewType.singleUrlView, + viewType: TableViewType.singleUrlView, linkDatas: linkDatas!, controller: controller, ), @@ -538,66 +351,6 @@ class _AllDeepLinkDataTable extends StatelessWidget { } } -class _PathCheckTable extends StatelessWidget { - @override - Widget build(BuildContext context) { - final notAvailableCell = DataCell( - Text( - 'Not available', - style: TextStyle( - color: Theme.of(context).colorScheme.deeplinkUnavailableColor, - ), - ), - ); - return Opacity( - opacity: 0.5, - child: DataTable( - headingRowColor: MaterialStateProperty.all( - Theme.of(context).colorScheme.deeplinkTableHeaderColor, - ), - dataRowColor: MaterialStateProperty.all( - Theme.of(context).colorScheme.alternatingBackgroundColor2, - ), - columns: const [ - DataColumn(label: Text('OS')), - DataColumn(label: Text('Issue type')), - DataColumn(label: Text('Status')), - ], - rows: [ - DataRow( - cells: [ - const DataCell(Text('Android')), - const DataCell(Text('Intent filter')), - notAvailableCell, - ], - ), - DataRow( - cells: [ - const DataCell(Text('iOS')), - const DataCell(Text('Associated domain')), - notAvailableCell, - ], - ), - DataRow( - cells: [ - const DataCell(Text('Android, iOS')), - const DataCell(Text('URL format')), - notAvailableCell, - ], - ), - DataRow( - cells: [ - const DataCell(Text('Android, iOS')), - const DataCell(Text('Routing')), - notAvailableCell, - ], - ), - ], - ), - ); - } -} - class _NotificationCardSection extends StatelessWidget { const _NotificationCardSection({ required this.domainErrorCount, diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart index 66d06e1ad21..71bd8d8c24a 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart @@ -11,6 +11,7 @@ import '../../shared/analytics/analytics.dart' as ga; import '../../shared/analytics/constants.dart' as gac; import '../../shared/config_specific/server/server.dart' as server; import 'deep_links_model.dart'; +import 'fake_data.dart'; typedef _DomainAndPath = ({String domain, String path}); @@ -27,41 +28,61 @@ enum FilterOption { final String description; } +enum SortingOption { + aToZ('A-Z'), + zToA('Z-A'), + errorOnTop('Error on top'); + + const SortingOption(this.description); + final String description; +} + class DisplayOptions { DisplayOptions({ this.domainErrorCount = 0, this.pathErrorCount = 0, this.showSplitScreen = false, this.filters = const { - FilterOption.http: true, - FilterOption.custom: true, - FilterOption.android: true, - FilterOption.ios: true, - FilterOption.noIssue: true, - FilterOption.failedDomainCheck: true, - FilterOption.failedPathCheck: true, + FilterOption.http, + FilterOption.custom, + FilterOption.android, + FilterOption.ios, + FilterOption.noIssue, + FilterOption.failedDomainCheck, + FilterOption.failedPathCheck, }, this.searchContent = '', + this.domainSortingOption = SortingOption.errorOnTop, + this.pathSortingOption = SortingOption.errorOnTop, }); int domainErrorCount = 0; int pathErrorCount = 0; bool showSplitScreen = false; String searchContent; + SortingOption? domainSortingOption; + SortingOption? pathSortingOption; - Map filters; + Set filters; DisplayOptions updateFilter(FilterOption option, bool value) { - final Map newFilters = - Map.from(filters); - newFilters[option] = value; + + final Set newFilter = Set.from(filters); + + if (value) { + newFilter.add(option); + } else { + newFilter.remove(option); + } return DisplayOptions( domainErrorCount: domainErrorCount, pathErrorCount: pathErrorCount, showSplitScreen: showSplitScreen, - filters: newFilters, + filters: newFilter, searchContent: searchContent, + domainSortingOption: domainSortingOption, + pathSortingOption: pathSortingOption, ); } @@ -70,6 +91,8 @@ class DisplayOptions { int? pathErrorCount, bool? showSplitScreen, String? searchContent, + SortingOption? domainSortingOption, + SortingOption? pathSortingOption, }) { return DisplayOptions( domainErrorCount: domainErrorCount ?? this.domainErrorCount, @@ -77,6 +100,8 @@ class DisplayOptions { showSplitScreen: showSplitScreen ?? this.showSplitScreen, filters: filters, searchContent: searchContent ?? '', + domainSortingOption: domainSortingOption ?? this.domainSortingOption, + pathSortingOption: pathSortingOption ?? this.pathSortingOption, ); } } @@ -142,47 +167,48 @@ class DeepLinksController { } Future _loadAndroidAppLinks() async { - if (!_androidAppLinks.containsKey(selectedVariantIndex.value)) { - final variant = - selectedProject.value!.androidVariants[selectedVariantIndex.value]; - await ga.timeAsync( - gac.deeplink, - gac.AnalyzeFlutterProject.loadAppLinks.name, - asyncOperation: () async { - final result = await server.requestAndroidAppLinkSettings( - selectedProject.value!.path, - buildVariant: variant, - ); - _androidAppLinks[selectedVariantIndex.value] = result; - }, - ); - } + // if (!_androidAppLinks.containsKey(selectedVariantIndex.value)) { + // final variant = + // selectedProject.value!.androidVariants[selectedVariantIndex.value]; + // await ga.timeAsync( + // gac.deeplink, + // gac.AnalyzeFlutterProject.loadAppLinks.name, + // asyncOperation: () async { + // final result = await server.requestAndroidAppLinkSettings( + // selectedProject.value!.path, + // buildVariant: variant, + // ); + // _androidAppLinks[selectedVariantIndex.value] = result; + // }, + // ); + // } updateLinks(); } List get _allLinkDatas { - final appLinks = _androidAppLinks[selectedVariantIndex.value]?.deeplinks; - if (appLinks == null) { - return const []; - } - final domainPathToScheme = <_DomainAndPath, Set>{}; - for (final appLink in appLinks) { - final schemes = domainPathToScheme.putIfAbsent( - (domain: appLink.host, path: appLink.path), - () => {}, - ); - schemes.add(appLink.scheme); - } - return domainPathToScheme.entries - .map( - (entry) => LinkData( - domain: entry.key.domain, - path: entry.key.path, - os: [PlatformOS.android], - scheme: entry.value.toList(), - ), - ) - .toList(); + return allLinkDatas; + // final appLinks = _androidAppLinks[selectedVariantIndex.value]?.deeplinks; + // if (appLinks == null) { + // return const []; + // } + // final domainPathToScheme = <_DomainAndPath, Set>{}; + // for (final appLink in appLinks) { + // final schemes = domainPathToScheme.putIfAbsent( + // (domain: appLink.host, path: appLink.path), + // () => {}, + // ); + // schemes.add(appLink.scheme); + // } + // return domainPathToScheme.entries + // .map( + // (entry) => LinkData( + // domain: entry.key.domain, + // path: entry.key.path, + // os: [PlatformOS.android], + // scheme: entry.value.toList(), + // ), + // ) + // .toList(); } final selectedProject = ValueNotifier(null); @@ -209,26 +235,30 @@ class DeepLinksController { linkDatasNotifier.value = _getFilterredLinks(_allLinkDatas); } - void updateFilterOptions({ - required FilterOption option, - required bool value, - }) { - displayOptionsNotifier.value = - displayOptionsNotifier.value.updateFilter(option, value); - - linkDatasNotifier.value = _getFilterredLinks(_allLinkDatas); - } - void updateDisplayOptions({ int? domainErrorCount, int? pathErrorCount, bool? showSplitScreen, + SortingOption? domainSortingOption, + SortingOption? pathSortingOption, + FilterOption? addedFilter, + FilterOption? removedFilter, }) { displayOptionsNotifier.value = displayOptionsNotifier.value.copyWith( domainErrorCount: domainErrorCount, pathErrorCount: pathErrorCount, showSplitScreen: showSplitScreen, + domainSortingOption: domainSortingOption, + pathSortingOption: pathSortingOption, ); + if (addedFilter != null) { + displayOptionsNotifier.value = + displayOptionsNotifier.value.updateFilter(addedFilter, true); + } if (removedFilter != null) { + displayOptionsNotifier.value = + displayOptionsNotifier.value.updateFilter(removedFilter, false); + } + linkDatasNotifier.value = _getFilterredLinks(_allLinkDatas); } @@ -243,19 +273,20 @@ class DeepLinksController { } if (!((linkData.os.contains(PlatformOS.android) && - displayOptions.filters[FilterOption.android]!) || + displayOptions.filters.contains(FilterOption.android)) || (linkData.os.contains(PlatformOS.ios) && - displayOptions.filters[FilterOption.ios]!))) { + displayOptions.filters.contains(FilterOption.ios)))) { return false; } if (!((linkData.domainError && - displayOptions.filters[FilterOption.failedDomainCheck]!) || + displayOptions.filters + .contains(FilterOption.failedDomainCheck)) || (linkData.pathError && - displayOptions.filters[FilterOption.failedPathCheck]!) || + displayOptions.filters.contains(FilterOption.failedPathCheck)) || (!linkData.domainError && !linkData.pathError && - displayOptions.filters[FilterOption.noIssue]!))) { + displayOptions.filters.contains(FilterOption.noIssue)))) { return false; } diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart index 04db92776bb..d32a72a84a8 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart @@ -60,21 +60,63 @@ class _ErrorAwareText extends StatelessWidget { const _ErrorAwareText({ required this.text, required this.isError, + required this.controller, + required this.link, }); final String text; final bool isError; + final DeepLinksController controller; + final LinkData link; @override Widget build(BuildContext context) { return Row( children: [ if (isError) - Padding( - padding: const EdgeInsets.only(right: denseSpacing), - child: Icon( - Icons.error, - color: Theme.of(context).colorScheme.error, - size: defaultIconSize, + DevToolsTooltip( + padding: const EdgeInsets.only( + top: defaultSpacing, + left: defaultSpacing, + right: defaultSpacing, + ), + preferBelow: true, + richMessage: WidgetSpan( + child: SizedBox( + width: 344, + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + 'This m.shopping.com domain has 1 issue to fix. Fixing this domain will fix ${link.associatedPath.length} associated deep links.', + style: TextStyle( + color: Theme.of(context).colorScheme.tooltipTextColor, + fontSize: defaultFontSize, + ), + ), + TextButton( + onPressed: () { + controller.updateDisplayOptions(showSplitScreen: true); + controller.selectedLink.value = link; + }, + child: Text( + 'Fix this domain', + style: TextStyle( + color: Theme.of(context).colorScheme.inversePrimary, + fontSize: defaultFontSize, + ), + ), + ), + ], + ), + ), + ), + child: Padding( + padding: const EdgeInsets.only(right: denseSpacing), + child: Icon( + Icons.error, + color: Theme.of(context).colorScheme.error, + size: defaultIconSize, + ), ), ), const SizedBox(width: denseSpacing), @@ -88,15 +130,52 @@ class _ErrorAwareText extends StatelessWidget { } class DomainColumn extends ColumnData - implements ColumnRenderer { - DomainColumn() + implements ColumnRenderer, ColumnHeaderRenderer { + DomainColumn(this.controller) : super( 'Domain', fixedWidthPx: scaleByFontFactor(kDeeplinkTableCellDefaultWidth), ); + DeepLinksController controller; + @override - bool get supportsSorting => true; + Widget? buildHeader( + BuildContext context, + Widget Function() defaultHeaderRenderer, + ) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Domain'), + PopupMenuButton( + itemBuilder: (BuildContext context) { + return [ + _buildPopupMenuSortingEntry( + controller, + SortingOption.errorOnTop, + isPath: false, + ), + _buildPopupMenuSortingEntry( + controller, + SortingOption.aToZ, + isPath: false, + ), + _buildPopupMenuSortingEntry( + controller, + SortingOption.zToA, + isPath: false, + ), + ]; + }, + child: Icon( + Icons.arrow_drop_down, + size: actionsIconSize, + ), + ), + ], + ); + } @override String getValue(LinkData dataObject) => dataObject.domain; @@ -110,29 +189,79 @@ class DomainColumn extends ColumnData }) { return _ErrorAwareText( isError: dataObject.domainError, + controller: controller, text: dataObject.domain, + link: dataObject, ); } - // Shows result with error first. + // Default to show result with error first. @override int compare(LinkData a, LinkData b) { - if (a.domainError) return -1; - if (b.domainError) return 1; - return getValue(a).compareTo(getValue(b)); + final SortingOption? sortingOption = + controller.displayOptions.domainSortingOption; + if (sortingOption == null) return 0; + + switch (sortingOption) { + case SortingOption.errorOnTop: + if (a.domainError) return 1; + if (b.domainError) return -1; + return 0; + case SortingOption.aToZ: + return a.domain.compareTo(b.domain); + case SortingOption.zToA: + return b.domain.compareTo(a.domain); + } } } class PathColumn extends ColumnData - implements ColumnRenderer { - PathColumn() + implements ColumnRenderer, ColumnHeaderRenderer { + PathColumn(this.controller) : super( 'Path', fixedWidthPx: scaleByFontFactor(kDeeplinkTableCellDefaultWidth), ); + DeepLinksController controller; + @override - bool get supportsSorting => true; + Widget? buildHeader( + BuildContext context, + Widget Function() defaultHeaderRenderer, + ) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Path'), + PopupMenuButton( + itemBuilder: (BuildContext context) { + return [ + _buildPopupMenuSortingEntry( + controller, + SortingOption.errorOnTop, + isPath: true, + ), + _buildPopupMenuSortingEntry( + controller, + SortingOption.aToZ, + isPath: true, + ), + _buildPopupMenuSortingEntry( + controller, + SortingOption.zToA, + isPath: true, + ), + ]; + }, + child: Icon( + Icons.arrow_drop_down, + size: actionsIconSize, + ), + ), + ], + ); + } @override String getValue(LinkData dataObject) => dataObject.path; @@ -146,16 +275,30 @@ class PathColumn extends ColumnData }) { return _ErrorAwareText( isError: dataObject.pathError, + controller: controller, text: dataObject.path, + link: dataObject, ); } - // Shows result with error first. + // Default to show result with error first. @override int compare(LinkData a, LinkData b) { - if (a.pathError) return -1; - if (b.pathError) return 1; - return getValue(a).compareTo(getValue(b)); + final SortingOption? sortingOption = + controller.displayOptions.pathSortingOption; + + if (sortingOption == null) return 0; + + switch (sortingOption) { + case SortingOption.errorOnTop: + if (a.pathError) return -1; + if (b.pathError) return 1; + return 0; + case SortingOption.aToZ: + return a.path.compareTo(b.path); + case SortingOption.zToA: + return b.path.compareTo(a.path); + } } } @@ -184,7 +327,7 @@ class NumberOfAssociatedDomainColumn extends ColumnData { } class SchemeColumn extends ColumnData - implements ColumnHeaderRenderer { + implements ColumnRenderer, ColumnHeaderRenderer { SchemeColumn(this.controller) : super( 'Scheme', @@ -205,8 +348,8 @@ class SchemeColumn extends ColumnData PopupMenuButton( itemBuilder: (BuildContext context) { return [ - _buildPopupMenuEntry(controller, FilterOption.http), - _buildPopupMenuEntry(controller, FilterOption.custom), + _buildPopupMenuFilterEntry(controller, FilterOption.http), + _buildPopupMenuFilterEntry(controller, FilterOption.custom), ]; }, child: Icon( @@ -219,11 +362,21 @@ class SchemeColumn extends ColumnData } @override - String getValue(LinkData dataObject) => dataObject.scheme.join(','); + Widget build( + BuildContext context, + LinkData dataObject, { + bool isRowSelected = false, + VoidCallback? onPressed, + }) { + return Text(getValue(dataObject)); + } + + @override + String getValue(LinkData dataObject) => dataObject.scheme.join(', '); } class OSColumn extends ColumnData - implements ColumnHeaderRenderer { + implements ColumnRenderer, ColumnHeaderRenderer { OSColumn(this.controller) : super( 'OS', @@ -244,8 +397,8 @@ class OSColumn extends ColumnData PopupMenuButton( itemBuilder: (BuildContext context) { return [ - _buildPopupMenuEntry(controller, FilterOption.android), - _buildPopupMenuEntry(controller, FilterOption.ios), + _buildPopupMenuFilterEntry(controller, FilterOption.android), + _buildPopupMenuFilterEntry(controller, FilterOption.ios), ]; }, child: Icon( @@ -257,14 +410,24 @@ class OSColumn extends ColumnData ); } + @override + Widget build( + BuildContext context, + LinkData dataObject, { + bool isRowSelected = false, + VoidCallback? onPressed, + }) { + return Text(getValue(dataObject)); + } + @override String getValue(LinkData dataObject) => - dataObject.os.map((e) => e.description).toList().join(','); + dataObject.os.map((e) => e.description).toList().join(', '); } class StatusColumn extends ColumnData implements ColumnRenderer, ColumnHeaderRenderer { - StatusColumn(this.controller, this.tableViewType) + StatusColumn(this.controller, this.viewType) : super( 'Status', fixedWidthPx: scaleByFontFactor(kDeeplinkTableCellDefaultWidth), @@ -272,7 +435,7 @@ class StatusColumn extends ColumnData DeepLinksController controller; - TableViewType tableViewType; + TableViewType viewType; @override String getValue(LinkData dataObject) { @@ -297,17 +460,17 @@ class StatusColumn extends ColumnData PopupMenuButton( itemBuilder: (BuildContext context) { return [ - if (tableViewType != TableViewType.domainView) - _buildPopupMenuEntry( + if (viewType != TableViewType.domainView) + _buildPopupMenuFilterEntry( controller, FilterOption.failedPathCheck, ), - if (tableViewType != TableViewType.pathView) - _buildPopupMenuEntry( + if (viewType != TableViewType.pathView) + _buildPopupMenuFilterEntry( controller, FilterOption.failedDomainCheck, ), - _buildPopupMenuEntry(controller, FilterOption.noIssue), + _buildPopupMenuFilterEntry(controller, FilterOption.noIssue), ]; }, child: Icon( @@ -365,7 +528,7 @@ class NavigationColumn extends ColumnData } } -PopupMenuEntry _buildPopupMenuEntry( +PopupMenuEntry _buildPopupMenuFilterEntry( DeepLinksController controller, FilterOption filterOption, ) { @@ -376,10 +539,10 @@ PopupMenuEntry _buildPopupMenuEntry( ValueListenableBuilder( valueListenable: controller.displayOptionsNotifier, builder: (context, option, _) => Checkbox( - value: option.filters[filterOption], - onChanged: (bool? checked) => controller.updateFilterOptions( - option: filterOption, - value: checked!, + value: option.filters.contains(filterOption), + onChanged: (bool? checked) => controller.updateDisplayOptions( + removedFilter: checked! ? null : filterOption, + addedFilter: checked ? filterOption : null, ), ), ), @@ -389,6 +552,23 @@ PopupMenuEntry _buildPopupMenuEntry( ); } +PopupMenuEntry _buildPopupMenuSortingEntry( + DeepLinksController controller, + SortingOption sortingOption, { + required bool isPath, +}) { + return PopupMenuItem( + onTap: () { + controller.updateDisplayOptions( + pathSortingOption: isPath ? sortingOption : null, + domainSortingOption: isPath ? null : sortingOption, + ); + }, + value: sortingOption, + child: Text(sortingOption.description), + ); +} + class FlutterProject { FlutterProject({ required this.path, diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/validation_details_view.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/validation_details_view.dart new file mode 100644 index 00000000000..a532ca56cac --- /dev/null +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/validation_details_view.dart @@ -0,0 +1,249 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:devtools_app_shared/ui.dart'; +import 'package:flutter/material.dart'; + +import '../../shared/ui/colors.dart'; +import 'deep_link_list_view.dart'; +import 'deep_links_controller.dart'; +import 'deep_links_model.dart'; + +class ValidationDetailScreen extends StatelessWidget { + const ValidationDetailScreen({ + super.key, + required this.linkData, + required this.viewType, + required this.controller, + }); + + final LinkData linkData; + final TableViewType viewType; + final DeepLinksController controller; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + return ListView( + children: [ + OutlineDecoration( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: defaultSpacing), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + viewType == TableViewType.domainView + ? 'Selected domain validation details' + : 'Selected Deep link validation details', + style: textTheme.titleSmall, + ), + IconButton( + onPressed: () => + controller.updateDisplayOptions(showSplitScreen: false), + icon: const Icon(Icons.close), + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: largeSpacing, + vertical: defaultSpacing, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'This tool assistants helps you diagnose Universal Links, App Links,' + ' and Custom Schemes in your app. Web check are done for the web association' + ' file on your website. App checks are done for the intent filters in' + ' the manifest and info.plist file, routing issues, URL format, etc.', + style: Theme.of(context).subtleTextStyle, + ), + if (viewType != TableViewType.pathView) ...[ + const SizedBox(height: intermediateSpacing), + Text('Domain check', style: textTheme.titleSmall), + _DomainCheckTable(linkData: linkData), + ], + if (viewType != TableViewType.domainView) ...[ + const SizedBox(height: intermediateSpacing), + Text('Path check (coming soon)', style: textTheme.titleSmall), + _PathCheckTable(), + ], + const SizedBox(height: largeSpacing), + Align( + alignment: Alignment.bottomRight, + child: FilledButton( + onPressed: () { + controller.updateLinks(); + }, + child: const Text('Recheck all'), + ), + ), + if (viewType == TableViewType.domainView) ...[ + Text('Associated deep link URL', style: textTheme.titleSmall), + Card( + color: colorScheme.surface, + shape: const RoundedRectangleBorder(), + child: Padding( + padding: const EdgeInsets.all(denseSpacing), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: linkData.associatedPath + .map( + (path) => Padding( + padding: const EdgeInsets.symmetric( + vertical: denseRowSpacing, + ), + child: Row( + children: [ + if (linkData.domainError) + Icon( + Icons.error, + color: colorScheme.error, + size: defaultIconSize, + ), + const SizedBox(width: denseSpacing), + Text(path), + ], + ), + ), + ) + .toList(), + ), + ), + ), + ], + ], + ), + ), + ], + ); + } +} + +class _DomainCheckTable extends StatelessWidget { + const _DomainCheckTable({ + required this.linkData, + }); + + final LinkData linkData; + + @override + Widget build(BuildContext context) { + return DataTable( + headingRowColor: MaterialStateProperty.all( + Theme.of(context).colorScheme.deeplinkTableHeaderColor, + ), + dataRowColor: MaterialStateProperty.all( + Theme.of(context).colorScheme.alternatingBackgroundColor2, + ), + columns: const [ + DataColumn(label: Text('OS')), + DataColumn(label: Text('Issue type')), + DataColumn(label: Text('Status')), + ], + rows: [ + if (linkData.os.contains(PlatformOS.android)) + DataRow( + cells: [ + const DataCell(Text('Android')), + const DataCell(Text('Digital assets link file')), + DataCell( + linkData.domainError + ? Text( + 'Check failed', + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), + ) + : Text( + 'No issues found', + style: TextStyle( + color: Theme.of(context).colorScheme.green, + ), + ), + ), + ], + ), + if (linkData.os.contains(PlatformOS.ios)) + DataRow( + cells: [ + const DataCell(Text('iOS')), + const DataCell(Text('Apple-App-Site-Association file')), + DataCell( + Text( + 'No issues found', + style: TextStyle(color: Theme.of(context).colorScheme.green), + ), + ), + ], + ), + ], + ); + } +} + +class _PathCheckTable extends StatelessWidget { + @override + Widget build(BuildContext context) { + final notAvailableCell = DataCell( + Text( + 'Not available', + style: TextStyle( + color: Theme.of(context).colorScheme.deeplinkUnavailableColor, + ), + ), + ); + return Opacity( + opacity: 0.5, + child: DataTable( + headingRowColor: MaterialStateProperty.all( + Theme.of(context).colorScheme.deeplinkTableHeaderColor, + ), + dataRowColor: MaterialStateProperty.all( + Theme.of(context).colorScheme.alternatingBackgroundColor2, + ), + columns: const [ + DataColumn(label: Text('OS')), + DataColumn(label: Text('Issue type')), + DataColumn(label: Text('Status')), + ], + rows: [ + DataRow( + cells: [ + const DataCell(Text('Android')), + const DataCell(Text('Intent filter')), + notAvailableCell, + ], + ), + DataRow( + cells: [ + const DataCell(Text('iOS')), + const DataCell(Text('Associated domain')), + notAvailableCell, + ], + ), + DataRow( + cells: [ + const DataCell(Text('Android, iOS')), + const DataCell(Text('URL format')), + notAvailableCell, + ], + ), + DataRow( + cells: [ + const DataCell(Text('Android, iOS')), + const DataCell(Text('Routing')), + notAvailableCell, + ], + ), + ], + ), + ); + } +} From 3d53bcb593fa296f827ac826a1e2ca5a23ce7479 Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Mon, 6 Nov 2023 16:32:39 -0800 Subject: [PATCH 29/47] update more UI --- .../deep_link_list_view.dart | 133 +++++------ .../deep_links_controller.dart | 219 +++++++++++++----- .../deep_links_model.dart | 27 ++- .../deep_link_validation/fake_data.dart | 2 +- .../validation_details_view.dart | 159 +++++++++---- .../deep_links_screen_test.dart | 2 +- 6 files changed, 358 insertions(+), 184 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart index 42bb0b42f33..d20b8ce0a7a 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart @@ -52,7 +52,7 @@ class _DeepLinkListViewState extends State // If not found, default to 0. releaseVariantIndex = max(releaseVariantIndex, 0); controller.selectedVariantIndex.value = releaseVariantIndex; - controller.updateLinks(); + controller.validateLinks(); }); } @@ -65,7 +65,6 @@ class _DeepLinkListViewState extends State crossAxisAlignment: CrossAxisAlignment.start, children: [ _DeepLinkListViewTopPanel(), - SizedBox(height: denseSpacing), Expanded(child: _DeepLinkListViewMainPanel()), ], ), @@ -80,63 +79,59 @@ class _DeepLinkListViewMainPanel extends StatelessWidget { @override Widget build(BuildContext context) { final controller = Provider.of(context); - final textTheme = Theme.of(context).textTheme; return ValueListenableBuilder( valueListenable: controller.displayOptionsNotifier, builder: (context, displayOptions, _) => ValueListenableBuilder?>( - valueListenable: controller.linkDatasNotifier, + valueListenable: controller.allLinkDatasNotifier, builder: (context, linkDatas, _) { if (linkDatas == null) { return const CenteredCircularProgressIndicator(); } + if (displayOptions.showSplitScreen) { + return Row( + children: [ + Expanded( + child: _AllDeepLinkDataTable(controller: controller), + ), + Expanded( + child: ValueListenableBuilder( + valueListenable: controller.selectedLink, + builder: (context, selectedLink, _) => TabBarView( + children: [ + ValidationDetailScreen( + linkData: selectedLink!, + controller: controller, + viewType: TableViewType.domainView, + ), + ValidationDetailScreen( + linkData: selectedLink, + controller: controller, + viewType: TableViewType.pathView, + ), + ValidationDetailScreen( + linkData: selectedLink, + controller: controller, + viewType: TableViewType.singleUrlView, + ), + ], + ), + ), + ), + ], + ); + } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - AreaPaneHeader( - title: Text('Validate and fix', style: textTheme.bodyLarge), + _NotificationCardSection( + domainErrorCount: displayOptions.domainErrorCount, + pathErrorCount: displayOptions.pathErrorCount, + controller: controller, ), - displayOptions.showSplitScreen - ? const SizedBox.shrink() - : _NotificationCardSection( - domainErrorCount: displayOptions.domainErrorCount, - pathErrorCount: displayOptions.pathErrorCount, - controller: controller, - ), Expanded( - child: Row( - children: [ - Expanded( - child: _AllDeepLinkDataTable(controller: controller), - ), - if (displayOptions.showSplitScreen) - Expanded( - child: ValueListenableBuilder( - valueListenable: controller.selectedLink, - builder: (context, selectedLink, _) => TabBarView( - children: [ - ValidationDetailScreen( - linkData: selectedLink!, - controller: controller, - viewType: TableViewType.domainView, - ), - ValidationDetailScreen( - linkData: selectedLink, - controller: controller, - viewType: TableViewType.pathView, - ), - ValidationDetailScreen( - linkData: selectedLink, - controller: controller, - viewType: TableViewType.singleUrlView, - ), - ], - ), - ), - ), - ], - ), + child: _AllDeepLinkDataTable(controller: controller), ), ], ); @@ -190,8 +185,10 @@ class _DataTable extends StatelessWidget { selectionNotifier: controller.selectedLink, defaultSortColumn: viewType == TableViewType.pathView ? path : domain, defaultSortDirection: SortDirection.ascending, - onItemSelected: (item) => - controller.updateDisplayOptions(showSplitScreen: true), + onItemSelected: (linkdata) { + controller.selectLink(linkdata); + controller.updateDisplayOptions(showSplitScreen: true); + }, ), ); } @@ -203,12 +200,15 @@ class _DeepLinkListViewTopPanel extends StatelessWidget { @override Widget build(BuildContext context) { final controller = Provider.of(context); - return Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Padding( - padding: const EdgeInsets.all(defaultSpacing), - child: ValueListenableBuilder( + return AreaPaneHeader( + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Validate and fix', + style: Theme.of(context).textTheme.bodyLarge, + ), + ValueListenableBuilder( valueListenable: controller.selectedVariantIndex, builder: (_, value, __) { return _AndroidVariantDropdown( @@ -221,8 +221,8 @@ class _DeepLinkListViewTopPanel extends StatelessWidget { ); }, ), - ), - ], + ], + ), ); } } @@ -268,8 +268,7 @@ class _AllDeepLinkDataTable extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = Theme.of(context); - final textTheme = theme.textTheme; + final textTheme = Theme.of(context).textTheme; return Column( children: [ OutlineDecoration( @@ -324,7 +323,7 @@ class _AllDeepLinkDataTable extends StatelessWidget { ), Expanded( child: ValueListenableBuilder?>( - valueListenable: controller.linkDatasNotifier, + valueListenable: controller.displayLinkDatasNotifier, builder: (context, linkDatas, _) => TabBarView( children: [ _DataTable( @@ -380,10 +379,11 @@ class _NotificationCardSection extends StatelessWidget { onPressed: () { // Switch to the domain view. Select the first link with domain error and show the split screen. DefaultTabController.of(context).index = 0; - controller.selectedLink.value = controller - .getLinkDatasByDomain - .where((element) => element.domainError) - .first; + controller.selectLink( + controller.getLinkDatasByDomain + .where((element) => element.domainErrors.isNotEmpty) + .first, + ); controller.updateDisplayOptions(showSplitScreen: true); }, child: const Text('Fix domain'), @@ -400,10 +400,11 @@ class _NotificationCardSection extends StatelessWidget { onPressed: () { // Switch to the path view. Select the first link with path error and show the split screen. DefaultTabController.of(context).index = 1; - controller.selectedLink.value = controller - .getLinkDatasByPath - .where((element) => element.pathError) - .first; + controller.selectLink( + controller.getLinkDatasByPath + .where((element) => element.pathError) + .first, + ); controller.updateDisplayOptions(showSplitScreen: true); }, child: const Text('Fix path'), diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart index 71bd8d8c24a..742f54e6f3d 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart @@ -3,16 +3,18 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'package:devtools_shared/devtools_deeplink.dart'; import 'package:flutter/foundation.dart'; +import 'package:http/http.dart' as http; import '../../shared/analytics/analytics.dart' as ga; import '../../shared/analytics/constants.dart' as gac; import '../../shared/config_specific/server/server.dart' as server; import 'deep_links_model.dart'; -import 'fake_data.dart'; +const String _apiKey = 'AIzaSyDVE6FP3GpwxgS4q8rbS7qaf6cAbxc_elc'; typedef _DomainAndPath = ({String domain, String path}); enum FilterOption { @@ -66,8 +68,7 @@ class DisplayOptions { Set filters; DisplayOptions updateFilter(FilterOption option, bool value) { - - final Set newFilter = Set.from(filters); + final Set newFilter = Set.from(filters); if (value) { newFilter.add(option); @@ -115,7 +116,7 @@ class DeepLinksController { List get getLinkDatasByPath { final linkDatasByPath = {}; - for (var linkData in linkDatasNotifier.value!) { + for (var linkData in allLinkDatasNotifier.value!) { final prevoisRecord = linkDatasByPath[linkData.path]; linkDatasByPath[linkData.path] = LinkData( domain: linkData.domain, @@ -142,7 +143,7 @@ class DeepLinksController { List get getLinkDatasByDomain { final linkDatasByDomain = {}; - for (var linkData in linkDatasNotifier.value!) { + for (var linkData in allLinkDatasNotifier.value!) { final prevoisRecord = linkDatasByDomain[linkData.domain]; linkDatasByDomain[linkData.domain] = LinkData( domain: linkData.domain, @@ -152,7 +153,7 @@ class DeepLinksController { ...prevoisRecord?.associatedPath ?? [], linkData.path, ], - domainError: linkData.domainError, + domainErrors: linkData.domainErrors, ); } return _getFilterredLinks(linkDatasByDomain.values.toList()); @@ -162,77 +163,179 @@ class DeepLinksController { late final selectedVariantIndex = ValueNotifier(0); void _handleSelectedVariantIndexChanged() { - linkDatasNotifier.value = null; + allLinkDatasNotifier.value = null; unawaited(_loadAndroidAppLinks()); } Future _loadAndroidAppLinks() async { - // if (!_androidAppLinks.containsKey(selectedVariantIndex.value)) { - // final variant = - // selectedProject.value!.androidVariants[selectedVariantIndex.value]; - // await ga.timeAsync( - // gac.deeplink, - // gac.AnalyzeFlutterProject.loadAppLinks.name, - // asyncOperation: () async { - // final result = await server.requestAndroidAppLinkSettings( - // selectedProject.value!.path, - // buildVariant: variant, - // ); - // _androidAppLinks[selectedVariantIndex.value] = result; - // }, - // ); - // } - updateLinks(); + if (!_androidAppLinks.containsKey(selectedVariantIndex.value)) { + final variant = + selectedProject.value!.androidVariants[selectedVariantIndex.value]; + await ga.timeAsync( + gac.deeplink, + gac.AnalyzeFlutterProject.loadAppLinks.name, + asyncOperation: () async { + final result = await server.requestAndroidAppLinkSettings( + selectedProject.value!.path, + buildVariant: variant, + ); + _androidAppLinks[selectedVariantIndex.value] = result; + }, + ); + } + validateLinks(); } List get _allLinkDatas { - return allLinkDatas; - // final appLinks = _androidAppLinks[selectedVariantIndex.value]?.deeplinks; - // if (appLinks == null) { - // return const []; - // } - // final domainPathToScheme = <_DomainAndPath, Set>{}; - // for (final appLink in appLinks) { - // final schemes = domainPathToScheme.putIfAbsent( - // (domain: appLink.host, path: appLink.path), - // () => {}, - // ); - // schemes.add(appLink.scheme); - // } - // return domainPathToScheme.entries - // .map( - // (entry) => LinkData( - // domain: entry.key.domain, - // path: entry.key.path, - // os: [PlatformOS.android], - // scheme: entry.value.toList(), - // ), - // ) - // .toList(); + final appLinks = _androidAppLinks[selectedVariantIndex.value]?.deeplinks; + if (appLinks == null) { + return const []; + } + final domainPathToScheme = <_DomainAndPath, Set>{}; + for (final appLink in appLinks) { + final schemes = domainPathToScheme.putIfAbsent( + (domain: appLink.host, path: appLink.path), + () => {}, + ); + schemes.add(appLink.scheme); + } + return domainPathToScheme.entries + .map( + (entry) => LinkData( + domain: entry.key.domain, + path: entry.key.path, + os: [PlatformOS.android], + scheme: entry.value.toList(), + ), + ) + .toList(); } - final selectedProject = ValueNotifier(null); + final selectedProject = ValueNotifier( null); final selectedLink = ValueNotifier(null); - final linkDatasNotifier = ValueNotifier?>(null); + + final allLinkDatasNotifier = ValueNotifier?>(null); + final displayLinkDatasNotifier = ValueNotifier?>(null); + final generatedAssetLinksForSelectedLink = ValueNotifier(null); final displayOptionsNotifier = ValueNotifier(DisplayOptions()); - void updateLinks() { - linkDatasNotifier.value = _allLinkDatas; + Future _generateAssetLinks() async { + final applicationId = + _androidAppLinks[selectedVariantIndex.value]?.applicationId ?? ''; + + final response = await http.post( + Uri.parse( + 'https://deeplinkassistant-pa.googleapis.com/android/generation/v1/assetlinks:generate?key=$_apiKey', + ), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode( + { + 'package_name': applicationId, + 'domains': [selectedLink.value!.domain], + }, + ), + ); + + final Map result = + json.decode(response.body) as Map; + if (result['domains'] != null) { + final String generatedContent = (((result['domains'] as List) + .first) as Map)['generatedContent']; + + generatedAssetLinksForSelectedLink.value = generatedContent; + } + } + + Future> _validateAndroidDomain() async { + final List linkdatas = _allLinkDatas; + final domains = linkdatas + .where((linkdata) => linkdata.os.contains(PlatformOS.android)) + .map((linkdata) => linkdata.domain) + .toSet() + .toList(); + + final applicationId = + _androidAppLinks[selectedVariantIndex.value]?.applicationId ?? ''; + + final response = await http.post( + Uri.parse( + 'https://deeplinkassistant-pa.googleapis.com/android/validation/v1/domains:batchValidate?key=$_apiKey', + ), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({ + 'package_name': applicationId, + 'app_link_domains': domains, + }), + ); + + final Map result = + json.decode(response.body) as Map; + + final Map> domainErrors = { + for (var domain in domains) domain: [], + }; + + final validationResult = result['validationResult'] as List; + for (final Map domainResult in validationResult) { + final String domainName = domainResult['domainName']; + final List? failedChecks = domainResult['failedChecks']; + if (failedChecks != null) { + for (final Map failedCheck in failedChecks) { + switch (failedCheck['checkName']) { + case 'EXISTENCE': + domainErrors[domainName]!.add(DomainError.existence); + case 'FINGERPRINT': + domainErrors[domainName]!.add(DomainError.fingerprints); + } + } + } + } + + return linkdatas.map((linkdata) { + if (domainErrors[linkdata.domain]?.isNotEmpty ?? false) { + return LinkData( + domain: linkdata.domain, + domainErrors: domainErrors[linkdata.domain]!, + path: linkdata.path, + pathError: linkdata.pathError, + os: linkdata.os, + scheme: linkdata.scheme, + associatedDomains: linkdata.associatedDomains, + associatedPath: linkdata.associatedPath, + ); + } + return linkdata; + }).toList(); + } + + void validateLinks() async { + allLinkDatasNotifier.value = await _validateAndroidDomain(); + displayLinkDatasNotifier.value = + _getFilterredLinks(allLinkDatasNotifier.value!); displayOptionsNotifier.value = displayOptionsNotifier.value.copyWith( - domainErrorCount: - getLinkDatasByDomain.where((element) => element.domainError).length, + domainErrorCount: getLinkDatasByDomain + .where((element) => element.domainErrors.isNotEmpty) + .length, pathErrorCount: getLinkDatasByPath.where((element) => element.pathError).length, ); } + void selectLink(LinkData linkdata) async { + selectedLink.value = linkdata; + if (linkdata.domainErrors.isNotEmpty) { + await _generateAssetLinks(); + } + } + set searchContent(String content) { displayOptionsNotifier.value = displayOptionsNotifier.value.copyWith(searchContent: content); - linkDatasNotifier.value = _getFilterredLinks(_allLinkDatas); + displayLinkDatasNotifier.value = + _getFilterredLinks(allLinkDatasNotifier.value!); } void updateDisplayOptions({ @@ -254,12 +357,14 @@ class DeepLinksController { if (addedFilter != null) { displayOptionsNotifier.value = displayOptionsNotifier.value.updateFilter(addedFilter, true); - } if (removedFilter != null) { + } + if (removedFilter != null) { displayOptionsNotifier.value = displayOptionsNotifier.value.updateFilter(removedFilter, false); } - linkDatasNotifier.value = _getFilterredLinks(_allLinkDatas); + displayLinkDatasNotifier.value = + _getFilterredLinks(allLinkDatasNotifier.value!); } List _getFilterredLinks(List linkDatas) { @@ -279,12 +384,12 @@ class DeepLinksController { return false; } - if (!((linkData.domainError && + if (!((linkData.domainErrors.isNotEmpty && displayOptions.filters .contains(FilterOption.failedDomainCheck)) || (linkData.pathError && displayOptions.filters.contains(FilterOption.failedPathCheck)) || - (!linkData.domainError && + (!linkData.domainErrors.isNotEmpty && !linkData.pathError && displayOptions.filters.contains(FilterOption.noIssue)))) { return false; diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart index d32a72a84a8..1f56aa7c921 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart @@ -23,6 +23,14 @@ enum PlatformOS { final String description; } +enum DomainError { + existence('Domain doesn\'t exist'), + fingerprints('Fingerprints unavailable'); + + const DomainError(this.description); + final String description; +} + /// Contains all data relevant to a deep link. class LinkData with SearchableDataMixin { LinkData({ @@ -30,7 +38,7 @@ class LinkData with SearchableDataMixin { required this.path, required this.os, this.scheme = const ['http://', 'https://'], - this.domainError = false, + this.domainErrors = const [], this.pathError = false, this.associatedPath = const [], this.associatedDomains = const [], @@ -40,7 +48,7 @@ class LinkData with SearchableDataMixin { final String domain; final List os; final List scheme; - final bool domainError; + final List domainErrors; final bool pathError; final List associatedPath; @@ -87,7 +95,8 @@ class _ErrorAwareText extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( - 'This m.shopping.com domain has 1 issue to fix. Fixing this domain will fix ${link.associatedPath.length} associated deep links.', + 'This m.shopping.com domain has ${link.domainErrors.length} issue to fix. ' + 'Fixing this domain will fix ${link.associatedPath.length} associated deep links.', style: TextStyle( color: Theme.of(context).colorScheme.tooltipTextColor, fontSize: defaultFontSize, @@ -96,7 +105,7 @@ class _ErrorAwareText extends StatelessWidget { TextButton( onPressed: () { controller.updateDisplayOptions(showSplitScreen: true); - controller.selectedLink.value = link; + controller.selectLink(link); }, child: Text( 'Fix this domain', @@ -188,7 +197,7 @@ class DomainColumn extends ColumnData VoidCallback? onPressed, }) { return _ErrorAwareText( - isError: dataObject.domainError, + isError: dataObject.domainErrors.isNotEmpty, controller: controller, text: dataObject.domain, link: dataObject, @@ -204,8 +213,8 @@ class DomainColumn extends ColumnData switch (sortingOption) { case SortingOption.errorOnTop: - if (a.domainError) return 1; - if (b.domainError) return -1; + if (a.domainErrors.isNotEmpty) return 1; + if (b.domainErrors.isNotEmpty) return -1; return 0; case SortingOption.aToZ: return a.domain.compareTo(b.domain); @@ -439,7 +448,7 @@ class StatusColumn extends ColumnData @override String getValue(LinkData dataObject) { - if (dataObject.domainError) { + if (dataObject.domainErrors.isNotEmpty) { return 'Failed domain checks'; } else if (dataObject.pathError) { return 'Failed path checks'; @@ -489,7 +498,7 @@ class StatusColumn extends ColumnData bool isRowSelected = false, VoidCallback? onPressed, }) { - if (dataObject.domainError || dataObject.pathError) { + if (dataObject.domainErrors.isNotEmpty || dataObject.pathError) { return Text( getValue(dataObject), overflow: TextOverflow.ellipsis, diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/fake_data.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/fake_data.dart index 5595652a17d..325d181d91c 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/fake_data.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/fake_data.dart @@ -20,7 +20,7 @@ final allLinkDatas = [ os: [PlatformOS.android, PlatformOS.ios], domain: 'm.shopping.com', path: path, - domainError: true, + domainErrors: [DomainError.existence], pathError: path.contains('shoe'), ), for (var path in paths) diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/validation_details_view.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/validation_details_view.dart index a532ca56cac..14ce9d0bc29 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/validation_details_view.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/validation_details_view.dart @@ -5,6 +5,7 @@ import 'package:devtools_app_shared/ui.dart'; import 'package:flutter/material.dart'; +import '../../shared/common_widgets.dart'; import '../../shared/ui/colors.dart'; import 'deep_link_list_view.dart'; import 'deep_links_controller.dart'; @@ -64,12 +65,15 @@ class ValidationDetailScreen extends StatelessWidget { ' the manifest and info.plist file, routing issues, URL format, etc.', style: Theme.of(context).subtleTextStyle, ), - if (viewType != TableViewType.pathView) ...[ + if (viewType == TableViewType.domainView || viewType == TableViewType.singleUrlView) ...[ const SizedBox(height: intermediateSpacing), Text('Domain check', style: textTheme.titleSmall), - _DomainCheckTable(linkData: linkData), + const SizedBox(height: denseSpacing), + _DomainCheckTable( + controller: controller, + ), ], - if (viewType != TableViewType.domainView) ...[ + if (viewType == TableViewType.pathView || viewType == TableViewType.singleUrlView) ...[ const SizedBox(height: intermediateSpacing), Text('Path check (coming soon)', style: textTheme.titleSmall), _PathCheckTable(), @@ -79,7 +83,7 @@ class ValidationDetailScreen extends StatelessWidget { alignment: Alignment.bottomRight, child: FilledButton( onPressed: () { - controller.updateLinks(); + controller.validateLinks(); }, child: const Text('Recheck all'), ), @@ -101,7 +105,7 @@ class ValidationDetailScreen extends StatelessWidget { ), child: Row( children: [ - if (linkData.domainError) + if (linkData.domainErrors.isNotEmpty) Icon( Icons.error, color: colorScheme.error, @@ -128,61 +132,116 @@ class ValidationDetailScreen extends StatelessWidget { class _DomainCheckTable extends StatelessWidget { const _DomainCheckTable({ - required this.linkData, + required this.controller, }); - final LinkData linkData; + final DeepLinksController controller; @override Widget build(BuildContext context) { - return DataTable( - headingRowColor: MaterialStateProperty.all( - Theme.of(context).colorScheme.deeplinkTableHeaderColor, - ), - dataRowColor: MaterialStateProperty.all( - Theme.of(context).colorScheme.alternatingBackgroundColor2, - ), - columns: const [ - DataColumn(label: Text('OS')), - DataColumn(label: Text('Issue type')), - DataColumn(label: Text('Status')), - ], - rows: [ - if (linkData.os.contains(PlatformOS.android)) - DataRow( - cells: [ - const DataCell(Text('Android')), - const DataCell(Text('Digital assets link file')), - DataCell( - linkData.domainError - ? Text( - 'Check failed', - style: TextStyle( - color: Theme.of(context).colorScheme.error, - ), - ) - : Text( - 'No issues found', - style: TextStyle( - color: Theme.of(context).colorScheme.green, - ), - ), + final linkData = controller.selectedLink.value!; + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + DataTable( + headingRowColor: MaterialStateProperty.all( + Theme.of(context).colorScheme.deeplinkTableHeaderColor, + ), + dataRowColor: MaterialStateProperty.all( + Theme.of(context).colorScheme.alternatingBackgroundColor2, + ), + columns: const [ + DataColumn(label: Text('OS')), + DataColumn(label: Text('Issue type')), + DataColumn(label: Text('Status')), + ], + dataRowMinHeight: 32, + dataRowMaxHeight: 32, + rows: [ + if (linkData.os.contains(PlatformOS.android)) + DataRow( + cells: [ + const DataCell(Text('Android')), + const DataCell(Text('Digital assets link file')), + DataCell( + linkData.domainErrors.isNotEmpty + ? Text( + 'Check failed', + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), + ) + : Text( + 'No issues found', + style: TextStyle( + color: Theme.of(context).colorScheme.green, + ), + ), + ), + ], ), - ], + if (linkData.os.contains(PlatformOS.ios)) + DataRow( + cells: [ + const DataCell(Text('iOS')), + const DataCell(Text('Apple-App-Site-Association file')), + DataCell( + Text( + 'No issues found', + style: + TextStyle(color: Theme.of(context).colorScheme.green), + ), + ), + ], + ), + ], + ), + if (linkData.domainErrors.isNotEmpty) ...[ + Text('How to fix'), + Text( + 'Add the new recommended Digital Asset Links JSON file to the failed website domain at the correct location.', + style: Theme.of(context).subtleTextStyle, ), - if (linkData.os.contains(PlatformOS.ios)) - DataRow( - cells: [ - const DataCell(Text('iOS')), - const DataCell(Text('Apple-App-Site-Association file')), - DataCell( - Text( - 'No issues found', - style: TextStyle(color: Theme.of(context).colorScheme.green), + Text( + 'Update and publish recommend Digital Asset Links JSON file below to this location: ', + style: Theme.of(context).subtleTextStyle, + ), + Align( + alignment: Alignment.centerLeft, + child: Card( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(4.0)), + ), + color: Theme.of(context).colorScheme.outline, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: denseSpacing), + child: SelectionArea( + child: Text( + 'https://${linkData.domain}/.well-known/assetlinks.json', + style: Theme.of(context).regularTextStyle.copyWith( + color: Colors.black, + fontWeight: FontWeight.w500, + ), + ), ), ), - ], + ), + ), + Card( + child: ValueListenableBuilder( + valueListenable: controller.generatedAssetLinksForSelectedLink, + builder: (_, String? generatedAssetLinks, __) => + generatedAssetLinks != null + ? Align( + alignment: Alignment.centerLeft, + child: SelectionArea( + child: Text(generatedAssetLinks), + ), + ) + : const CenteredCircularProgressIndicator(), + ), ), + ], ], ); } diff --git a/packages/devtools_app/test/deep_link_vlidation/deep_links_screen_test.dart b/packages/devtools_app/test/deep_link_vlidation/deep_links_screen_test.dart index 52c207d1403..c7bcc65eed2 100644 --- a/packages/devtools_app/test/deep_link_vlidation/deep_links_screen_test.dart +++ b/packages/devtools_app/test/deep_link_vlidation/deep_links_screen_test.dart @@ -80,7 +80,7 @@ void main() { (WidgetTester tester) async { deepLinksController.selectedProject.value = FlutterProject(path: '/abc', androidVariants: ['debug', 'release']); - deepLinksController.linkDatasNotifier.value = [ + deepLinksController.allLinkDatasNotifier.value = [ LinkData( domain: 'www.google.com', path: '/', From 2b1384464c89b45b4f75c92b3d9da2e146e45280 Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Mon, 6 Nov 2023 16:33:55 -0800 Subject: [PATCH 30/47] 1 --- packages/devtools_app/lib/src/app.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devtools_app/lib/src/app.dart b/packages/devtools_app/lib/src/app.dart index 002a296438c..7d4f1d5867f 100644 --- a/packages/devtools_app/lib/src/app.dart +++ b/packages/devtools_app/lib/src/app.dart @@ -586,7 +586,7 @@ List defaultScreens({ AppSizeScreen(), createController: (_) => AppSizeController(), ), - //if (FeatureFlags.deepLinkValidation) + if (FeatureFlags.deepLinkValidation) DevToolsScreen( DeepLinksScreen(), createController: (_) => DeepLinksController(), From 5fc40b508c078df76bad90f86ac6d91c890c4734 Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Tue, 7 Nov 2023 11:17:58 -0800 Subject: [PATCH 31/47] rename --- .../screens/deep_link_validation/deep_link_list_view.dart | 6 +++--- .../deep_link_validation/validation_details_view.dart | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart index d20b8ce0a7a..aedd77fd6c5 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart @@ -100,17 +100,17 @@ class _DeepLinkListViewMainPanel extends StatelessWidget { valueListenable: controller.selectedLink, builder: (context, selectedLink, _) => TabBarView( children: [ - ValidationDetailScreen( + ValidationDetailView( linkData: selectedLink!, controller: controller, viewType: TableViewType.domainView, ), - ValidationDetailScreen( + ValidationDetailView( linkData: selectedLink, controller: controller, viewType: TableViewType.pathView, ), - ValidationDetailScreen( + ValidationDetailView( linkData: selectedLink, controller: controller, viewType: TableViewType.singleUrlView, diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/validation_details_view.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/validation_details_view.dart index 14ce9d0bc29..41f45ee6504 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/validation_details_view.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/validation_details_view.dart @@ -11,8 +11,8 @@ import 'deep_link_list_view.dart'; import 'deep_links_controller.dart'; import 'deep_links_model.dart'; -class ValidationDetailScreen extends StatelessWidget { - const ValidationDetailScreen({ +class ValidationDetailView extends StatelessWidget { + const ValidationDetailView({ super.key, required this.linkData, required this.viewType, From f4d545a85c2d8106eb39145fe70f4400baa04226 Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Tue, 7 Nov 2023 13:28:34 -0800 Subject: [PATCH 32/47] Update validation_details_view.dart --- .../deep_link_validation/validation_details_view.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/validation_details_view.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/validation_details_view.dart index 41f45ee6504..071e581c61d 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/validation_details_view.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/validation_details_view.dart @@ -6,6 +6,7 @@ import 'package:devtools_app_shared/ui.dart'; import 'package:flutter/material.dart'; import '../../shared/common_widgets.dart'; +import '../../shared/table/table.dart'; import '../../shared/ui/colors.dart'; import 'deep_link_list_view.dart'; import 'deep_links_controller.dart'; @@ -155,8 +156,8 @@ class _DomainCheckTable extends StatelessWidget { DataColumn(label: Text('Issue type')), DataColumn(label: Text('Status')), ], - dataRowMinHeight: 32, - dataRowMaxHeight: 32, + dataRowMinHeight: defaultRowHeight, + dataRowMaxHeight: defaultRowHeight, rows: [ if (linkData.os.contains(PlatformOS.android)) DataRow( @@ -197,7 +198,7 @@ class _DomainCheckTable extends StatelessWidget { ], ), if (linkData.domainErrors.isNotEmpty) ...[ - Text('How to fix'), + const Text('How to fix'), Text( 'Add the new recommended Digital Asset Links JSON file to the failed website domain at the correct location.', style: Theme.of(context).subtleTextStyle, From 4c83571dcac31a7aa54f41f5e20a33f48f0726e2 Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Wed, 8 Nov 2023 10:28:45 -0800 Subject: [PATCH 33/47] Update deep_links_controller.dart --- .../src/screens/deep_link_validation/deep_links_controller.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart index 742f54e6f3d..196129749fe 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart @@ -234,6 +234,8 @@ class DeepLinksController { { 'package_name': applicationId, 'domains': [selectedLink.value!.domain], + // TODO(hangyujin): The fake fingerprints here is just for testing usage, should remove it later. + 'supplemental_sha256_cert_fingerprints': ['5A:33:EA:64:09:97:F2:F0:24:21:0F:B6:7A:A8:18:1C:18:A9:83:03:20:21:8F:9B:0B:98:BF:43:69:C2:AF:4A'], }, ), ); From 58f732c9707d20ee9e11cd1be9bbba359c7b6617 Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Wed, 8 Nov 2023 13:55:14 -0800 Subject: [PATCH 34/47] resolve comments --- .../deep_link_list_view.dart | 30 ++++++++-------- .../deep_links_controller.dart | 34 +++++++++++-------- 2 files changed, 34 insertions(+), 30 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart index aedd77fd6c5..07381e29513 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart @@ -153,18 +153,18 @@ class _DataTable extends StatelessWidget { @override Widget build(BuildContext context) { - final ColumnData domain = DomainColumn(controller); - final ColumnData path = PathColumn(controller); + final domain = DomainColumn(controller); + final path = PathColumn(controller); return Padding( padding: const EdgeInsets.only(top: denseSpacing), - child: FlatTable( + child: FlatTable( keyFactory: (node) => ValueKey(node.toString), data: linkDatas, dataKey: 'deep-links', autoScrollContent: true, headerColor: Theme.of(context).colorScheme.deeplinkTableHeaderColor, - columns: [ + columns: >[ ...(() { switch (viewType) { case TableViewType.domainView: @@ -172,7 +172,7 @@ class _DataTable extends StatelessWidget { case TableViewType.pathView: return [path, NumberOfAssociatedDomainColumn()]; case TableViewType.singleUrlView: - return [domain, path]; + return >[domain, path]; } })(), SchemeColumn(controller), @@ -183,10 +183,10 @@ class _DataTable extends StatelessWidget { ], ], selectionNotifier: controller.selectedLink, - defaultSortColumn: viewType == TableViewType.pathView ? path : domain, + defaultSortColumn: (viewType == TableViewType.pathView ? path : domain) as ColumnData, defaultSortDirection: SortDirection.ascending, onItemSelected: (linkdata) { - controller.selectLink(linkdata); + controller.selectLink(linkdata!); controller.updateDisplayOptions(showSplitScreen: true); }, ), @@ -264,6 +264,7 @@ class _AllDeepLinkDataTable extends StatelessWidget { const _AllDeepLinkDataTable({ required this.controller, }); + final DeepLinksController controller; @override @@ -273,15 +274,13 @@ class _AllDeepLinkDataTable extends StatelessWidget { children: [ OutlineDecoration( child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded( - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: defaultSpacing), - child: Text( - 'All deep links', - style: textTheme.bodyLarge, - ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: defaultSpacing), + child: Text( + 'All deep links', + style: textTheme.bodyLarge, ), ), Padding( @@ -359,6 +358,7 @@ class _NotificationCardSection extends StatelessWidget { final int domainErrorCount; final int pathErrorCount; + final DeepLinksController controller; @override Widget build(BuildContext context) { diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart index 196129749fe..1fe910f598d 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart @@ -15,6 +15,12 @@ import '../../shared/config_specific/server/server.dart' as server; import 'deep_links_model.dart'; const String _apiKey = 'AIzaSyDVE6FP3GpwxgS4q8rbS7qaf6cAbxc_elc'; + +const String _assetLinksGenerationURL = + 'https://deeplinkassistant-pa.googleapis.com/android/generation/v1/assetlinks:generate?key=$_apiKey'; + +const String _androidDomainValidationURL = + 'https://deeplinkassistant-pa.googleapis.com/android/validation/v1/domains:batchValidate?key=$_apiKey'; typedef _DomainAndPath = ({String domain, String path}); enum FilterOption { @@ -68,7 +74,7 @@ class DisplayOptions { Set filters; DisplayOptions updateFilter(FilterOption option, bool value) { - final Set newFilter = Set.from(filters); + final newFilter = Set.from(filters); if (value) { newFilter.add(option); @@ -117,20 +123,20 @@ class DeepLinksController { List get getLinkDatasByPath { final linkDatasByPath = {}; for (var linkData in allLinkDatasNotifier.value!) { - final prevoisRecord = linkDatasByPath[linkData.path]; + final previousRecord = linkDatasByPath[linkData.path]; linkDatasByPath[linkData.path] = LinkData( domain: linkData.domain, path: linkData.path, os: [ - if (prevoisRecord?.os.contains(PlatformOS.android) ?? + if (previousRecord?.os.contains(PlatformOS.android) ?? false || linkData.os.contains(PlatformOS.android)) PlatformOS.android, - if (prevoisRecord?.os.contains(PlatformOS.ios) ?? + if (previousRecord?.os.contains(PlatformOS.ios) ?? false || linkData.os.contains(PlatformOS.ios)) PlatformOS.ios, ], associatedDomains: [ - ...prevoisRecord?.associatedDomains ?? [], + ...previousRecord?.associatedDomains ?? [], linkData.domain, ], pathError: linkData.pathError, @@ -144,13 +150,13 @@ class DeepLinksController { final linkDatasByDomain = {}; for (var linkData in allLinkDatasNotifier.value!) { - final prevoisRecord = linkDatasByDomain[linkData.domain]; + final previousRecord = linkDatasByDomain[linkData.domain]; linkDatasByDomain[linkData.domain] = LinkData( domain: linkData.domain, path: linkData.path, os: linkData.os, associatedPath: [ - ...prevoisRecord?.associatedPath ?? [], + ...previousRecord?.associatedPath ?? [], linkData.path, ], domainErrors: linkData.domainErrors, @@ -211,7 +217,7 @@ class DeepLinksController { .toList(); } - final selectedProject = ValueNotifier( null); + final selectedProject = ValueNotifier(null); final selectedLink = ValueNotifier(null); final allLinkDatasNotifier = ValueNotifier?>(null); @@ -226,16 +232,16 @@ class DeepLinksController { _androidAppLinks[selectedVariantIndex.value]?.applicationId ?? ''; final response = await http.post( - Uri.parse( - 'https://deeplinkassistant-pa.googleapis.com/android/generation/v1/assetlinks:generate?key=$_apiKey', - ), + Uri.parse(_assetLinksGenerationURL), headers: {'Content-Type': 'application/json'}, body: jsonEncode( { 'package_name': applicationId, 'domains': [selectedLink.value!.domain], // TODO(hangyujin): The fake fingerprints here is just for testing usage, should remove it later. - 'supplemental_sha256_cert_fingerprints': ['5A:33:EA:64:09:97:F2:F0:24:21:0F:B6:7A:A8:18:1C:18:A9:83:03:20:21:8F:9B:0B:98:BF:43:69:C2:AF:4A'], + 'supplemental_sha256_cert_fingerprints': [ + '5A:33:EA:64:09:97:F2:F0:24:21:0F:B6:7A:A8:18:1C:18:A9:83:03:20:21:8F:9B:0B:98:BF:43:69:C2:AF:4A' + ], }, ), ); @@ -262,9 +268,7 @@ class DeepLinksController { _androidAppLinks[selectedVariantIndex.value]?.applicationId ?? ''; final response = await http.post( - Uri.parse( - 'https://deeplinkassistant-pa.googleapis.com/android/validation/v1/domains:batchValidate?key=$_apiKey', - ), + Uri.parse(_androidDomainValidationURL), headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'package_name': applicationId, From 7f5c6d725f45abea91f7486caaa6b0f12e1e289b Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Wed, 8 Nov 2023 15:16:02 -0800 Subject: [PATCH 35/47] resolve comments --- .../deep_link_list_view.dart | 23 +- .../deep_links_controller.dart | 43 ++- .../deep_links_model.dart | 144 ++++--- .../validation_details_view.dart | 353 ++++++++++-------- 4 files changed, 315 insertions(+), 248 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart index 07381e29513..b47d41e1f4c 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart @@ -13,6 +13,7 @@ import '../../shared/primitives/utils.dart'; import '../../shared/table/table.dart'; import '../../shared/table/table_data.dart'; import '../../shared/ui/colors.dart'; +import '../../shared/ui/tab.dart'; import '../../shared/utils.dart'; import 'deep_links_controller.dart'; import 'deep_links_model.dart'; @@ -183,7 +184,8 @@ class _DataTable extends StatelessWidget { ], ], selectionNotifier: controller.selectedLink, - defaultSortColumn: (viewType == TableViewType.pathView ? path : domain) as ColumnData, + defaultSortColumn: (viewType == TableViewType.pathView ? path : domain) + as ColumnData, defaultSortDirection: SortDirection.ascending, onItemSelected: (linkdata) { controller.selectLink(linkdata!); @@ -270,6 +272,7 @@ class _AllDeepLinkDataTable extends StatelessWidget { @override Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; + const gaPrefix = 'deepLinkTab'; return Column( children: [ OutlineDecoration( @@ -304,17 +307,17 @@ class _AllDeepLinkDataTable extends StatelessWidget { ), TabBar( tabs: [ - Text( - 'Domain view', - style: textTheme.bodyLarge, + DevToolsTab.create( + tabName: 'Domain view', + gaPrefix: gaPrefix, ), - Text( - 'Path view', - style: textTheme.bodyLarge, + DevToolsTab.create( + tabName: 'Path view', + gaPrefix: gaPrefix, ), - Text( - 'Single URL view', - style: textTheme.bodyLarge, + DevToolsTab.create( + tabName: 'Single URL view', + gaPrefix: gaPrefix, ), ], tabAlignment: TabAlignment.start, diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart index 1fe910f598d..4498069cadc 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart @@ -15,12 +15,20 @@ import '../../shared/config_specific/server/server.dart' as server; import 'deep_links_model.dart'; const String _apiKey = 'AIzaSyDVE6FP3GpwxgS4q8rbS7qaf6cAbxc_elc'; - const String _assetLinksGenerationURL = 'https://deeplinkassistant-pa.googleapis.com/android/generation/v1/assetlinks:generate?key=$_apiKey'; - const String _androidDomainValidationURL = 'https://deeplinkassistant-pa.googleapis.com/android/validation/v1/domains:batchValidate?key=$_apiKey'; +const postHeader = {'Content-Type': 'application/json'}; +const String _packageNameKey = 'package_name'; +const String _domainsKey = 'domains'; +const String _appLinkDomainsKey = 'app_link_domains'; +const String _validationResultKey = 'validationResult'; +const String _domainNameKey = 'domainName'; +const String _checkNameKey = 'checkName'; +const String _failedChecksKey = 'failedChecks'; +const String _generatedContentKey = 'generatedContent'; + typedef _DomainAndPath = ({String domain, String path}); enum FilterOption { @@ -60,6 +68,7 @@ class DisplayOptions { FilterOption.failedPathCheck, }, this.searchContent = '', + // Default to show result with error first. this.domainSortingOption = SortingOption.errorOnTop, this.pathSortingOption = SortingOption.errorOnTop, }); @@ -71,7 +80,7 @@ class DisplayOptions { SortingOption? domainSortingOption; SortingOption? pathSortingOption; - Set filters; + final Set filters; DisplayOptions updateFilter(FilterOption option, bool value) { final newFilter = Set.from(filters); @@ -233,14 +242,14 @@ class DeepLinksController { final response = await http.post( Uri.parse(_assetLinksGenerationURL), - headers: {'Content-Type': 'application/json'}, + headers: postHeader, body: jsonEncode( { - 'package_name': applicationId, - 'domains': [selectedLink.value!.domain], + _packageNameKey: applicationId, + _domainsKey: [selectedLink.value!.domain], // TODO(hangyujin): The fake fingerprints here is just for testing usage, should remove it later. 'supplemental_sha256_cert_fingerprints': [ - '5A:33:EA:64:09:97:F2:F0:24:21:0F:B6:7A:A8:18:1C:18:A9:83:03:20:21:8F:9B:0B:98:BF:43:69:C2:AF:4A' + '5A:33:EA:64:09:97:F2:F0:24:21:0F:B6:7A:A8:18:1C:18:A9:83:03:20:21:8F:9B:0B:98:BF:43:69:C2:AF:4A', ], }, ), @@ -248,9 +257,9 @@ class DeepLinksController { final Map result = json.decode(response.body) as Map; - if (result['domains'] != null) { - final String generatedContent = (((result['domains'] as List) - .first) as Map)['generatedContent']; + if (result[_domainsKey] != null) { + final String generatedContent = (((result[_domainsKey] as List) + .first) as Map)[_generatedContentKey]; generatedAssetLinksForSelectedLink.value = generatedContent; } @@ -269,10 +278,10 @@ class DeepLinksController { final response = await http.post( Uri.parse(_androidDomainValidationURL), - headers: {'Content-Type': 'application/json'}, + headers: postHeader, body: jsonEncode({ - 'package_name': applicationId, - 'app_link_domains': domains, + _packageNameKey: applicationId, + _appLinkDomainsKey: domains, }), ); @@ -283,13 +292,13 @@ class DeepLinksController { for (var domain in domains) domain: [], }; - final validationResult = result['validationResult'] as List; + final validationResult = result[_validationResultKey] as List; for (final Map domainResult in validationResult) { - final String domainName = domainResult['domainName']; - final List? failedChecks = domainResult['failedChecks']; + final String domainName = domainResult[_domainNameKey]; + final List? failedChecks = domainResult[_failedChecksKey]; if (failedChecks != null) { for (final Map failedCheck in failedChecks) { - switch (failedCheck['checkName']) { + switch (failedCheck[_checkNameKey]) { case 'EXISTENCE': domainErrors[domainName]!.add(DomainError.existence); case 'FINGERPRINT': diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart index 1f56aa7c921..3e5ea9f3101 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart @@ -14,6 +14,7 @@ import 'deep_link_list_view.dart'; import 'deep_links_controller.dart'; const kDeeplinkTableCellDefaultWidth = 200.0; +const kToolTipWidth = 344.0; enum PlatformOS { android('Android'), @@ -90,7 +91,7 @@ class _ErrorAwareText extends StatelessWidget { preferBelow: true, richMessage: WidgetSpan( child: SizedBox( - width: 344, + width: kToolTipWidth, child: Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ @@ -158,25 +159,8 @@ class DomainColumn extends ColumnData children: [ const Text('Domain'), PopupMenuButton( - itemBuilder: (BuildContext context) { - return [ - _buildPopupMenuSortingEntry( - controller, - SortingOption.errorOnTop, - isPath: false, - ), - _buildPopupMenuSortingEntry( - controller, - SortingOption.aToZ, - isPath: false, - ), - _buildPopupMenuSortingEntry( - controller, - SortingOption.zToA, - isPath: false, - ), - ]; - }, + itemBuilder: (BuildContext context) => + _buildPopupMenuSortingEntries(controller, isPath: false), child: Icon( Icons.arrow_drop_down, size: actionsIconSize, @@ -204,24 +188,13 @@ class DomainColumn extends ColumnData ); } - // Default to show result with error first. @override - int compare(LinkData a, LinkData b) { - final SortingOption? sortingOption = - controller.displayOptions.domainSortingOption; - if (sortingOption == null) return 0; - - switch (sortingOption) { - case SortingOption.errorOnTop: - if (a.domainErrors.isNotEmpty) return 1; - if (b.domainErrors.isNotEmpty) return -1; - return 0; - case SortingOption.aToZ: - return a.domain.compareTo(b.domain); - case SortingOption.zToA: - return b.domain.compareTo(a.domain); - } - } + int compare(LinkData a, LinkData b) => _compareLinkData( + a, + b, + sortingOption: controller.displayOptions.domainSortingOption, + compareDomain: true, + ); } class PathColumn extends ColumnData @@ -244,25 +217,8 @@ class PathColumn extends ColumnData children: [ const Text('Path'), PopupMenuButton( - itemBuilder: (BuildContext context) { - return [ - _buildPopupMenuSortingEntry( - controller, - SortingOption.errorOnTop, - isPath: true, - ), - _buildPopupMenuSortingEntry( - controller, - SortingOption.aToZ, - isPath: true, - ), - _buildPopupMenuSortingEntry( - controller, - SortingOption.zToA, - isPath: true, - ), - ]; - }, + itemBuilder: (BuildContext context) => + _buildPopupMenuSortingEntries(controller, isPath: true), child: Icon( Icons.arrow_drop_down, size: actionsIconSize, @@ -290,25 +246,13 @@ class PathColumn extends ColumnData ); } - // Default to show result with error first. @override - int compare(LinkData a, LinkData b) { - final SortingOption? sortingOption = - controller.displayOptions.pathSortingOption; - - if (sortingOption == null) return 0; - - switch (sortingOption) { - case SortingOption.errorOnTop: - if (a.pathError) return -1; - if (b.pathError) return 1; - return 0; - case SortingOption.aToZ: - return a.path.compareTo(b.path); - case SortingOption.zToA: - return b.path.compareTo(a.path); - } - } + int compare(LinkData a, LinkData b) => _compareLinkData( + a, + b, + sortingOption: controller.displayOptions.pathSortingOption, + compareDomain: false, + ); } class NumberOfAssociatedPathColumn extends ColumnData { @@ -561,6 +505,29 @@ PopupMenuEntry _buildPopupMenuFilterEntry( ); } +List> _buildPopupMenuSortingEntries( + DeepLinksController controller, { + required bool isPath, +}) { + return [ + _buildPopupMenuSortingEntry( + controller, + SortingOption.errorOnTop, + isPath: isPath, + ), + _buildPopupMenuSortingEntry( + controller, + SortingOption.aToZ, + isPath: isPath, + ), + _buildPopupMenuSortingEntry( + controller, + SortingOption.zToA, + isPath: isPath, + ), + ]; +} + PopupMenuEntry _buildPopupMenuSortingEntry( DeepLinksController controller, SortingOption sortingOption, { @@ -586,3 +553,32 @@ class FlutterProject { final String path; final List androidVariants; } + +int _compareLinkData( + LinkData a, + LinkData b, { + SortingOption? sortingOption, + required bool compareDomain, +}) { + if (sortingOption == null) return 0; + + switch (sortingOption) { + case SortingOption.errorOnTop: + if (compareDomain) { + if (a.domainErrors.isNotEmpty) return 1; + if (b.domainErrors.isNotEmpty) return -1; + } else { + if (a.pathError) return 1; + if (b.pathError) return -1; + } + return 0; + case SortingOption.aToZ: + if (compareDomain) return a.domain.compareTo(b.domain); + + return a.path.compareTo(b.path); + case SortingOption.zToA: + if (compareDomain) return b.domain.compareTo(a.domain); + + return b.path.compareTo(a.path); + } +} diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/validation_details_view.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/validation_details_view.dart index 071e581c61d..8479ee02c2a 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/validation_details_view.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/validation_details_view.dart @@ -26,31 +26,9 @@ class ValidationDetailView extends StatelessWidget { @override Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final textTheme = Theme.of(context).textTheme; return ListView( children: [ - OutlineDecoration( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: defaultSpacing), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - viewType == TableViewType.domainView - ? 'Selected domain validation details' - : 'Selected Deep link validation details', - style: textTheme.titleSmall, - ), - IconButton( - onPressed: () => - controller.updateDisplayOptions(showSplitScreen: false), - icon: const Icon(Icons.close), - ), - ], - ), - ), - ), + ValidationDetailHeader(viewType: viewType, controller: controller), Padding( padding: const EdgeInsets.symmetric( horizontal: largeSpacing, @@ -66,19 +44,14 @@ class ValidationDetailView extends StatelessWidget { ' the manifest and info.plist file, routing issues, URL format, etc.', style: Theme.of(context).subtleTextStyle, ), - if (viewType == TableViewType.domainView || viewType == TableViewType.singleUrlView) ...[ - const SizedBox(height: intermediateSpacing), - Text('Domain check', style: textTheme.titleSmall), - const SizedBox(height: denseSpacing), + if (viewType == TableViewType.domainView || + viewType == TableViewType.singleUrlView) _DomainCheckTable( controller: controller, ), - ], - if (viewType == TableViewType.pathView || viewType == TableViewType.singleUrlView) ...[ - const SizedBox(height: intermediateSpacing), - Text('Path check (coming soon)', style: textTheme.titleSmall), + if (viewType == TableViewType.pathView || + viewType == TableViewType.singleUrlView) _PathCheckTable(), - ], const SizedBox(height: largeSpacing), Align( alignment: Alignment.bottomRight, @@ -89,40 +62,8 @@ class ValidationDetailView extends StatelessWidget { child: const Text('Recheck all'), ), ), - if (viewType == TableViewType.domainView) ...[ - Text('Associated deep link URL', style: textTheme.titleSmall), - Card( - color: colorScheme.surface, - shape: const RoundedRectangleBorder(), - child: Padding( - padding: const EdgeInsets.all(denseSpacing), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: linkData.associatedPath - .map( - (path) => Padding( - padding: const EdgeInsets.symmetric( - vertical: denseRowSpacing, - ), - child: Row( - children: [ - if (linkData.domainErrors.isNotEmpty) - Icon( - Icons.error, - color: colorScheme.error, - size: defaultIconSize, - ), - const SizedBox(width: denseSpacing), - Text(path), - ], - ), - ), - ) - .toList(), - ), - ), - ), - ], + if (viewType == TableViewType.domainView) + _DomainAssociatedLinksPanel(controller: controller), ], ), ), @@ -131,6 +72,42 @@ class ValidationDetailView extends StatelessWidget { } } +class ValidationDetailHeader extends StatelessWidget { + const ValidationDetailHeader({ + super.key, + required this.viewType, + required this.controller, + }); + + final TableViewType viewType; + final DeepLinksController controller; + + @override + Widget build(BuildContext context) { + return OutlineDecoration( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: defaultSpacing), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + viewType == TableViewType.domainView + ? 'Selected domain validation details' + : 'Selected Deep link validation details', + style: Theme.of(context).textTheme.titleSmall, + ), + IconButton( + onPressed: () => + controller.updateDisplayOptions(showSplitScreen: false), + icon: const Icon(Icons.close), + ), + ], + ), + ), + ); + } +} + class _DomainCheckTable extends StatelessWidget { const _DomainCheckTable({ required this.controller, @@ -144,6 +121,9 @@ class _DomainCheckTable extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + const SizedBox(height: intermediateSpacing), + Text('Domain check', style: Theme.of(context).textTheme.titleSmall), + const SizedBox(height: denseSpacing), DataTable( headingRowColor: MaterialStateProperty.all( Theme.of(context).colorScheme.deeplinkTableHeaderColor, @@ -197,52 +177,121 @@ class _DomainCheckTable extends StatelessWidget { ), ], ), - if (linkData.domainErrors.isNotEmpty) ...[ - const Text('How to fix'), - Text( - 'Add the new recommended Digital Asset Links JSON file to the failed website domain at the correct location.', - style: Theme.of(context).subtleTextStyle, - ), - Text( - 'Update and publish recommend Digital Asset Links JSON file below to this location: ', - style: Theme.of(context).subtleTextStyle, - ), - Align( - alignment: Alignment.centerLeft, - child: Card( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(4.0)), - ), - color: Theme.of(context).colorScheme.outline, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: denseSpacing), - child: SelectionArea( - child: Text( - 'https://${linkData.domain}/.well-known/assetlinks.json', - style: Theme.of(context).regularTextStyle.copyWith( - color: Colors.black, - fontWeight: FontWeight.w500, - ), - ), + if (linkData.domainErrors.isNotEmpty) + _DomainFixPanel(controller: controller), + ], + ); + } +} + +class _DomainFixPanel extends StatelessWidget { + const _DomainFixPanel({ + required this.controller, + }); + + final DeepLinksController controller; + + @override + Widget build(BuildContext context) { + final linkData = controller.selectedLink.value!; + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text('How to fix'), + Text( + 'Add the new recommended Digital Asset Links JSON file to the failed website domain at the correct location.', + style: Theme.of(context).subtleTextStyle, + ), + Text( + 'Update and publish recommend Digital Asset Links JSON file below to this location: ', + style: Theme.of(context).subtleTextStyle, + ), + Align( + alignment: Alignment.centerLeft, + child: Card( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(4.0)), + ), + color: Theme.of(context).colorScheme.outline, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: denseSpacing), + child: SelectionArea( + child: Text( + 'https://${linkData.domain}/.well-known/assetlinks.json', + style: Theme.of(context).regularTextStyle.copyWith( + color: Colors.black, + fontWeight: FontWeight.w500, + ), ), ), ), ), - Card( - child: ValueListenableBuilder( - valueListenable: controller.generatedAssetLinksForSelectedLink, - builder: (_, String? generatedAssetLinks, __) => - generatedAssetLinks != null - ? Align( - alignment: Alignment.centerLeft, - child: SelectionArea( - child: Text(generatedAssetLinks), - ), - ) - : const CenteredCircularProgressIndicator(), + ), + Card( + child: ValueListenableBuilder( + valueListenable: controller.generatedAssetLinksForSelectedLink, + builder: (_, String? generatedAssetLinks, __) => + generatedAssetLinks != null + ? Align( + alignment: Alignment.centerLeft, + child: SelectionArea( + child: Text(generatedAssetLinks), + ), + ) + : const CenteredCircularProgressIndicator(), + ), + ), + ], + ); + } +} + +class _DomainAssociatedLinksPanel extends StatelessWidget { + const _DomainAssociatedLinksPanel({ + required this.controller, + }); + + final DeepLinksController controller; + + @override + Widget build(BuildContext context) { + final linkData = controller.selectedLink.value!; + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text('Associated deep link URL', + style: Theme.of(context).textTheme.titleSmall), + Card( + color: Theme.of(context).colorScheme.surface, + shape: const RoundedRectangleBorder(), + child: Padding( + padding: const EdgeInsets.all(denseSpacing), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: linkData.associatedPath + .map( + (path) => Padding( + padding: const EdgeInsets.symmetric( + vertical: denseRowSpacing, + ), + child: Row( + children: [ + if (linkData.domainErrors.isNotEmpty) + Icon( + Icons.error, + color: Theme.of(context).colorScheme.error, + size: defaultIconSize, + ), + const SizedBox(width: denseSpacing), + Text(path), + ], + ), + ), + ) + .toList(), ), ), - ], + ), ], ); } @@ -259,51 +308,61 @@ class _PathCheckTable extends StatelessWidget { ), ), ); - return Opacity( - opacity: 0.5, - child: DataTable( - headingRowColor: MaterialStateProperty.all( - Theme.of(context).colorScheme.deeplinkTableHeaderColor, - ), - dataRowColor: MaterialStateProperty.all( - Theme.of(context).colorScheme.alternatingBackgroundColor2, + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: intermediateSpacing), + Text( + 'Path check (coming soon)', + style: Theme.of(context).textTheme.titleSmall, ), - columns: const [ - DataColumn(label: Text('OS')), - DataColumn(label: Text('Issue type')), - DataColumn(label: Text('Status')), - ], - rows: [ - DataRow( - cells: [ - const DataCell(Text('Android')), - const DataCell(Text('Intent filter')), - notAvailableCell, - ], - ), - DataRow( - cells: [ - const DataCell(Text('iOS')), - const DataCell(Text('Associated domain')), - notAvailableCell, - ], - ), - DataRow( - cells: [ - const DataCell(Text('Android, iOS')), - const DataCell(Text('URL format')), - notAvailableCell, + Opacity( + opacity: 0.5, + child: DataTable( + headingRowColor: MaterialStateProperty.all( + Theme.of(context).colorScheme.deeplinkTableHeaderColor, + ), + dataRowColor: MaterialStateProperty.all( + Theme.of(context).colorScheme.alternatingBackgroundColor2, + ), + columns: const [ + DataColumn(label: Text('OS')), + DataColumn(label: Text('Issue type')), + DataColumn(label: Text('Status')), ], - ), - DataRow( - cells: [ - const DataCell(Text('Android, iOS')), - const DataCell(Text('Routing')), - notAvailableCell, + rows: [ + DataRow( + cells: [ + const DataCell(Text('Android')), + const DataCell(Text('Intent filter')), + notAvailableCell, + ], + ), + DataRow( + cells: [ + const DataCell(Text('iOS')), + const DataCell(Text('Associated domain')), + notAvailableCell, + ], + ), + DataRow( + cells: [ + const DataCell(Text('Android, iOS')), + const DataCell(Text('URL format')), + notAvailableCell, + ], + ), + DataRow( + cells: [ + const DataCell(Text('Android, iOS')), + const DataCell(Text('Routing')), + notAvailableCell, + ], + ), ], ), - ], - ), + ), + ], ); } } From ef479b19302bb56c999c9af245376b40ba4f259a Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Wed, 8 Nov 2023 15:17:24 -0800 Subject: [PATCH 36/47] Update deep_link_list_view.dart --- .../src/screens/deep_link_validation/deep_link_list_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart index b47d41e1f4c..3a01e81b1c2 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart @@ -80,7 +80,7 @@ class _DeepLinkListViewMainPanel extends StatelessWidget { @override Widget build(BuildContext context) { final controller = Provider.of(context); - + // TODO(hangyujin): Use MultiValueListenableBuilder. return ValueListenableBuilder( valueListenable: controller.displayOptionsNotifier, builder: (context, displayOptions, _) => From 08212543c5914710c7983cef1a54ad33c1fac78b Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Wed, 8 Nov 2023 17:33:00 -0800 Subject: [PATCH 37/47] lint --- .../screens/deep_link_validation/deep_links_controller.dart | 2 +- .../deep_link_validation/validation_details_view.dart | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart index 4498069cadc..f71e45c76e6 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart @@ -83,7 +83,7 @@ class DisplayOptions { final Set filters; DisplayOptions updateFilter(FilterOption option, bool value) { - final newFilter = Set.from(filters); + final newFilter = Set.of(filters); if (value) { newFilter.add(option); diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/validation_details_view.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/validation_details_view.dart index 8479ee02c2a..c12e6fc61c6 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/validation_details_view.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/validation_details_view.dart @@ -259,8 +259,10 @@ class _DomainAssociatedLinksPanel extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text('Associated deep link URL', - style: Theme.of(context).textTheme.titleSmall), + Text( + 'Associated deep link URL', + style: Theme.of(context).textTheme.titleSmall, + ), Card( color: Theme.of(context).colorScheme.surface, shape: const RoundedRectangleBorder(), From 47540b69a6c37d041011d7ffffb06256fe7300ca Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Wed, 8 Nov 2023 18:22:59 -0800 Subject: [PATCH 38/47] Update deep_links_controller.dart --- .../deep_link_validation/deep_links_controller.dart | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart index f71e45c76e6..d3f254ad656 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart @@ -258,8 +258,9 @@ class DeepLinksController { final Map result = json.decode(response.body) as Map; if (result[_domainsKey] != null) { - final String generatedContent = (((result[_domainsKey] as List) - .first) as Map)[_generatedContentKey]; + final String generatedContent = + (result[_domainsKey] as List>) + .first[_generatedContentKey]; generatedAssetLinksForSelectedLink.value = generatedContent; } @@ -292,10 +293,12 @@ class DeepLinksController { for (var domain in domains) domain: [], }; - final validationResult = result[_validationResultKey] as List; + final validationResult = + result[_validationResultKey] as List>; for (final Map domainResult in validationResult) { final String domainName = domainResult[_domainNameKey]; - final List? failedChecks = domainResult[_failedChecksKey]; + final List>? failedChecks = + domainResult[_failedChecksKey]; if (failedChecks != null) { for (final Map failedCheck in failedChecks) { switch (failedCheck[_checkNameKey]) { From bf911bf3a75ae5893be24a0173983aa88671bd4e Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Wed, 8 Nov 2023 18:44:34 -0800 Subject: [PATCH 39/47] Update deep_links_controller.dart --- .../screens/deep_link_validation/deep_links_controller.dart | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart index d3f254ad656..b01a68997f2 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart @@ -293,12 +293,10 @@ class DeepLinksController { for (var domain in domains) domain: [], }; - final validationResult = - result[_validationResultKey] as List>; + final validationResult = result[_validationResultKey] as List; for (final Map domainResult in validationResult) { final String domainName = domainResult[_domainNameKey]; - final List>? failedChecks = - domainResult[_failedChecksKey]; + final List? failedChecks = domainResult[_failedChecksKey]; if (failedChecks != null) { for (final Map failedCheck in failedChecks) { switch (failedCheck[_checkNameKey]) { From 9e0f93780acceeddba2a37284e0ac1057009e652 Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Thu, 9 Nov 2023 11:06:47 -0800 Subject: [PATCH 40/47] Update deep_links_screen_test.dart --- .../test/deep_link_vlidation/deep_links_screen_test.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/devtools_app/test/deep_link_vlidation/deep_links_screen_test.dart b/packages/devtools_app/test/deep_link_vlidation/deep_links_screen_test.dart index c7bcc65eed2..527eee52970 100644 --- a/packages/devtools_app/test/deep_link_vlidation/deep_links_screen_test.dart +++ b/packages/devtools_app/test/deep_link_vlidation/deep_links_screen_test.dart @@ -47,7 +47,7 @@ void main() { group('DeepLinkScreen', () { setUp(() { screen = DeepLinksScreen(); - deepLinksController = DeepLinksController(); + deepLinksController = DeepLinksTestController(); }); testWidgets('builds its tab', (WidgetTester tester) async { @@ -98,3 +98,8 @@ void main() { ); }); } + +class DeepLinksTestController extends DeepLinksController { + @override + void validateLinks() async {} +} From ae0c59b9f0e53f1c70990168f0e7f4f1cddf3cdb Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Thu, 9 Nov 2023 13:02:42 -0800 Subject: [PATCH 41/47] update tests --- .../deep_links_controller.dart | 14 ++++-- .../deep_links_model.dart | 9 ++-- .../deep_links_screen_test.dart | 46 +++++++++++++++---- 3 files changed, 52 insertions(+), 17 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart index b01a68997f2..ae05cea7c7c 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart @@ -5,6 +5,7 @@ import 'dart:async'; import 'dart:convert'; +import 'package:devtools_app_shared/utils.dart'; import 'package:devtools_shared/devtools_deeplink.dart'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; @@ -122,11 +123,17 @@ class DisplayOptions { } } -class DeepLinksController { +class DeepLinksController extends DisposableController { DeepLinksController() { selectedVariantIndex.addListener(_handleSelectedVariantIndexChanged); } + @override + void dispose() { + super.dispose(); + selectedVariantIndex.removeListener(_handleSelectedVariantIndexChanged); + } + DisplayOptions get displayOptions => displayOptionsNotifier.value; List get getLinkDatasByPath { @@ -178,7 +185,6 @@ class DeepLinksController { late final selectedVariantIndex = ValueNotifier(0); void _handleSelectedVariantIndexChanged() { - allLinkDatasNotifier.value = null; unawaited(_loadAndroidAppLinks()); } @@ -198,7 +204,7 @@ class DeepLinksController { }, ); } - validateLinks(); + await validateLinks(); } List get _allLinkDatas { @@ -326,7 +332,7 @@ class DeepLinksController { }).toList(); } - void validateLinks() async { + Future validateLinks() async { allLinkDatasNotifier.value = await _validateAndroidDomain(); displayLinkDatasNotifier.value = _getFilterredLinks(allLinkDatasNotifier.value!); diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart index 3e5ea9f3101..887f4fe0a49 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart @@ -130,9 +130,12 @@ class _ErrorAwareText extends StatelessWidget { ), ), const SizedBox(width: denseSpacing), - Text( - text, - overflow: TextOverflow.ellipsis, + Flexible( + child: + Text( + text, + overflow: TextOverflow.ellipsis, + ), ), ], ); diff --git a/packages/devtools_app/test/deep_link_vlidation/deep_links_screen_test.dart b/packages/devtools_app/test/deep_link_vlidation/deep_links_screen_test.dart index 527eee52970..fb558757ae2 100644 --- a/packages/devtools_app/test/deep_link_vlidation/deep_links_screen_test.dart +++ b/packages/devtools_app/test/deep_link_vlidation/deep_links_screen_test.dart @@ -5,6 +5,7 @@ import 'package:devtools_app/devtools_app.dart'; import 'package:devtools_app/src/screens/deep_link_validation/deep_link_list_view.dart'; import 'package:devtools_app/src/screens/deep_link_validation/deep_links_model.dart'; +import 'package:devtools_app/src/screens/deep_link_validation/validation_details_view.dart'; import 'package:devtools_app/src/shared/directory_picker.dart'; import 'package:devtools_app_shared/ui.dart'; import 'package:devtools_app_shared/utils.dart'; @@ -40,7 +41,7 @@ void main() { ), ); deferredLoadingSupportEnabled = true; - await tester.pumpAndSettle(const Duration(seconds: 1)); + await tester.pump(const Duration(seconds: 1)); expect(find.byType(DeepLinkPage), findsOneWidget); } @@ -75,18 +76,11 @@ void main() { ); testWidgetsWithWindowSize( - 'builds deeplink list page', + 'builds deeplink list page with no links', windowSize, (WidgetTester tester) async { deepLinksController.selectedProject.value = FlutterProject(path: '/abc', androidVariants: ['debug', 'release']); - deepLinksController.allLinkDatasNotifier.value = [ - LinkData( - domain: 'www.google.com', - path: '/', - os: [PlatformOS.android], - ), - ]; await pumpDeepLinkScreen( tester, controller: deepLinksController, @@ -94,6 +88,36 @@ void main() { expect(find.byType(DeepLinkPage), findsOneWidget); expect(find.byType(DeepLinkListView), findsOneWidget); + expect(find.byType(CenteredCircularProgressIndicator), findsOneWidget); + }, + ); + + testWidgetsWithWindowSize( + 'builds deeplink list page with links and split screen', + windowSize, + (WidgetTester tester) async { + final deepLinksController = DeepLinksTestController(); + + deepLinksController.selectedProject.value = + FlutterProject(path: '/abc', androidVariants: ['debug', 'release']); + final linkData = LinkData( + domain: 'www.google.com', + path: '/', + os: [PlatformOS.android], + ); + deepLinksController.allLinkDatasNotifier.value = [linkData]; + deepLinksController.displayOptionsNotifier.value = + DisplayOptions(showSplitScreen: true); + deepLinksController.selectedLink.value = linkData; + + await pumpDeepLinkScreen( + tester, + controller: deepLinksController, + ); + + expect(find.byType(DeepLinkPage), findsOneWidget); + expect(find.byType(DeepLinkListView), findsOneWidget); + expect(find.byType(ValidationDetailView), findsOneWidget); }, ); }); @@ -101,5 +125,7 @@ void main() { class DeepLinksTestController extends DeepLinksController { @override - void validateLinks() async {} + Future validateLinks() async { + displayLinkDatasNotifier.value = allLinkDatasNotifier.value; + } } From e43d1502235a9689bd37587f45881860b846efe4 Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Thu, 9 Nov 2023 13:10:43 -0800 Subject: [PATCH 42/47] Update deep_links_screen_test.dart --- .../test/deep_link_vlidation/deep_links_screen_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devtools_app/test/deep_link_vlidation/deep_links_screen_test.dart b/packages/devtools_app/test/deep_link_vlidation/deep_links_screen_test.dart index fb558757ae2..fb26c086f7d 100644 --- a/packages/devtools_app/test/deep_link_vlidation/deep_links_screen_test.dart +++ b/packages/devtools_app/test/deep_link_vlidation/deep_links_screen_test.dart @@ -122,7 +122,7 @@ void main() { ); }); } - +//TODO(hangyujin): Add more unit tests. class DeepLinksTestController extends DeepLinksController { @override Future validateLinks() async { From 68bf8b910245a02cf93c69b4c4c4614289bf709c Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Thu, 9 Nov 2023 13:12:03 -0800 Subject: [PATCH 43/47] lint --- .../screens/deep_link_validation/deep_links_controller.dart | 2 +- .../lib/src/screens/deep_link_validation/deep_links_model.dart | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart index ae05cea7c7c..4ffd446e13a 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart @@ -204,7 +204,7 @@ class DeepLinksController extends DisposableController { }, ); } - await validateLinks(); + await validateLinks(); } List get _allLinkDatas { diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart index 887f4fe0a49..bc79fd88b45 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart @@ -131,8 +131,7 @@ class _ErrorAwareText extends StatelessWidget { ), const SizedBox(width: denseSpacing), Flexible( - child: - Text( + child: Text( text, overflow: TextOverflow.ellipsis, ), From 8b86de4b38fe7714d6bdc0fc0e3af13fcb3ae55e Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Thu, 9 Nov 2023 13:16:08 -0800 Subject: [PATCH 44/47] add todo --- .../src/screens/deep_link_validation/deep_links_controller.dart | 1 + .../lib/src/screens/deep_link_validation/deep_links_model.dart | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart index 4ffd446e13a..88c1ba60bc7 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart @@ -254,6 +254,7 @@ class DeepLinksController extends DisposableController { _packageNameKey: applicationId, _domainsKey: [selectedLink.value!.domain], // TODO(hangyujin): The fake fingerprints here is just for testing usage, should remove it later. + // TODO(hangyujin): Handle the error case when user doesn't have play console project set up. 'supplemental_sha256_cert_fingerprints': [ '5A:33:EA:64:09:97:F2:F0:24:21:0F:B6:7A:A8:18:1C:18:A9:83:03:20:21:8F:9B:0B:98:BF:43:69:C2:AF:4A', ], diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart index bc79fd88b45..ebdd04290c9 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart @@ -24,6 +24,7 @@ enum PlatformOS { final String description; } +// TODO(hangyujin): Handle more domain error cases. enum DomainError { existence('Domain doesn\'t exist'), fingerprints('Fingerprints unavailable'); From e52ce2708af98c9799ccf1ccdb9a2a4a0c46e45f Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Thu, 9 Nov 2023 13:24:52 -0800 Subject: [PATCH 45/47] lint --- .../test/deep_link_vlidation/deep_links_screen_test.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/devtools_app/test/deep_link_vlidation/deep_links_screen_test.dart b/packages/devtools_app/test/deep_link_vlidation/deep_links_screen_test.dart index fb26c086f7d..b28a2792044 100644 --- a/packages/devtools_app/test/deep_link_vlidation/deep_links_screen_test.dart +++ b/packages/devtools_app/test/deep_link_vlidation/deep_links_screen_test.dart @@ -122,6 +122,7 @@ void main() { ); }); } + //TODO(hangyujin): Add more unit tests. class DeepLinksTestController extends DeepLinksController { @override From c199bf94317fa819afb4dc1bc41ddfbfd882a029 Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Thu, 9 Nov 2023 13:58:30 -0800 Subject: [PATCH 46/47] lint --- .../src/screens/deep_link_validation/deep_link_list_view.dart | 3 ++- .../screens/deep_link_validation/validation_details_view.dart | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart index 3a01e81b1c2..ee95e795c09 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; import 'dart:math'; import 'package:devtools_app_shared/ui.dart'; @@ -53,7 +54,7 @@ class _DeepLinkListViewState extends State // If not found, default to 0. releaseVariantIndex = max(releaseVariantIndex, 0); controller.selectedVariantIndex.value = releaseVariantIndex; - controller.validateLinks(); + unawaited(controller.validateLinks()); }); } diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/validation_details_view.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/validation_details_view.dart index c12e6fc61c6..54d5843ea84 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/validation_details_view.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/validation_details_view.dart @@ -56,9 +56,7 @@ class ValidationDetailView extends StatelessWidget { Align( alignment: Alignment.bottomRight, child: FilledButton( - onPressed: () { - controller.validateLinks(); - }, + onPressed: () async => controller.validateLinks(), child: const Text('Recheck all'), ), ), From 634d4894f6025dd4c7f5eb8d78418c0a61be1781 Mon Sep 17 00:00:00 2001 From: hangyu <108393416+hangyujin@users.noreply.github.com> Date: Thu, 9 Nov 2023 14:07:19 -0800 Subject: [PATCH 47/47] lint --- .../screens/deep_link_validation/validation_details_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/validation_details_view.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/validation_details_view.dart index 54d5843ea84..b670f52cb1f 100644 --- a/packages/devtools_app/lib/src/screens/deep_link_validation/validation_details_view.dart +++ b/packages/devtools_app/lib/src/screens/deep_link_validation/validation_details_view.dart @@ -56,7 +56,7 @@ class ValidationDetailView extends StatelessWidget { Align( alignment: Alignment.bottomRight, child: FilledButton( - onPressed: () async => controller.validateLinks(), + onPressed: () async => await controller.validateLinks(), child: const Text('Recheck all'), ), ),