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