diff --git a/github_client_app/ios/Runner/Info.plist b/github_client_app/ios/Runner/Info.plist index a38ad21..d6e2385 100644 --- a/github_client_app/ios/Runner/Info.plist +++ b/github_client_app/ios/Runner/Info.plist @@ -41,5 +41,7 @@ UIViewControllerBasedStatusBarAppearance + io.flutter.embedded_views_preview + diff --git a/github_client_app/lib/main.dart b/github_client_app/lib/main.dart index cd67c8a..fadcbb0 100644 --- a/github_client_app/lib/main.dart +++ b/github_client_app/lib/main.dart @@ -60,6 +60,7 @@ class MyApp extends StatelessWidget { "login": (context) => LoginRoute(), "themes": (context) => ThemeChangeRoute(), "language": (context) => LanguageRoute(), + "webview": (context)=> WebViewRoute(), }, ); }, diff --git a/github_client_app/lib/routes/index.dart b/github_client_app/lib/routes/index.dart index e9c8c14..a2ac33c 100644 --- a/github_client_app/lib/routes/index.dart +++ b/github_client_app/lib/routes/index.dart @@ -1,4 +1,5 @@ export 'home_page.dart'; export 'login.dart'; export 'theme_change.dart'; -export 'language.dart'; \ No newline at end of file +export 'language.dart'; +export 'webview.dart'; \ No newline at end of file diff --git a/github_client_app/lib/routes/webview.dart b/github_client_app/lib/routes/webview.dart new file mode 100644 index 0000000..edbe3df --- /dev/null +++ b/github_client_app/lib/routes/webview.dart @@ -0,0 +1,333 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +const String kNavigationExamplePage = ''' + +Navigation Delegate Example + +

+The navigation delegate is set to block navigation to the youtube website. +

+ + + +'''; + +class WebViewRoute extends StatefulWidget { + + + @override + _WebViewRouteState createState() => _WebViewRouteState(); +} + +class _WebViewRouteState extends State { + + final Completer _controller = + Completer(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Flutter WebView example'), + // This drop down menu demonstrates that Flutter widgets can be shown over the web view. + actions: [ + NavigationControls(_controller.future), + SampleMenu(_controller.future), + ], + ), + // We're using a Builder here so we have a context that is below the Scaffold + // to allow calling Scaffold.of(context) so we can show a snackbar. + body: Builder(builder: (BuildContext context) { + return WebView( + initialUrl: ModalRoute.of(context).settings.arguments, + javascriptMode: JavascriptMode.unrestricted, + onWebViewCreated: (WebViewController webViewController) { + _controller.complete(webViewController); + }, + // TODO(iskakaushik): Remove this when collection literals makes it to stable. + // ignore: prefer_collection_literals + javascriptChannels: [ + _toasterJavascriptChannel(context), + ].toSet(), + navigationDelegate: (NavigationRequest request) { + if (request.url.startsWith('https://www.youtube.com/')) { + print('blocking navigation to $request}'); + return NavigationDecision.prevent; + } + print('allowing navigation to $request'); + return NavigationDecision.navigate; + }, + onPageStarted: (String url) { + print('Page started loading: $url'); + }, + onPageFinished: (String url) { + print('Page finished loading: $url'); + }, + gestureNavigationEnabled: true, + ); + }), + floatingActionButton: favoriteButton(), + ); + } + + JavascriptChannel _toasterJavascriptChannel(BuildContext context) { + return JavascriptChannel( + name: 'Toaster', + onMessageReceived: (JavascriptMessage message) { + Scaffold.of(context).showSnackBar( + SnackBar(content: Text(message.message)), + ); + }); + } + + Widget favoriteButton() { + return FutureBuilder( + future: _controller.future, + builder: (BuildContext context, + AsyncSnapshot controller) { + if (controller.hasData) { + return FloatingActionButton( + onPressed: () async { + final String url = await controller.data.currentUrl(); + Scaffold.of(context).showSnackBar( + SnackBar(content: Text('Favorited $url')), + ); + }, + child: const Icon(Icons.favorite), + ); + } + return Container(); + }); + } +} + +enum MenuOptions { + showUserAgent, + listCookies, + clearCookies, + addToCache, + listCache, + clearCache, + navigationDelegate, +} + +class SampleMenu extends StatelessWidget { + SampleMenu(this.controller); + + final Future controller; + final CookieManager cookieManager = CookieManager(); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: controller, + builder: + (BuildContext context, AsyncSnapshot controller) { + return PopupMenuButton( + onSelected: (MenuOptions value) { + switch (value) { + case MenuOptions.showUserAgent: + _onShowUserAgent(controller.data, context); + break; + case MenuOptions.listCookies: + _onListCookies(controller.data, context); + break; + case MenuOptions.clearCookies: + _onClearCookies(context); + break; + case MenuOptions.addToCache: + _onAddToCache(controller.data, context); + break; + case MenuOptions.listCache: + _onListCache(controller.data, context); + break; + case MenuOptions.clearCache: + _onClearCache(controller.data, context); + break; + case MenuOptions.navigationDelegate: + _onNavigationDelegateExample(controller.data, context); + break; + } + }, + itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + value: MenuOptions.showUserAgent, + child: const Text('Show user agent'), + enabled: controller.hasData, + ), + const PopupMenuItem( + value: MenuOptions.listCookies, + child: Text('List cookies'), + ), + const PopupMenuItem( + value: MenuOptions.clearCookies, + child: Text('Clear cookies'), + ), + const PopupMenuItem( + value: MenuOptions.addToCache, + child: Text('Add to cache'), + ), + const PopupMenuItem( + value: MenuOptions.listCache, + child: Text('List cache'), + ), + const PopupMenuItem( + value: MenuOptions.clearCache, + child: Text('Clear cache'), + ), + const PopupMenuItem( + value: MenuOptions.navigationDelegate, + child: Text('Navigation Delegate example'), + ), + ], + ); + }, + ); + } + + void _onShowUserAgent( + WebViewController controller, BuildContext context) async { + // Send a message with the user agent string to the Toaster JavaScript channel we registered + // with the WebView. + await controller.evaluateJavascript( + 'Toaster.postMessage("User Agent: " + navigator.userAgent);'); + } + + void _onListCookies( + WebViewController controller, BuildContext context) async { + final String cookies = + await controller.evaluateJavascript('document.cookie'); + Scaffold.of(context).showSnackBar(SnackBar( + content: Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Cookies:'), + _getCookieList(cookies), + ], + ), + )); + } + + void _onAddToCache(WebViewController controller, BuildContext context) async { + await controller.evaluateJavascript( + 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";'); + Scaffold.of(context).showSnackBar(const SnackBar( + content: Text('Added a test entry to cache.'), + )); + } + + void _onListCache(WebViewController controller, BuildContext context) async { + await controller.evaluateJavascript('caches.keys()' + '.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))' + '.then((caches) => Toaster.postMessage(caches))'); + } + + void _onClearCache(WebViewController controller, BuildContext context) async { + await controller.clearCache(); + Scaffold.of(context).showSnackBar(const SnackBar( + content: Text("Cache cleared."), + )); + } + + void _onClearCookies(BuildContext context) async { + final bool hadCookies = await cookieManager.clearCookies(); + String message = 'There were cookies. Now, they are gone!'; + if (!hadCookies) { + message = 'There are no cookies.'; + } + Scaffold.of(context).showSnackBar(SnackBar( + content: Text(message), + )); + } + + void _onNavigationDelegateExample( + WebViewController controller, BuildContext context) async { + final String contentBase64 = + base64Encode(const Utf8Encoder().convert(kNavigationExamplePage)); + await controller.loadUrl('data:text/html;base64,$contentBase64'); + } + + Widget _getCookieList(String cookies) { + if (cookies == null || cookies == '""') { + return Container(); + } + final List cookieList = cookies.split(';'); + final Iterable cookieWidgets = + cookieList.map((String cookie) => Text(cookie)); + return Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: cookieWidgets.toList(), + ); + } +} + +class NavigationControls extends StatelessWidget { + const NavigationControls(this._webViewControllerFuture) + : assert(_webViewControllerFuture != null); + + final Future _webViewControllerFuture; + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _webViewControllerFuture, + builder: + (BuildContext context, AsyncSnapshot snapshot) { + final bool webViewReady = + snapshot.connectionState == ConnectionState.done; + final WebViewController controller = snapshot.data; + return Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back_ios), + onPressed: !webViewReady + ? null + : () async { + if (await controller.canGoBack()) { + await controller.goBack(); + } else { + Scaffold.of(context).showSnackBar( + const SnackBar(content: Text("No back history item")), + ); + return; + } + }, + ), + IconButton( + icon: const Icon(Icons.arrow_forward_ios), + onPressed: !webViewReady + ? null + : () async { + if (await controller.canGoForward()) { + await controller.goForward(); + } else { + Scaffold.of(context).showSnackBar( + const SnackBar( + content: Text("No forward history item")), + ); + return; + } + }, + ), + IconButton( + icon: const Icon(Icons.replay), + onPressed: !webViewReady + ? null + : () { + controller.reload(); + }, + ), + ], + ); + }, + ); + } +} \ No newline at end of file diff --git a/github_client_app/lib/widgets/repo_item.dart b/github_client_app/lib/widgets/repo_item.dart index 8462456..8a86a3c 100644 --- a/github_client_app/lib/widgets/repo_item.dart +++ b/github_client_app/lib/widgets/repo_item.dart @@ -13,83 +13,88 @@ class _RepoItemState extends State { @override Widget build(BuildContext context) { var subtitle; - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Material( - color: Colors.white, - shape: BorderDirectional( - bottom: BorderSide( - color: Theme.of(context).dividerColor, - width: .5, - ), - ), + return GestureDetector( + onTap: () { + Navigator.of(context) + .pushNamed('webview', arguments: widget.repo.html_url); + }, child: Padding( - padding: const EdgeInsets.only(top: 0.0, bottom: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ListTile( - dense: true, - leading: gmAvatar( - //项目owner头像 - widget.repo.owner.avatar_url, - width: 24.0, - borderRadius: BorderRadius.circular(12), - ), - title: Text( - widget.repo.owner.login, - textScaleFactor: .9, - ), - subtitle: subtitle, - trailing: Text(widget.repo.language ?? ""), + padding: const EdgeInsets.only(top: 8.0), + child: Material( + color: Colors.white, + shape: BorderDirectional( + bottom: BorderSide( + color: Theme.of(context).dividerColor, + width: .5, ), - // 构建项目标题和简介 - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.repo.fork - ? widget.repo.full_name - : widget.repo.name, - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.bold, - fontStyle: widget.repo.fork - ? FontStyle.italic - : FontStyle.normal, - ), + ), + child: Padding( + padding: const EdgeInsets.only(top: 0.0, bottom: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListTile( + dense: true, + leading: gmAvatar( + //项目owner头像 + widget.repo.owner.avatar_url, + width: 24.0, + borderRadius: BorderRadius.circular(12), + ), + title: Text( + widget.repo.owner.login, + textScaleFactor: .9, ), - Padding( - padding: const EdgeInsets.only(top: 8, bottom: 12), - child: widget.repo.description == null - ? Text( - GmLocalizations.of(context).noDescription, - style: TextStyle( - fontStyle: FontStyle.italic, - color: Colors.grey[700]), - ) - : Text( - widget.repo.description, - maxLines: 3, - style: TextStyle( - height: 1.15, - color: Colors.blueGrey[700], - fontSize: 13, - ), - ), + subtitle: subtitle, + trailing: Text(widget.repo.language ?? ""), + ), + // 构建项目标题和简介 + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.repo.fork + ? widget.repo.full_name + : widget.repo.name, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + fontStyle: widget.repo.fork + ? FontStyle.italic + : FontStyle.normal, + ), + ), + Padding( + padding: const EdgeInsets.only(top: 8, bottom: 12), + child: widget.repo.description == null + ? Text( + GmLocalizations.of(context).noDescription, + style: TextStyle( + fontStyle: FontStyle.italic, + color: Colors.grey[700]), + ) + : Text( + widget.repo.description, + maxLines: 3, + style: TextStyle( + height: 1.15, + color: Colors.blueGrey[700], + fontSize: 13, + ), + ), + ), + ], ), - ], - ), + ), + // 构建卡片底部信息 + _buildBottom() + ], ), - // 构建卡片底部信息 - _buildBottom() - ], + ), ), - ), - ), - ); + )); } // 构建卡片底部信息 diff --git a/github_client_app/pubspec.yaml b/github_client_app/pubspec.yaml index 03a8da8..a13ff44 100644 --- a/github_client_app/pubspec.yaml +++ b/github_client_app/pubspec.yaml @@ -14,10 +14,10 @@ dependencies: dio: ^3.0.3 provider: ^3.0.0+1 shared_preferences: ^0.5.1+1 - cached_network_image: ^0.8.0 + cached_network_image: ^2.0.0 fluttertoast: ^3.0.3 flukit: ^1.0.2 - + webview_flutter: ^0.3.19 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^0.1.2