From a3777d4d549c8ce60058b19bd787dcffcae9be69 Mon Sep 17 00:00:00 2001 From: Seven Lju Date: Sun, 26 Feb 2017 15:49:10 +0800 Subject: [PATCH 01/24] Initial commit --- .gitignore | 37 ++++++++++ LICENSE | 201 +++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 6 ++ 3 files changed, 244 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5148e52 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules +jspm_packages + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..07c609d --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 Seven Lju + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a26bed2 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# NodeBase +Android NodeJS Platform to Build Sharable Application + +For previous matural version, please explore source code on kotlin branch. + +Currently we are redesigning whole NodeBase based on Flutter. And we will also change the LICENSE from GPL to Apache. From e96faf7995556ea0d2200c162abff4dc1571b1df Mon Sep 17 00:00:00 2001 From: Seven Lju Date: Sat, 5 Sep 2020 15:53:13 +0800 Subject: [PATCH 02/24] init NodeBase app based on Flutter --- .gitignore | 82 ++++--- .metadata | 10 + README.md | 21 +- android/.gitignore | 11 + android/app/build.gradle | 62 +++++ android/app/src/debug/AndroidManifest.xml | 7 + android/app/src/main/AndroidManifest.xml | 47 ++++ .../kotlin/net/seven/nodebase/MainActivity.kt | 50 ++++ .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3173 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 3173 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 3173 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 3173 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 3173 bytes android/app/src/main/res/values/styles.xml | 18 ++ android/app/src/profile/AndroidManifest.xml | 7 + android/build.gradle | 31 +++ android/settings.gradle | 11 + lib/homepage.dart | 87 +++++++ lib/io.dart | 72 ++++++ lib/item_editor.dart | 46 ++++ lib/main.dart | 33 +++ lib/search.dart | 45 ++++ pubspec.lock | 224 ++++++++++++++++++ pubspec.yaml | 75 ++++++ test/widget_test.dart | 30 +++ 26 files changed, 943 insertions(+), 38 deletions(-) create mode 100644 .metadata create mode 100644 android/.gitignore create mode 100644 android/app/build.gradle create mode 100644 android/app/src/debug/AndroidManifest.xml create mode 100644 android/app/src/main/AndroidManifest.xml create mode 100644 android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt create mode 100644 android/app/src/main/res/drawable/launch_background.xml create mode 100644 android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 android/app/src/main/res/values/styles.xml create mode 100644 android/app/src/profile/AndroidManifest.xml create mode 100644 android/build.gradle create mode 100644 android/settings.gradle create mode 100644 lib/homepage.dart create mode 100644 lib/io.dart create mode 100644 lib/item_editor.dart create mode 100644 lib/main.dart create mode 100644 lib/search.dart create mode 100644 pubspec.lock create mode 100644 pubspec.yaml create mode 100644 test/widget_test.dart diff --git a/.gitignore b/.gitignore index 5148e52..9b5dec2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,37 +1,47 @@ -# Logs -logs +# Miscellaneous +*.class *.log -npm-debug.log* - -# Runtime data -pids -*.pid -*.seed - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules -jspm_packages - -# Optional npm cache directory -.npm - -# Optional REPL history -.node_repl_history +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Exceptions to above rules. +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages + +android/gradle* +local diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..d7031d1 --- /dev/null +++ b/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 216dee60c0cc9449f0b29bcf922974d612263e24 + channel: stable + +project_type: app diff --git a/README.md b/README.md index a26bed2..e28ae01 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,23 @@ # NodeBase + + Android NodeJS Platform to Build Sharable Application -For previous matural version, please explore source code on kotlin branch. +Running Node.js application over Wifi and share with your friends. + +For previous mature version, please explore source code on kotlin branch. + +Currently we are redesigning whole NodeBase based on Flutter. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) -Currently we are redesigning whole NodeBase based on Flutter. And we will also change the LICENSE from GPL to Apache. +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..0a741cb --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,11 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..1f1647a --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,62 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 28 + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + applicationId "net.seven.nodebase" + minSdkVersion 16 + targetSdkVersion 28 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..e6181ef --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..f000ca4 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt b/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt new file mode 100644 index 0000000..6d9cdd5 --- /dev/null +++ b/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt @@ -0,0 +1,50 @@ +package net.seven.nodebase + +import androidx.annotation.NonNull +import android.content.Context +import android.content.ContextWrapper +import android.content.Intent +import android.content.IntentFilter +import android.os.BatteryManager +import android.os.Build.VERSION +import android.os.Build.VERSION_CODES + +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodChannel + +class MainActivity: FlutterActivity() { + private val CHANNEL = "samples.flutter.dev/battery" + + override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { + // Note: this method is invoked on the main thread. + call, result -> + if (call.method == "getBatteryLevel") { + val batteryLevel = getBatteryLevel() + + if (batteryLevel != -1) { + result.success(batteryLevel) + } else { + result.error("UNAVAILABLE", "Battery level not available.", null) + } + } else { + result.notImplemented() + } + } + } + + private fun getBatteryLevel(): Int { + val batteryLevel: Int + if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager + batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) + } else { + val intent = ContextWrapper(applicationContext).registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED)) + batteryLevel = intent!!.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1) + } + + return batteryLevel + } +} diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..3a6aa2abfa7356272e613da56ff82696e9043f0f GIT binary patch literal 3173 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 + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..e6181ef --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..3100ad2 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.3.50' + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.5.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..44e62bc --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/lib/homepage.dart b/lib/homepage.dart new file mode 100644 index 0000000..d981f3a --- /dev/null +++ b/lib/homepage.dart @@ -0,0 +1,87 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import './io.dart'; +import './search.dart'; +import './item_editor.dart'; + +class NodeBaseHomePage extends StatefulWidget { + NodeBaseHomePage({Key key, this.title}) : super(key: key); + + final String title; + + @override + _NodeBaseHomePageState createState() => _NodeBaseHomePageState(); +} + +class _NodeBaseHomePageState extends State { + int _counter = 0; + List entries = []; + + @override + void initState() { + super.initState(); + for (var i = 0; i < 100; i ++) entries.add('$i'); + readAppFileAsString("/config.json").then((config) { + if (config != "") { + onReady(jsonDecode(config)); + } + }); + ioLs("/").then((list) { + entries.clear(); + setState(() { + for (var i = 0; i < list.length; i++) entries.add(_dirname(list[i].path)); + }); + }); + } + _dirname(String filepath) { + return filepath.split("/").last; + } + + onReady(config) { + print(config); + if (config == null) return; + setState(() { _counter = config['counter']; }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + actions: [ + IconButton( + onPressed: () { showSearch(context: context, delegate: NodeBaseSearch()); }, + icon: Icon(Icons.search) + ) + ] + ), + body: ListView.separated( + padding: const EdgeInsets.all(8), + itemCount: entries.length, + itemBuilder: (BuildContext context, int index) { + return Container( + child: Card( + child: ListTile( + title: Text('Card ${entries[index]}'), + trailing: PopupMenuButton( + icon: Icon(Icons.more_vert), + onSelected: (int result) { + Navigator.push(context, MaterialPageRoute(builder: (context) => NodeBaseItemEditor())); + }, + itemBuilder: (BuildContext context) => >[ + const PopupMenuItem( value: 101, child: Text('TODO') ) + ] + ) + ) + ) + ); + }, + separatorBuilder: (BuildContext context, int index) => const Divider() + ), + floatingActionButton: FloatingActionButton( + tooltip: 'Add Card', + child: Icon(Icons.add), + ) + ); + } +} diff --git a/lib/io.dart b/lib/io.dart new file mode 100644 index 0000000..84fabfa --- /dev/null +++ b/lib/io.dart @@ -0,0 +1,72 @@ +import 'dart:io'; +import 'package:path_provider/path_provider.dart'; + +// getApplicationDocumentsDirectory -> /data/data/app/... +// getExternalStorageDirectory -> /storage/sdcard-external/android/data/app/... + +Future get _appPath async { + final directory = await getApplicationDocumentsDirectory(); + return directory.path; +} + +Future getAppFileReference(filepath) async { + final path = await _appPath; + return File('$path$filepath'); +} + +Future readAppFileAsString(filepath) async { + try { + final file = await getAppFileReference(filepath); + String contents = await file.readAsString(); + return contents; + } catch (e) { + return ""; + } +} + +Future writeAppFileAsString(filepath, contents) async { + final file = await getAppFileReference(filepath); + file.writeAsString(contents); +} + +Future ioGetEntity(filepath) async { + final path = await _appPath; + final filename = '$path$filepath'; + final T = await FileSystemEntity.type(filename); + if (T == FileSystemEntityType.notFound) { + return null; + } + if (T == FileSystemEntityType.link) { + return Link(filepath); + } + if (T == FileSystemEntityType.file) { + return File(filepath); + } + return Directory(filepath); +} + +Future ioMkdir(filepath) async { + final path = await _appPath; + return await Directory('$path$filepath').create(recursive: true); +} + +Future> ioLs(filepath) async { + final path = await _appPath; + final filename = '$path$filepath'; + final list = []; + final T = await FileSystemEntity.type(filename); + if (T == FileSystemEntityType.notFound) { + } else if (T == FileSystemEntityType.link) { + // ignore files under link directory + list.add(Link(filename)); + } else if (T == FileSystemEntityType.file) { + list.add(File(filename)); + } else { + final dir = Directory(filename); + final entities = await dir.list(recursive: false, followLinks: false).toList(); + entities.forEach((FileSystemEntity entity) { + list.add(entity); + }); + } + return list; +} diff --git a/lib/item_editor.dart b/lib/item_editor.dart new file mode 100644 index 0000000..eeb014d --- /dev/null +++ b/lib/item_editor.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class NodeBaseItemEditor extends StatefulWidget { + NodeBaseItemEditor({Key key}): super(key: key); + @override + _NodeBaseItemEditorState createState() => _NodeBaseItemEditorState(); +} + +class _NodeBaseItemEditorState extends State { + static const platform = const MethodChannel('samples.flutter.dev/battery'); + String _batteryLevel = 'Unknown battery level.'; + + @override + void initState () { + _getBatteryLevel(); + } + + Future _getBatteryLevel() async { + String batteryLevel; + try { + final int result = await platform.invokeMethod('getBatteryLevel'); + batteryLevel = 'Battery level at $result % .'; + } on PlatformException catch (e) { + batteryLevel = "Failed to get battery level: '${e.message}'."; + } + + setState(() { + _batteryLevel = batteryLevel; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('ItemEditor'), + leading: IconButton( + icon: Icon(Icons.arrow_back), + onPressed: () { Navigator.pop(context); } + ) + ), + body: Center( child: Text('ItemEditor $_batteryLevel') ) + ); + } +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..a20a069 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import './homepage.dart'; + +void main() { + runApp(NodeBaseApp()); +} + +class NodeBaseApp extends StatelessWidget { + // This widget is the root of your application. + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'NodeBase', + theme: ThemeData( + // This is the theme of your application. + // + // Try running your application with "flutter run". You'll see the + // application has a blue toolbar. Then, without quitting the app, try + // changing the primarySwatch below to Colors.green and then invoke + // "hot reload" (press "r" in the console where you ran "flutter run", + // or simply save your changes to "hot reload" in a Flutter IDE). + // Notice that the counter didn't reset back to zero; the application + // is not restarted. + primarySwatch: Colors.blue, + // This makes the visual density adapt to the platform that you run + // the app on. For desktop platforms, the controls will be smaller and + // closer together (more dense) than on mobile platforms. + visualDensity: VisualDensity.adaptivePlatformDensity, + ), + home: NodeBaseHomePage(title: 'NodeBase'), + ); + } +} diff --git a/lib/search.dart b/lib/search.dart new file mode 100644 index 0000000..e767054 --- /dev/null +++ b/lib/search.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; + +class NodeBaseSearch extends SearchDelegate { + @override + List buildActions(BuildContext context) { + return [ + IconButton( icon: Icon(Icons.close), onPressed: () { + if (query == "") { + Navigator.pop(context); + } else { + query = ""; + } + } ) + ]; + } + + @override + Widget buildLeading(BuildContext context) { + return IconButton( icon: Icon(Icons.arrow_back), onPressed: () { Navigator.pop(context); } ); + } + + @override + Widget buildResults(BuildContext context) { + return Container( + child: Center( + child: Text("hello") + ) + ); + } + + @override + Widget buildSuggestions(BuildContext context) { + List candidateList = ["a", "b", "c"]; + List suggestionList = []; + query.isEmpty + ? suggestionList = candidateList + : suggestionList.addAll(candidateList.where((x) => x.contains(query))); + return ListView.builder( + itemCount: suggestionList.length, + itemBuilder: (context, index) { + return ListTile( title: Text(suggestionList[index]), onTap: () { } ); + } + ); + } +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..c3589fb --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,224 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.2" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.0" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.3" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.1" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.14.13" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.3" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" + file: + dependency: transitive + description: + name: file + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.2.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + intl: + dependency: transitive + description: + name: intl + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.16.1" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.8" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.8" + path: + dependency: transitive + description: + name: path + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.7.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.6.14" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.0.1+2" + path_provider_macos: + dependency: transitive + description: + name: path_provider_macos + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.0.4+3" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.3" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.1" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.2" + process: + dependency: transitive + description: + name: process + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.13" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.7.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.9.5" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.5" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.17" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.0" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.8" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.0" +sdks: + dart: ">=2.9.0-14.0.dev <3.0.0" + flutter: ">=1.12.13+hotfix.5 <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..5d1edb0 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,75 @@ +name: NodeBase +description: Running Node.js application over Wifi and share with your friends. + +# The following line prevents the package from being accidentally published to +# pub.dev using `pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +version: 1.0.0+1 + +environment: + sdk: ">=2.7.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + path_provider: ^1.6.14 + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^0.1.3 + +dev_dependencies: + flutter_test: + sdk: flutter + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..ac4ec98 --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility that Flutter provides. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:NodeBase/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} From d09d0cfd5cf1f42d4dfcf27b46c79da621d29a94 Mon Sep 17 00:00:00 2001 From: Seven Lju Date: Sat, 5 Sep 2020 23:51:36 +0800 Subject: [PATCH 03/24] migrate original code to flutter - move kotlin code to flutter - init home page, env, platform settings and app view --- android/app/src/main/AndroidManifest.xml | 13 +- .../main/kotlin/net/seven/nodebase/Alarm.kt | 10 + .../kotlin/net/seven/nodebase/Download.kt | 118 +++++++++ .../kotlin/net/seven/nodebase/External.kt | 36 +++ .../kotlin/net/seven/nodebase/MainActivity.kt | 36 ++- .../main/kotlin/net/seven/nodebase/Network.kt | 43 ++++ .../kotlin/net/seven/nodebase/NodeMonitor.kt | 85 ++++++ .../net/seven/nodebase/NodeMonitorEvent.kt | 8 + .../kotlin/net/seven/nodebase/NodeService.kt | 146 +++++++++++ .../kotlin/net/seven/nodebase/Permission.kt | 40 +++ .../main/kotlin/net/seven/nodebase/Storage.kt | 243 ++++++++++++++++++ .../kotlin/net/seven/nodebase/StringUtils.kt | 65 +++++ lib/api.dart | 21 ++ lib/app_model.dart | 25 ++ lib/homepage.dart | 69 +++-- lib/item_editor.dart | 46 ---- lib/page_apps.dart | 42 +++ lib/page_environment.dart | 38 +++ lib/page_platform.dart | 42 +++ pubspec.lock | 7 + pubspec.yaml | 1 + 21 files changed, 1044 insertions(+), 90 deletions(-) create mode 100644 android/app/src/main/kotlin/net/seven/nodebase/Alarm.kt create mode 100644 android/app/src/main/kotlin/net/seven/nodebase/Download.kt create mode 100644 android/app/src/main/kotlin/net/seven/nodebase/External.kt create mode 100644 android/app/src/main/kotlin/net/seven/nodebase/Network.kt create mode 100644 android/app/src/main/kotlin/net/seven/nodebase/NodeMonitor.kt create mode 100644 android/app/src/main/kotlin/net/seven/nodebase/NodeMonitorEvent.kt create mode 100644 android/app/src/main/kotlin/net/seven/nodebase/NodeService.kt create mode 100644 android/app/src/main/kotlin/net/seven/nodebase/Permission.kt create mode 100644 android/app/src/main/kotlin/net/seven/nodebase/Storage.kt create mode 100644 android/app/src/main/kotlin/net/seven/nodebase/StringUtils.kt create mode 100644 lib/api.dart create mode 100644 lib/app_model.dart delete mode 100644 lib/item_editor.dart create mode 100644 lib/page_apps.dart create mode 100644 lib/page_environment.dart create mode 100644 lib/page_platform.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f000ca4..cc57187 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,10 +1,12 @@ - + + + + + + + + () { + + override fun doInBackground(vararg strings: String): String? { + val url = strings[0] + val outfile = strings[1] + var download_stream: InputStream? = null + var output_stream: OutputStream? = null + var conn: HttpURLConnection? = null + publishProgress("Starting ...") + try { + val urlobj = URL(url) + conn = urlobj.openConnection() as HttpURLConnection + if (conn.responseCode / 100 != 2) { + throw IOException("server error: " + conn.responseCode) + } + val file_len = conn.contentLength + val buf = ByteArray(1024 * 1024) + var read_len = 0 + var total_read_len = 0 + download_stream = conn.inputStream + Storage.unlink(outfile) + Storage.touch(outfile) + output_stream = FileOutputStream(outfile) + read_len = download_stream!!.read(buf) + while (read_len >= 0) { + if (isCancelled) { + throw IOException("user cancelled") + } + total_read_len += read_len + output_stream.write(buf, 0, read_len) + var read_size = Storage.readableSize(total_read_len) + if (file_len > 0) { + read_size += " / " + Storage.readableSize(file_len) + } + publishProgress(read_size) + read_len = download_stream!!.read(buf) + } + output_stream.close() + download_stream!!.close() + publishProgress("Finishing ...") + } catch (e: MalformedURLException) { + e.printStackTrace() + return e.toString() + } catch (e: IOException) { + e.printStackTrace() + return e.toString() + } finally { + if (download_stream != null) try { + download_stream.close() + } catch (e: IOException) { + } + + if (output_stream != null) try { + output_stream.close() + } catch (e: IOException) { + } + + if (conn != null) conn.disconnect() + } + return null + } + + override fun onPreExecute() { + super.onPreExecute() + downloader.progress.max = 100 + downloader.progress.progress = 0 + downloader.progress.show() + } + + override fun onProgressUpdate(vararg data: String) { + downloader.progress.setMessage(data[0]) + } + + override fun onPostExecute(result: String?) { + downloader.progress.setMessage("do post actions ...") + if (downloader.callback != null) { + downloader.callback.run() + } + downloader.progress.dismiss() + if (result == null) { + Alarm.showToast(downloader.context, "Download successful") + } else { + Alarm.showToast(downloader.context, "Download failed: $result") + } + } + } + + init { + progress = ProgressDialog(context) + progress.isIndeterminate = true + progress.setProgressStyle(ProgressDialog.STYLE_SPINNER) + progress.setCancelable(true) + } + + fun act(title: String, url: String, outfile: String) { + val task = DownloadTask(this) + progress.setTitle(title) + progress.setOnCancelListener { task.cancel(true) } + task.execute(url, outfile) + } +} diff --git a/android/app/src/main/kotlin/net/seven/nodebase/External.kt b/android/app/src/main/kotlin/net/seven/nodebase/External.kt new file mode 100644 index 0000000..df831b0 --- /dev/null +++ b/android/app/src/main/kotlin/net/seven/nodebase/External.kt @@ -0,0 +1,36 @@ +package net.seven.nodebase + +import android.content.Context +import android.content.Intent +import android.net.Uri + +import java.io.File + +object External { + fun openBrowser(context: Context, url: String) { + val intent = Intent(Intent.ACTION_SEND) + intent.action = "android.intent.action.VIEW" + intent.data = Uri.parse(url) + context.startActivity(intent) + } + + fun shareInformation( + context: Context, title: String, + label: String, text: String, imgFilePath: String?) { + val intent = Intent(Intent.ACTION_SEND) + if (imgFilePath == null || imgFilePath == "") { + intent.type = "text/plain" + } else { + val f = File(imgFilePath) + if (f != null && f.exists() && f.isFile) { + intent.type = "image/jpg" + val u = Uri.fromFile(f) + intent.putExtra(Intent.EXTRA_STREAM, u) + } + } + intent.putExtra(Intent.EXTRA_SUBJECT, label) + intent.putExtra(Intent.EXTRA_TEXT, text) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + context.startActivity(Intent.createChooser(intent, title)) + } +} diff --git a/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt b/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt index 6d9cdd5..659627e 100644 --- a/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt +++ b/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt @@ -14,12 +14,15 @@ import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel class MainActivity: FlutterActivity() { - private val CHANNEL = "samples.flutter.dev/battery" + private val BATTERY_CHANNEL = "net.seven.nodebase/battery" + private val APP_CHANNEL = "net.seven.nodebase/app" + private val NODEBASE_CHANNEL = "net.seven.nodebase/nodebase" override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) - MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { - // Note: this method is invoked on the main thread. + + // Note: MethodCallHandler is invoked on the main thread. + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, BATTERY_CHANNEL).setMethodCallHandler { call, result -> if (call.method == "getBatteryLevel") { val batteryLevel = getBatteryLevel() @@ -33,6 +36,32 @@ class MainActivity: FlutterActivity() { result.notImplemented() } } + + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, APP_CHANNEL).setMethodCallHandler { + call, result -> + if (call.method == "RequestExternalStoragePermission") { + result.success(requestExternalStoragePermission()) + } else if (call.method == "KeepScreenOn") { + var sw: Boolean? = call.argument("sw") + if (sw == true) { + keepScreenOn(true) + } else { + keepScreenOn(false) + } + result.success(0) + } else { + result.notImplemented() + } + } + } + + private fun requestExternalStoragePermission(): Int { + Permission.request(this) + return 0 + } + + private fun keepScreenOn(sw: Boolean) { + Permission.keepScreen(this, sw) } private fun getBatteryLevel(): Int { @@ -44,7 +73,6 @@ class MainActivity: FlutterActivity() { val intent = ContextWrapper(applicationContext).registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED)) batteryLevel = intent!!.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1) } - return batteryLevel } } diff --git a/android/app/src/main/kotlin/net/seven/nodebase/Network.kt b/android/app/src/main/kotlin/net/seven/nodebase/Network.kt new file mode 100644 index 0000000..3617cc5 --- /dev/null +++ b/android/app/src/main/kotlin/net/seven/nodebase/Network.kt @@ -0,0 +1,43 @@ +package net.seven.nodebase + +import android.content.Context +import android.net.wifi.WifiManager + +import java.net.NetworkInterface +import java.net.SocketException +import java.util.Collections +import java.util.HashMap + +object Network { + val nicIps: HashMap> + get() { + val name_ip = HashMap>() + try { + for (nic in Collections.list(NetworkInterface.getNetworkInterfaces())) { + val nic_addr = nic.interfaceAddresses + if (nic_addr.size == 0) continue + val ips = Array(nic_addr.size, { it -> "" }); + val name = nic.name + var index = 0 + for (ia in nic_addr) { + var addr = ia.address.hostAddress + if (addr.indexOf('%') >= 0) { + addr = addr.split("%".toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray()[0] + } + ips[index++] = addr.orEmpty() + } + name_ip[name] = ips + } + } catch (e: SocketException) { + } + + return name_ip + } + + fun getWifiIpv4(context: Context): String { + val wifiManager = context.getSystemService(Context.WIFI_SERVICE) as WifiManager + val wifiInfo = wifiManager.connectionInfo + val ip = wifiInfo.ipAddress + return String.format("%d.%d.%d.%d", ip and 0xff, ip shr 8 and 0xff, ip shr 16 and 0xff, ip shr 24 and 0xff) + } +} diff --git a/android/app/src/main/kotlin/net/seven/nodebase/NodeMonitor.kt b/android/app/src/main/kotlin/net/seven/nodebase/NodeMonitor.kt new file mode 100644 index 0000000..f97a225 --- /dev/null +++ b/android/app/src/main/kotlin/net/seven/nodebase/NodeMonitor.kt @@ -0,0 +1,85 @@ +package net.seven.nodebase + +import android.util.Log + +import java.io.IOException + +class NodeMonitor(val serviceName: String, val command: Array) : Thread() { + + val isRunning: Boolean + get() = state == STATE.RUNNING + + val isReady: Boolean + get() = state == STATE.READY + + val isDead: Boolean + get() = state == STATE.DEAD + + private var state: STATE? = null + private var node_process: Process? = null + private var event: NodeMonitorEvent? = null + + enum class STATE { + BORN, READY, RUNNING, DEAD + } + + init { + state = STATE.BORN + event = null + } + + fun setEvent(event: NodeMonitorEvent): NodeMonitor { + this.event = event + return this + } + + override fun run() { + try { + state = STATE.READY + if (event != null) event!!.before(command) + Log.i("NodeService:NodeMonitor", String.format("node process starting - %s", *command)) + node_process = Runtime.getRuntime().exec(command) + state = STATE.RUNNING + if (event != null) event!!.started(command, node_process!!) + Log.i("NodeService:NodeMonitor", "node process running ...") + node_process!!.waitFor() + /* + BufferedReader reader = new BufferedReader( + new InputStreamReader(_process.getInputStream())); + String line = null; + while ((line = reader.readLine()) != null) { + Log.d("NodeMonitor", line); + } + Log.d("-----", "=========================="); + reader = new BufferedReader( + new InputStreamReader(_process.getErrorStream())); + while ((line = reader.readLine()) != null) { + Log.d("NodeMonitor", line); + } + */ + } catch (e: IOException) { + Log.e("NodeService:NodeMonitor", "node process error", e) + node_process = null + if (event != null) event!!.error(command, null!!) + } catch (e: InterruptedException) { + Log.e("NodeService:NodeMonitor", "node process error", e) + if (event != null) event!!.error(command, node_process!!) + } finally { + state = STATE.DEAD + if (event != null) event!!.after(command, node_process!!) + Log.i("NodeService:NodeMonitor", "node process stopped ...") + } + } + + fun stopService(): Boolean { + if (state == STATE.RUNNING) node_process!!.destroy() + return true + } + + fun restartService(): NodeMonitor { + stopService() + val m = NodeMonitor(serviceName, command) + if (event != null) m.setEvent(event!!) + return m + } +} diff --git a/android/app/src/main/kotlin/net/seven/nodebase/NodeMonitorEvent.kt b/android/app/src/main/kotlin/net/seven/nodebase/NodeMonitorEvent.kt new file mode 100644 index 0000000..15fdf18 --- /dev/null +++ b/android/app/src/main/kotlin/net/seven/nodebase/NodeMonitorEvent.kt @@ -0,0 +1,8 @@ +package net.seven.nodebase + +interface NodeMonitorEvent { + fun before(cmd: Array) + fun started(cmd: Array, process: Process) + fun error(cmd: Array, process: Process) + fun after(cmd: Array, process: Process) +} diff --git a/android/app/src/main/kotlin/net/seven/nodebase/NodeService.kt b/android/app/src/main/kotlin/net/seven/nodebase/NodeService.kt new file mode 100644 index 0000000..dc606c7 --- /dev/null +++ b/android/app/src/main/kotlin/net/seven/nodebase/NodeService.kt @@ -0,0 +1,146 @@ +package net.seven.nodebase + + +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.IBinder +import android.util.Log + +import java.util.HashMap +import java.util.UUID + +class NodeService : Service() { + + override fun onBind(intent: Intent): IBinder? { + throw UnsupportedOperationException("Not yet implemented") + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + while (intent != null) { + val argv = intent.getStringArrayExtra(ARGV) + if (argv.size < 3) break + val auth_token = argv[0] + var cmd = argv[1] + val first = argv[2] + if (AUTH_TOKEN.compareTo(auth_token) != 0) break + /* + command: + - start + - start + - = ... + - restart + - restart -> restart node app by name + - stop + - stop ! -> stop all node apps + - stop -> stop node app by name + */ + when (cmd) { + "start" -> { + if (argv.size >= 4) { + cmd = argv[3] + startNodeApp(first /* name */, cmd) + } + } + "restart" -> restartNodeApp(first /* name */) + "stop" -> if ("!".compareTo(first) == 0) { + stopNodeApps() + } else { + stopNodeApp(first /* name */) + } + } + break + } + // running until explicitly stop + return Service.START_STICKY + } + + override fun onCreate() { + NodeService.refreshAuthToken() + stopNodeApps() + } + + override fun onDestroy() { + stopNodeApps() + } + + private fun stopNodeApps() { + val n = services.keys.size + val keys = arrayOfNulls(n) + for (name in services.keys.iterator()) { + stopNodeApp(name.orEmpty()) + } + } + + private fun stopNodeApp(name: String) { + if (!services.containsKey(name)) return + val monitor = services[name] + monitor!!.stopService() + services.remove(name) + } + + private fun restartNodeApp(name: String) { + if (!services.containsKey(name)) return + var monitor = services[name] + stopNodeApp(name) + monitor = monitor!!.restartService() + services[name] = monitor + monitor.start() + } + + private fun startNodeApp(name: String, cmd: String) { + stopNodeApp(name) + Log.d("NodeService:Command", String.format("%s", cmd)) + val exec = StringUtils.parseArgv(cmd) + val monitor = NodeMonitor(name, exec!!) + services[name] = monitor + monitor.start() + } + + companion object { + val ARGV = "NodeService" + val services = HashMap() + var AUTH_TOKEN = refreshAuthToken() + + fun refreshAuthToken(): String { + val uuid = UUID.randomUUID() + return uuid.toString() + } + + fun checkOutput(cmd: Array): String? { + try { + val p = Runtime.getRuntime().exec(cmd) + p.waitFor() + val `is` = p.inputStream + var len = `is`.available() + var b: ByteArray? = null + if (len > 0) { + b = ByteArray(len) + len = `is`.read(b) + } + `is`.close() + return if (b == null) { + null + } else String(b, 0, len) + } catch (e: Exception) { + return null + } + + } + + + fun touchService(context: Context, args: Array) { + Log.i("NodeService:Signal", "Start Service") + Log.i("NodeService:Signal", String.format("Command - %s", args[1])) + val intent = Intent(context, NodeService::class.java) + intent.putExtra(NodeService.ARGV, args) + context.startService(intent) + } + + fun stopService(context: Context) { + Log.i("NodeService:Signal", "Stop Service") + val intent = Intent(context, NodeService::class.java) + context.stopService(intent) + } + } +} diff --git a/android/app/src/main/kotlin/net/seven/nodebase/Permission.kt b/android/app/src/main/kotlin/net/seven/nodebase/Permission.kt new file mode 100644 index 0000000..af9f0ae --- /dev/null +++ b/android/app/src/main/kotlin/net/seven/nodebase/Permission.kt @@ -0,0 +1,40 @@ +package net.seven.nodebase + +import android.Manifest +import android.app.Activity +import android.content.Context +import android.content.pm.PackageManager +import android.os.PowerManager +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat + +object Permission { + + private var power_wake_lock: PowerManager.WakeLock? = null + private var PERMISSIONS_EXTERNAL_STORAGE = 1 + fun request(activity: Activity) { + val permission: Int + permission = ContextCompat.checkSelfPermission( + activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) + if (permission != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions( + activity, + arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE), + PERMISSIONS_EXTERNAL_STORAGE) + } + } + + fun keepScreen(activity: Activity, on: Boolean) { + val pm = activity.getSystemService(Context.POWER_SERVICE) as PowerManager + if (power_wake_lock == null) { + power_wake_lock = pm.newWakeLock( + PowerManager.PARTIAL_WAKE_LOCK, Permission::class.java!!.getName() + ) + } + if (on) { + power_wake_lock!!.acquire() + } else { + power_wake_lock!!.release() + } + } +} diff --git a/android/app/src/main/kotlin/net/seven/nodebase/Storage.kt b/android/app/src/main/kotlin/net/seven/nodebase/Storage.kt new file mode 100644 index 0000000..97c5112 --- /dev/null +++ b/android/app/src/main/kotlin/net/seven/nodebase/Storage.kt @@ -0,0 +1,243 @@ +package net.seven.nodebase + +import java.io.BufferedOutputStream +import java.io.File +import java.io.FileInputStream +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.net.MalformedURLException +import java.net.URL +import java.net.URLConnection +import java.nio.charset.Charset +import java.util.ArrayList +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream + +object Storage { + + private val READABLE_SIZE_UNIT = arrayOf("B", "KB", "MB", "GB", "TB") + fun download(url: String, outfile: String): Boolean { + var download_stream: InputStream? = null + var output_stream: OutputStream? = null + try { + val urlobj = URL(url) + val conn = urlobj.openConnection() + // int file_len = conn.getContentLength(); + val buf = ByteArray(4096) + var read_len = 0 + download_stream = conn.getInputStream() + Storage.unlink(outfile) + Storage.touch(outfile) + output_stream = FileOutputStream(outfile) + read_len = download_stream!!.read(buf) + while (read_len >= 0) { + output_stream.write(buf, 0, read_len) + read_len = download_stream!!.read(buf) + } + output_stream.close() + download_stream!!.close() + } catch (e: MalformedURLException) { + } catch (e: IOException) { + return false + } finally { + if (download_stream != null) try { + download_stream.close() + } catch (e: IOException) { + } + + if (output_stream != null) try { + output_stream.close() + } catch (e: IOException) { + } + + } + return true + } + + fun copy(infile: String, outfile: String): Boolean { + var in_stream: InputStream? = null + var out_stream: OutputStream? = null + try { + in_stream = FileInputStream(infile) + Storage.unlink(outfile) + Storage.touch(outfile) + out_stream = FileOutputStream(outfile) + val buf = ByteArray(4096) + var read_len = in_stream.read(buf) + while (read_len >= 0) { + out_stream.write(buf, 0, read_len) + read_len = in_stream.read(buf) + } + } catch (e: FileNotFoundException) { + return false + } catch (e: IOException) { + return false + } finally { + if (in_stream != null) try { + in_stream.close() + } catch (e: IOException) { + } + + if (out_stream != null) try { + out_stream.close() + } catch (e: IOException) { + } + + } + return true + } + + fun unlink(infile: String): Boolean { + val file = File(infile) + return if (file.exists()) file.delete() else false + } + + fun touch(infile: String): Boolean { + val file = File(infile) + if (!file.exists()) try { + file.createNewFile() + } catch (e: IOException) { + } + + return true + } + + fun move(infile: String, outfile: String): Boolean { + var r = Storage.copy(infile, outfile) + if (r) { + r = Storage.unlink(infile) + } else { + // rollback + Storage.unlink(outfile) + } + return r + } + + fun executablize(infile: String): Boolean { + val file = File(infile) + return file.setExecutable(true) + } + + fun makeDirectory(path: String): Boolean { + val dir = File(path) + return if (dir.exists()) dir.isDirectory else dir.mkdirs() + } + + fun read(infile: String): String? { + var reader: FileInputStream? = null + val file = File(infile) + try { + val buf = ByteArray(file.length().toInt()) + reader = FileInputStream(file) + reader.read(buf) + return buf.toString(Charset.defaultCharset()) + } catch (e: IOException) { + return null + } finally { + if (reader != null) try { + reader.close() + } catch (e: Exception) { + } + + } + } + + fun write(text: String, outfile: String): Boolean { + var writer: OutputStream? = null + try { + val buf = text.toByteArray() + Storage.touch(outfile) + writer = FileOutputStream(outfile) + writer.write(buf) + } catch (e: FileNotFoundException) { + return false + } catch (e: IOException) { + return false + } finally { + if (writer != null) try { + writer.close() + } catch (e: Exception) { + } + + } + return true + } + + fun listDirectories(path: String): Array? { + val filtered = ArrayList() + val dir = File(path) + if (!dir.exists()) return null + var list = dir.listFiles() + for (f in list) { + if (f.isDirectory) filtered.add(f) + } + return filtered.toTypedArray() + } + + fun listFiles(path: String): Array? { + val filtered = ArrayList() + val dir = File(path) + if (!dir.exists()) return null + var list = dir.listFiles() + for (f in list) { + if (f.isFile) filtered.add(f) + } + return filtered.toTypedArray() + } + + fun unzip(zipfile: String, target_dir: String): Boolean { + try { + val `in` = FileInputStream(zipfile) + val zip = ZipInputStream(`in`) + var entry: ZipEntry? = null + entry = zip.nextEntry + while (entry != null) { + val target_filename = String.format("%s/%s", target_dir, entry!!.name) + if (entry!!.isDirectory) { + Storage.makeDirectory(target_filename) + } else { + val out = FileOutputStream(target_filename) + val writer = BufferedOutputStream(out) + val buf = ByteArray(4096) + var count = zip.read(buf) + while (count != -1) { + writer.write(buf, 0, count) + count = zip.read(buf) + } + writer.close() + out.close() + zip.closeEntry() + } + entry = zip.nextEntry + } + zip.close() + `in`.close() + } catch (e: FileNotFoundException) { + e.printStackTrace() + return false + } catch (e: IOException) { + e.printStackTrace() + return false + } + + return true + } + + fun exists(infile: String): Boolean { + return File(infile).exists() + } + + fun readableSize(size: Int): String { + var index = 0 + val n = READABLE_SIZE_UNIT.size - 1 + var `val` = size.toDouble() + while (`val` > 1024 && index < n) { + index++ + `val` /= 1024.0 + } + return String.format("%.2f %s", `val`, READABLE_SIZE_UNIT[index]) + } +} diff --git a/android/app/src/main/kotlin/net/seven/nodebase/StringUtils.kt b/android/app/src/main/kotlin/net/seven/nodebase/StringUtils.kt new file mode 100644 index 0000000..6f0082c --- /dev/null +++ b/android/app/src/main/kotlin/net/seven/nodebase/StringUtils.kt @@ -0,0 +1,65 @@ +package net.seven.nodebase + +import java.util.ArrayList + +object StringUtils { + fun parseArgv(argv: String?): Array? { + val r = ArrayList() + if (argv == null) return null + var buf = StringBuffer() + var state = 0 + var last = ' ' + loop@ for (ch in argv.toCharArray()) { + when (state) { + 0 -> { + if (Character.isSpaceChar(ch)) { + if (Character.isSpaceChar(last)) { + continue@loop + } + if (buf.length > 0) { + r.add(String(buf)) + buf = StringBuffer() + } + last = ch + continue@loop + } else if (ch == '"') { + state = 1 + } else if (ch == '\'') { + state = 2 + } else if (ch == '\\') { + state += 90 + } + buf.append(ch) + last = ch + } + 1 -> { + buf.append(ch) + if (ch == '"' && last != '\\') { + last = ch + state = 0 + continue@loop + } + last = ch + } + 2 -> { + buf.append(ch) + if (ch == '\'' && last != '\\') { + last = ch + state = 0 + continue@loop + } + last = ch + } + 90, 91, 92 -> { + buf.append(ch) + last = ch + state -= 90 + } + } + } + if (buf.length > 0) { + r.add(String(buf)) + } + return r.toTypedArray() + } +} diff --git a/lib/api.dart b/lib/api.dart new file mode 100644 index 0000000..779a681 --- /dev/null +++ b/lib/api.dart @@ -0,0 +1,21 @@ +import 'package:flutter/services.dart'; + +class NodeBaseApi { + static final batteryApi = const MethodChannel('net.seven.nodebase/battery'); + static final appApi = const MethodChannel('net.seven.nodebase/app'); + + static Future getBatteryLevel() async { + String batteryLevel; + try { + final int lv = await batteryApi.invokeMethod('getBatteryLevel'); + batteryLevel = '${lv}%'; + } on PlatformException catch (e) { + batteryLevel = 'Failed: ${e.message}'; + } + return batteryLevel; + } + + static Future requestExternalStoragePermission () async { + try { appApi.invokeMethod('RequestExternalStoragePermission'); } catch (e) {} + } +} diff --git a/lib/app_model.dart b/lib/app_model.dart new file mode 100644 index 0000000..879a2a7 --- /dev/null +++ b/lib/app_model.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; + +class NodeBaseAppModule { + NodeBaseAppModule({Key key, this.id, this.icon, this.name, this.desc}) {} + + final int id; + final String name; + final String desc; + final Icon icon; + + static final List list = [ + NodeBaseAppModule( + id: 101, icon: Icon(Icons.settings), name: "Environment", + desc: "application configurations." + ), + NodeBaseAppModule( + id: 102, icon: Icon(Icons.settings), name: "Platform", + desc: "platform management, like node, go, python, ..." + ), + NodeBaseAppModule( + id: 102, icon: Icon(Icons.apps), name: "Application", + desc: "application management, like running, developing, sharing, ..." + ) + ]; +} diff --git a/lib/homepage.dart b/lib/homepage.dart index d981f3a..8ddadd0 100644 --- a/lib/homepage.dart +++ b/lib/homepage.dart @@ -2,7 +2,11 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import './io.dart'; import './search.dart'; -import './item_editor.dart'; +import './app_model.dart'; +import './page_environment.dart'; +import './page_platform.dart'; +import './page_apps.dart'; + class NodeBaseHomePage extends StatefulWidget { NodeBaseHomePage({Key key, this.title}) : super(key: key); @@ -14,33 +18,37 @@ class NodeBaseHomePage extends StatefulWidget { } class _NodeBaseHomePageState extends State { - int _counter = 0; - List entries = []; @override void initState() { super.initState(); - for (var i = 0; i < 100; i ++) entries.add('$i'); readAppFileAsString("/config.json").then((config) { if (config != "") { onReady(jsonDecode(config)); } }); - ioLs("/").then((list) { - entries.clear(); - setState(() { - for (var i = 0; i < list.length; i++) entries.add(_dirname(list[i].path)); - }); - }); - } - _dirname(String filepath) { - return filepath.split("/").last; } onReady(config) { print(config); if (config == null) return; - setState(() { _counter = config['counter']; }); + // setState(() { _counter = config['counter']; }); + } + + onNavigate(NodeBaseAppModule module) { + var route; + switch (module.name) { + case "Environment": { + route = MaterialPageRoute(builder: (context) => NodeBaseEnvironmentSettings()); + } break; + case "Platform": { + route = MaterialPageRoute(builder: (context) => NodeBasePlatformSettings()); + } break; + case "Application": { + route = MaterialPageRoute(builder: (context) => NodeBaseApplications()); + } break; + } + Navigator.push(context, route); } @override @@ -55,33 +63,24 @@ class _NodeBaseHomePageState extends State { ) ] ), - body: ListView.separated( + body: ListView.builder( padding: const EdgeInsets.all(8), - itemCount: entries.length, + itemCount: NodeBaseAppModule.list.length, itemBuilder: (BuildContext context, int index) { return Container( child: Card( child: ListTile( - title: Text('Card ${entries[index]}'), - trailing: PopupMenuButton( - icon: Icon(Icons.more_vert), - onSelected: (int result) { - Navigator.push(context, MaterialPageRoute(builder: (context) => NodeBaseItemEditor())); - }, - itemBuilder: (BuildContext context) => >[ - const PopupMenuItem( value: 101, child: Text('TODO') ) - ] - ) - ) - ) + onTap: () => onNavigate(NodeBaseAppModule.list[index]), + title: Text('${NodeBaseAppModule.list[index].name}'), + subtitle: Text('${NodeBaseAppModule.list[index].desc}'), + leading: IconButton( + icon: NodeBaseAppModule.list[index].icon, + ) // IconButton + ) // ListTile + ) // Card ); - }, - separatorBuilder: (BuildContext context, int index) => const Divider() - ), - floatingActionButton: FloatingActionButton( - tooltip: 'Add Card', - child: Icon(Icons.add), - ) + } + ) // body ); } } diff --git a/lib/item_editor.dart b/lib/item_editor.dart deleted file mode 100644 index eeb014d..0000000 --- a/lib/item_editor.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -class NodeBaseItemEditor extends StatefulWidget { - NodeBaseItemEditor({Key key}): super(key: key); - @override - _NodeBaseItemEditorState createState() => _NodeBaseItemEditorState(); -} - -class _NodeBaseItemEditorState extends State { - static const platform = const MethodChannel('samples.flutter.dev/battery'); - String _batteryLevel = 'Unknown battery level.'; - - @override - void initState () { - _getBatteryLevel(); - } - - Future _getBatteryLevel() async { - String batteryLevel; - try { - final int result = await platform.invokeMethod('getBatteryLevel'); - batteryLevel = 'Battery level at $result % .'; - } on PlatformException catch (e) { - batteryLevel = "Failed to get battery level: '${e.message}'."; - } - - setState(() { - _batteryLevel = batteryLevel; - }); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text('ItemEditor'), - leading: IconButton( - icon: Icon(Icons.arrow_back), - onPressed: () { Navigator.pop(context); } - ) - ), - body: Center( child: Text('ItemEditor $_batteryLevel') ) - ); - } -} diff --git a/lib/page_apps.dart b/lib/page_apps.dart new file mode 100644 index 0000000..92a7758 --- /dev/null +++ b/lib/page_apps.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import './api.dart'; + +class NodeBaseApplications extends StatefulWidget { + NodeBaseApplications({Key key}): super(key: key); + @override + _NodeBaseApplicationsState createState() => _NodeBaseApplicationsState(); +} + +class _NodeBaseApplicationsState extends State { + String _batteryLevel = 'Unknown'; + + @override + void initState () { + _getBatteryLevel(); + } + + Future _getBatteryLevel() async { + final String batteryLevel = await NodeBaseApi.getBatteryLevel(); + setState(() { + _batteryLevel = batteryLevel; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Applications'), + leading: IconButton( + icon: Icon(Icons.arrow_back), + onPressed: () { Navigator.pop(context); } + ) + ), + body: Center( child: Text('Applications $_batteryLevel') ), + floatingActionButton: FloatingActionButton( + tooltip: 'Add Application', + child: Icon(Icons.add), + ) + ); + } +} diff --git a/lib/page_environment.dart b/lib/page_environment.dart new file mode 100644 index 0000000..c0c8a3d --- /dev/null +++ b/lib/page_environment.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import './api.dart'; + +class NodeBaseEnvironmentSettings extends StatefulWidget { + NodeBaseEnvironmentSettings({Key key}): super(key: key); + @override + _NodeBaseEnvironmentSettingsState createState() => _NodeBaseEnvironmentSettingsState(); +} + +class _NodeBaseEnvironmentSettingsState extends State { + String _batteryLevel = 'Unknown'; + + @override + void initState () { + _getBatteryLevel(); + } + + Future _getBatteryLevel() async { + final String batteryLevel = await NodeBaseApi.getBatteryLevel(); + setState(() { + _batteryLevel = batteryLevel; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Environment Settings'), + leading: IconButton( + icon: Icon(Icons.arrow_back), + onPressed: () { Navigator.pop(context); } + ) + ), + body: Center( child: Text('Environment Settings $_batteryLevel') ) + ); + } +} diff --git a/lib/page_platform.dart b/lib/page_platform.dart new file mode 100644 index 0000000..72f907c --- /dev/null +++ b/lib/page_platform.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import './api.dart'; + +class NodeBasePlatformSettings extends StatefulWidget { + NodeBasePlatformSettings({Key key}): super(key: key); + @override + _NodeBasePlatformSettingsState createState() => _NodeBasePlatformSettingsState(); +} + +class _NodeBasePlatformSettingsState extends State { + String _batteryLevel = 'Unknown'; + + @override + void initState () { + _getBatteryLevel(); + } + + Future _getBatteryLevel() async { + final String batteryLevel = await NodeBaseApi.getBatteryLevel(); + setState(() { + _batteryLevel = batteryLevel; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Platform Settings'), + leading: IconButton( + icon: Icon(Icons.arrow_back), + onPressed: () { Navigator.pop(context); } + ) + ), + body: Center( child: Text('Platform Settings $_batteryLevel') ), + floatingActionButton: FloatingActionButton( + tooltip: 'Add Platform', + child: Icon(Icons.add), + ) + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index c3589fb..9632367 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -212,6 +212,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.0.8" + webview_flutter: + dependency: "direct main" + description: + name: webview_flutter + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.22+1" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5d1edb0..4d24852 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,6 +24,7 @@ dependencies: flutter: sdk: flutter path_provider: ^1.6.14 + webview_flutter: ^0.3.22+1 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^0.1.3 From 546658a4abe96d6e37d42c1c0c2a0998828b0b65 Mon Sep 17 00:00:00 2001 From: Seven Lju Date: Sun, 6 Sep 2020 13:56:10 +0800 Subject: [PATCH 04/24] implement platform module - platform item CRUD - platform items presistence "platform.json" - download file from remote and set as executable --- .../kotlin/net/seven/nodebase/Download.kt | 4 +- .../kotlin/net/seven/nodebase/MainActivity.kt | 35 +++ lib/api.dart | 16 ++ lib/app_model.dart | 9 + lib/page_platform.dart | 239 +++++++++++++++++- 5 files changed, 294 insertions(+), 9 deletions(-) diff --git a/android/app/src/main/kotlin/net/seven/nodebase/Download.kt b/android/app/src/main/kotlin/net/seven/nodebase/Download.kt index fbf1d30..6f1d65d 100644 --- a/android/app/src/main/kotlin/net/seven/nodebase/Download.kt +++ b/android/app/src/main/kotlin/net/seven/nodebase/Download.kt @@ -12,10 +12,10 @@ import java.net.HttpURLConnection import java.net.MalformedURLException import java.net.URL -class Downloader(private val context: Context, private val callback: Runnable?) { +class Download(private val context: Context, private val callback: Runnable?) { private val progress: ProgressDialog - class DownloadTask(private val downloader: Downloader) : AsyncTask() { + class DownloadTask(private val downloader: Download) : AsyncTask() { override fun doInBackground(vararg strings: String): String? { val url = strings[0] diff --git a/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt b/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt index 659627e..ab2c662 100644 --- a/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt +++ b/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt @@ -9,6 +9,8 @@ import android.os.BatteryManager import android.os.Build.VERSION import android.os.Build.VERSION_CODES +import java.io.File + import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel @@ -49,12 +51,45 @@ class MainActivity: FlutterActivity() { keepScreenOn(false) } result.success(0) + } else if (call.method == "FetchExecutable") { + var src: String? = call.argument("url") + var dst: String? = call.argument("target") + if (src == null || dst == null) { + result.error("INVALID_PARAMS", "invalid parameter.", null) + } else { + val file = File(dst) + val dir = file.getParentFile() + if (!dir.exists()) { + Storage.makeDirectory(dir.getAbsolutePath()) + } + result.success(fetchAndMarkExecutable(src, dst)) + } } else { result.notImplemented() } } } + private fun fetchAndMarkExecutable(src: String, dst: String): Int { + if (src == null) return -1 + if (src.startsWith("file://")) { + Permission.request(this) + var final_src = src + final_src = final_src.substring("file://".length) + if (!Storage.copy(final_src, dst)) return -2 + if (!Storage.executablize(dst)) return -3 + return 0 + } else { + // download + Download(this, Runnable() { + fun run() { + Storage.executablize(dst) + } + }).act("fetch", src, dst) + } + return 0 + } + private fun requestExternalStoragePermission(): Int { Permission.request(this) return 0 diff --git a/lib/api.dart b/lib/api.dart index 779a681..79dbb01 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -1,4 +1,5 @@ import 'package:flutter/services.dart'; +import './io.dart'; class NodeBaseApi { static final batteryApi = const MethodChannel('net.seven.nodebase/battery'); @@ -18,4 +19,19 @@ class NodeBaseApi { static Future requestExternalStoragePermission () async { try { appApi.invokeMethod('RequestExternalStoragePermission'); } catch (e) {} } + + static Future fetchExecutable (String url) async { + if (url == null || url == "") return null; + final name = url.split("/").last; + final dst = (await getAppFileReference('/bin/${name}')).path; + try { + appApi.invokeMethod('FetchExecutable', { + "url": url, + "target": dst + }); + return dst; + } catch(e) { + return null; + } + } } diff --git a/lib/app_model.dart b/lib/app_model.dart index 879a2a7..96d719a 100644 --- a/lib/app_model.dart +++ b/lib/app_model.dart @@ -23,3 +23,12 @@ class NodeBaseAppModule { ) ]; } + +class NodeBasePlatform { + NodeBasePlatform({Key key, this.name}) {} + + String name; + String version; + String path; + String updateUrl; +} diff --git a/lib/page_platform.dart b/lib/page_platform.dart index 72f907c..e533c4a 100644 --- a/lib/page_platform.dart +++ b/lib/page_platform.dart @@ -1,29 +1,238 @@ +import 'dart:convert'; import 'package:flutter/material.dart'; +import './io.dart'; +import './app_model.dart'; import './api.dart'; +class NodeBasePlatformItem extends StatefulWidget { + final Function(NodeBasePlatformItem item) fnRemove; + final Function() fnSaveConfig; + NodeBasePlatform item; + bool isEdit = false; + bool isCreated = false; + + NodeBasePlatformItem({Key key, this.item, this.fnRemove, this.fnSaveConfig}): super(key: key); + + @override + _NodeBasePlatformItemState createState() => _NodeBasePlatformItemState(); +} +class _NodeBasePlatformItemState extends State { + + final ctrlName = TextEditingController(); + final ctrlVersion = TextEditingController(); + final ctrlDownloadUrl = TextEditingController(); + bool _initialized = false; + + @override + void dispose() { + ctrlName.dispose(); + ctrlVersion.dispose(); + ctrlDownloadUrl.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (widget.isEdit) { + if (!_initialized) { + ctrlName.text = widget.item.name; + ctrlVersion.text = widget.item.version; + ctrlDownloadUrl.text = widget.item.updateUrl; + _initialized = true; + } + final entities = [ + ListTile( + leading: Icon(Icons.call_to_action), + title: TextField( + controller: ctrlName, + decoration: InputDecoration( labelText: 'Name' ) + ) + ), + ListTile( + leading: SizedBox(width: 5), + title: TextField( + controller: ctrlVersion, + decoration: InputDecoration( labelText: 'Version' ) + ) + ), + ListTile( + leading: Icon(Icons.attachment), + title: TextField( + controller: ctrlDownloadUrl, + decoration: InputDecoration( labelText: 'Download URL' ) + ), + trailing: IconButton( + icon: Icon(Icons.file_download), + onPressed: () { + if (ctrlDownloadUrl.text == "") return; + NodeBaseApi.fetchExecutable(ctrlDownloadUrl.text).then((dst) { + setState(() { + widget.item.path = dst; + if (!widget.isCreated) widget.fnSaveConfig(); + }); + }); + } + ) // trailing + ) // ListTile + ]; + if (widget.item.path != null && widget.item.path != "") { + entities.add(ListTile( + leading: SizedBox(width: 5), + title: Text(widget.item.path) + )); + } + entities.add( + Row( + children: [ + FlatButton.icon( + icon: Icon(Icons.check), + label: Text("Save"), + onPressed: () { + if (ctrlName.text == "") return; + setState(() { + widget.item.name = ctrlName.text; + widget.item.version = ctrlVersion.text; + widget.item.updateUrl = ctrlDownloadUrl.text; + widget.isCreated = false; + widget.isEdit = false; + widget.fnSaveConfig(); + }); + } + ), + FlatButton.icon( + icon: Icon(Icons.close), + label: Text("Cancel"), + onPressed: () { + if (widget.isCreated) { + widget.fnRemove(widget); + } else { + setState(() { widget.isEdit = false; }); + } + } + ) + ] + ) // Row + ); + return Card( + child: Column( + children: entities + ) // ListView + ); + } + var name = widget.item.name == null?"":widget.item.name; + var version = widget.item.version == null?"":"${widget.item.version}"; + if (version == "") version = ": N/A"; else version = ": ${version}"; + var path = widget.item.path == null?"":widget.item.path; + if (path == "") { + var url = widget.item.updateUrl == null?"":widget.item.updateUrl; + if (url != "") path = "Remotely available @ ${url}"; + else path = "Not yet configured."; + } + return Card( + child: ListTile( + title: Text("${name}${version}"), + subtitle: Text(path), + trailing: PopupMenuButton( + icon: Icon(Icons.more_vert), + onSelected: (int result) { + switch(result) { + case 101: { + setState(() { widget.isEdit = true; }); + } break; + case 102: { + // TODO: if we remove this item, do we need also remove the file at + // widget.item.path? + widget.fnRemove(widget); + widget.fnSaveConfig(); + } break; + } + }, + itemBuilder: (BuildContext context) => >[ + const PopupMenuItem( value: 101, child: Text('Edit') ), + const PopupMenuItem( value: 102, child: Text('Delete') ) + ] + ) // PopupMenuButton + ) + ); + } +} + class NodeBasePlatformSettings extends StatefulWidget { NodeBasePlatformSettings({Key key}): super(key: key); @override _NodeBasePlatformSettingsState createState() => _NodeBasePlatformSettingsState(); } - class _NodeBasePlatformSettingsState extends State { - String _batteryLevel = 'Unknown'; + List entities = []; + var loading = true; @override void initState () { - _getBatteryLevel(); + super.initState(); + loadConfig(); + } + + loadConfig() async { + setState(() { loading = true; }); + var config = await readAppFileAsString("/platform.json"); + final List list = []; + if (config != "") { + final data = jsonDecode(config); + entities.clear(); + data['platforms'].toList().forEach((x) { + final item = NodeBasePlatform(name: x['name']); + item.version = x['version']; + item.path = x['path']; + item.updateUrl = x['url']; + final NodeBasePlatformItem node = makeItem(item); + list.add(node); + }); + } + setState(() { + entities.addAll(list); + loading = false; + }); + } + + saveConfig() async { + setState(() { loading = true; }); + await writeAppFileAsString("/platform.json", JsonEncoder((x) { + if (x is NodeBasePlatformItem) { + return { + "name": x.item.name, + "version": x.item.version, + "path": x.item.path, + "url": x.item.updateUrl + }; + } + return null; + }).convert({ + "platforms": entities + })); + setState(() { loading = false; }); } - Future _getBatteryLevel() async { - final String batteryLevel = await NodeBaseApi.getBatteryLevel(); + removeItem(NodeBasePlatformItem item) { + final index = entities.indexOf(item); + if (index < 0) return; setState(() { - _batteryLevel = batteryLevel; + entities.removeAt(index); }); } + makeItem(NodeBasePlatform item) { + return NodeBasePlatformItem(item: item, fnRemove: removeItem, fnSaveConfig: saveConfig); + } + @override Widget build(BuildContext context) { + if (loading) { + return Scaffold( + body: Center( child: CircularProgressIndicator( + semanticsLabel: "Loading ..." + ) ) + ); + } return Scaffold( appBar: AppBar( title: Text('Platform Settings'), @@ -32,10 +241,26 @@ class _NodeBasePlatformSettingsState extends State { onPressed: () { Navigator.pop(context); } ) ), - body: Center( child: Text('Platform Settings $_batteryLevel') ), + body: (entities.length == 0) + ? Center( child: Text('No platform item.') ) + : ListView.builder( + padding: const EdgeInsets.all(8), + itemCount: entities.length, + itemBuilder: (BuildContext context, int index) { + return Container( child: entities[index] ); + }), // body floatingActionButton: FloatingActionButton( tooltip: 'Add Platform', child: Icon(Icons.add), + onPressed: () { + final item = NodeBasePlatform(name: ""); + final entity = makeItem(item); + setState(() { + entity.isEdit = true; + entity.isCreated = true; + entities.add(entity); + }); + } ) ); } From 5c7765486f2af6dbca79e04833d80e458acdaf50 Mon Sep 17 00:00:00 2001 From: Seven Lju Date: Mon, 7 Sep 2020 00:11:24 +0800 Subject: [PATCH 05/24] implement app module for app list --- lib/app_model.dart | 9 +- lib/page_app_home.dart | 30 ++++++ lib/page_apps.dart | 207 +++++++++++++++++++++++++++++++++++++++-- lib/page_platform.dart | 17 +--- 4 files changed, 239 insertions(+), 24 deletions(-) create mode 100644 lib/page_app_home.dart diff --git a/lib/app_model.dart b/lib/app_model.dart index 96d719a..377ae54 100644 --- a/lib/app_model.dart +++ b/lib/app_model.dart @@ -28,7 +28,14 @@ class NodeBasePlatform { NodeBasePlatform({Key key, this.name}) {} String name; - String version; String path; String updateUrl; } + +class NodeBaseApp { + NodeBaseApp({Key key, this.name}) {} + + String name; + String path; + String platform; +} diff --git a/lib/page_app_home.dart b/lib/page_app_home.dart new file mode 100644 index 0000000..f69446f --- /dev/null +++ b/lib/page_app_home.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import './api.dart'; + +class NodeBaseAppHome extends StatefulWidget { + NodeBaseAppHome({Key key}): super(key: key); + @override + _NodeBaseAppHomeState createState() => _NodeBaseAppHomeState(); +} + +class _NodeBaseAppHomeState extends State { + + @override + void initState () { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Application'), + leading: IconButton( + icon: Icon(Icons.arrow_back), + onPressed: () { Navigator.pop(context); } + ) + ), + body: Center( child: Text('AppHome') ) + ); + } +} diff --git a/lib/page_apps.dart b/lib/page_apps.dart index 92a7758..fb55785 100644 --- a/lib/page_apps.dart +++ b/lib/page_apps.dart @@ -1,29 +1,206 @@ +import 'dart:convert'; import 'package:flutter/material.dart'; +import './io.dart'; +import './app_model.dart'; import './api.dart'; +class NodeBaseAppItem extends StatefulWidget { + final Function(NodeBaseAppItem item) fnRemove; + final Function() fnSaveConfig; + NodeBaseApp item; + bool isEdit = false; + bool isCreated = false; + + NodeBaseAppItem({Key key, this.item, this.fnRemove, this.fnSaveConfig}): super(key: key); + + @override + _NodeBaseAppItemState createState() => _NodeBaseAppItemState(); +} +class _NodeBaseAppItemState extends State { + + final ctrlName = TextEditingController(); + final ctrlPlatform = TextEditingController(); + bool _initialized = false; + + @override + void dispose() { + ctrlName.dispose(); + ctrlPlatform.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (widget.isEdit) { + if (!_initialized) { + ctrlName.text = widget.item.name; + ctrlPlatform.text = widget.item.platform; + _initialized = true; + } + final entities = [ + ListTile( + leading: Icon(Icons.bookmark), + title: TextField( + controller: ctrlName, + decoration: InputDecoration( labelText: 'Name' ) + ) + ), + ListTile( + leading: Icon(Icons.cloud_queue), + title: TextField( + controller: ctrlPlatform, + decoration: InputDecoration( labelText: 'Platform' ) + ) + ) // ListTile + ]; + if (widget.item.path != null && widget.item.path != "") { + entities.add(ListTile( + leading: SizedBox(width: 5), + title: Text(widget.item.path) + )); + } + entities.add( + Row( + children: [ + FlatButton.icon( + icon: Icon(Icons.check), + label: Text("Save"), + onPressed: () { + if (ctrlName.text == "") return; + setState(() { + widget.item.name = ctrlName.text; + widget.item.platform = ctrlPlatform.text; + widget.isCreated = false; + widget.isEdit = false; + widget.fnSaveConfig(); + }); + } + ), + FlatButton.icon( + icon: Icon(Icons.close), + label: Text("Cancel"), + onPressed: () { + if (widget.isCreated) { + widget.fnRemove(widget); + } else { + setState(() { widget.isEdit = false; }); + } + } + ) + ] + ) // Row + ); + return Card( + child: Column( + children: entities + ) // ListView + ); + } + var name = widget.item.name == null?"":widget.item.name; + var platform = widget.item.platform == null?"":widget.item.platform; + return Card( + child: ListTile( + title: Text(name), + subtitle: Text(platform), + trailing: PopupMenuButton( + icon: Icon(Icons.more_vert), + onSelected: (int result) { + switch(result) { + case 101: { + setState(() { widget.isEdit = true; }); + } break; + case 102: { + // TODO: if we remove this item, do we need also remove the file at + // widget.item.path? + widget.fnRemove(widget); + widget.fnSaveConfig(); + } break; + } + }, + itemBuilder: (BuildContext context) => >[ + const PopupMenuItem( value: 101, child: Text('Edit') ), + const PopupMenuItem( value: 102, child: Text('Delete') ) + ] + ) // PopupMenuButton + ) + ); + } +} + class NodeBaseApplications extends StatefulWidget { NodeBaseApplications({Key key}): super(key: key); @override _NodeBaseApplicationsState createState() => _NodeBaseApplicationsState(); } - class _NodeBaseApplicationsState extends State { - String _batteryLevel = 'Unknown'; + List entities = []; + var loading = true; @override void initState () { - _getBatteryLevel(); + super.initState(); + loadConfig(); + } + + loadConfig() async { + setState(() { loading = true; }); + var config = await readAppFileAsString("/apps.json"); + final List list = []; + if (config != "") { + final data = jsonDecode(config); + entities.clear(); + data['apps'].toList().forEach((x) { + final item = NodeBaseApp(name: x['name']); + item.path = x['path']; + item.platform = x['platform']; + final NodeBaseAppItem node = makeItem(item); + list.add(node); + }); + } + setState(() { + entities.addAll(list); + loading = false; + }); + } + + saveConfig() async { + setState(() { loading = true; }); + await writeAppFileAsString("/apps.json", JsonEncoder((x) { + if (x is NodeBaseAppItem) { + return { + "name": x.item.name, + "path": x.item.path, + "platform": x.item.platform + }; + } + return null; + }).convert({ + "apps": entities + })); + setState(() { loading = false; }); } - Future _getBatteryLevel() async { - final String batteryLevel = await NodeBaseApi.getBatteryLevel(); + removeItem(NodeBaseAppItem item) { + final index = entities.indexOf(item); + if (index < 0) return; setState(() { - _batteryLevel = batteryLevel; + entities.removeAt(index); }); } + makeItem(NodeBaseApp item) { + return NodeBaseAppItem(item: item, fnRemove: removeItem, fnSaveConfig: saveConfig); + } + @override Widget build(BuildContext context) { + if (loading) { + return Scaffold( + body: Center( child: CircularProgressIndicator( + semanticsLabel: "Loading ..." + ) ) + ); + } return Scaffold( appBar: AppBar( title: Text('Applications'), @@ -32,10 +209,26 @@ class _NodeBaseApplicationsState extends State { onPressed: () { Navigator.pop(context); } ) ), - body: Center( child: Text('Applications $_batteryLevel') ), + body: (entities.length == 0) + ? Center( child: Text('No application.') ) + : ListView.builder( + padding: const EdgeInsets.all(8), + itemCount: entities.length, + itemBuilder: (BuildContext context, int index) { + return Container( child: entities[index] ); + }), // body floatingActionButton: FloatingActionButton( tooltip: 'Add Application', child: Icon(Icons.add), + onPressed: () { + final item = NodeBaseApp(name: ""); + final entity = makeItem(item); + setState(() { + entity.isEdit = true; + entity.isCreated = true; + entities.add(entity); + }); + } ) ); } diff --git a/lib/page_platform.dart b/lib/page_platform.dart index e533c4a..ac5809c 100644 --- a/lib/page_platform.dart +++ b/lib/page_platform.dart @@ -19,14 +19,12 @@ class NodeBasePlatformItem extends StatefulWidget { class _NodeBasePlatformItemState extends State { final ctrlName = TextEditingController(); - final ctrlVersion = TextEditingController(); final ctrlDownloadUrl = TextEditingController(); bool _initialized = false; @override void dispose() { ctrlName.dispose(); - ctrlVersion.dispose(); ctrlDownloadUrl.dispose(); super.dispose(); } @@ -36,7 +34,6 @@ class _NodeBasePlatformItemState extends State { if (widget.isEdit) { if (!_initialized) { ctrlName.text = widget.item.name; - ctrlVersion.text = widget.item.version; ctrlDownloadUrl.text = widget.item.updateUrl; _initialized = true; } @@ -48,13 +45,6 @@ class _NodeBasePlatformItemState extends State { decoration: InputDecoration( labelText: 'Name' ) ) ), - ListTile( - leading: SizedBox(width: 5), - title: TextField( - controller: ctrlVersion, - decoration: InputDecoration( labelText: 'Version' ) - ) - ), ListTile( leading: Icon(Icons.attachment), title: TextField( @@ -91,7 +81,6 @@ class _NodeBasePlatformItemState extends State { if (ctrlName.text == "") return; setState(() { widget.item.name = ctrlName.text; - widget.item.version = ctrlVersion.text; widget.item.updateUrl = ctrlDownloadUrl.text; widget.isCreated = false; widget.isEdit = false; @@ -120,8 +109,6 @@ class _NodeBasePlatformItemState extends State { ); } var name = widget.item.name == null?"":widget.item.name; - var version = widget.item.version == null?"":"${widget.item.version}"; - if (version == "") version = ": N/A"; else version = ": ${version}"; var path = widget.item.path == null?"":widget.item.path; if (path == "") { var url = widget.item.updateUrl == null?"":widget.item.updateUrl; @@ -130,7 +117,7 @@ class _NodeBasePlatformItemState extends State { } return Card( child: ListTile( - title: Text("${name}${version}"), + title: Text(name), subtitle: Text(path), trailing: PopupMenuButton( icon: Icon(Icons.more_vert), @@ -181,7 +168,6 @@ class _NodeBasePlatformSettingsState extends State { entities.clear(); data['platforms'].toList().forEach((x) { final item = NodeBasePlatform(name: x['name']); - item.version = x['version']; item.path = x['path']; item.updateUrl = x['url']; final NodeBasePlatformItem node = makeItem(item); @@ -200,7 +186,6 @@ class _NodeBasePlatformSettingsState extends State { if (x is NodeBasePlatformItem) { return { "name": x.item.name, - "version": x.item.version, "path": x.item.path, "url": x.item.updateUrl }; From 1900f608efb43c3df3c0374206da453928fb8e67 Mon Sep 17 00:00:00 2001 From: Seven Lju Date: Mon, 7 Sep 2020 14:13:21 +0800 Subject: [PATCH 06/24] init framework for starting application --- .../main/kotlin/net/seven/nodebase/Event.kt | 19 ++++ .../kotlin/net/seven/nodebase/MainActivity.kt | 71 ++++++++++++ .../kotlin/net/seven/nodebase/StringUtils.kt | 14 +-- lib/api.dart | 43 +++++++ lib/page_app_home.dart | 105 +++++++++++++++++- lib/page_apps.dart | 10 +- 6 files changed, 249 insertions(+), 13 deletions(-) create mode 100644 android/app/src/main/kotlin/net/seven/nodebase/Event.kt diff --git a/android/app/src/main/kotlin/net/seven/nodebase/Event.kt b/android/app/src/main/kotlin/net/seven/nodebase/Event.kt new file mode 100644 index 0000000..cbd3de5 --- /dev/null +++ b/android/app/src/main/kotlin/net/seven/nodebase/Event.kt @@ -0,0 +1,19 @@ +package net.seven.nodebase + +import io.flutter.plugin.common.EventChannel + +class NodeBaseEventHandler() : EventChannel.StreamHandler { + var _sink: EventChannel.EventSink? = null + + override fun onListen(p0: Any?, p1: EventChannel.EventSink?) { + _sink = p1 + } + + override fun onCancel(p0: Any?) { + _sink = null + } + + fun send(text: String) { + _sink?.success(text) + } +} diff --git a/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt b/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt index ab2c662..792956c 100644 --- a/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt +++ b/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt @@ -8,17 +8,22 @@ import android.content.IntentFilter import android.os.BatteryManager import android.os.Build.VERSION import android.os.Build.VERSION_CODES +import android.os.Handler import java.io.File import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.EventChannel class MainActivity: FlutterActivity() { private val BATTERY_CHANNEL = "net.seven.nodebase/battery" private val APP_CHANNEL = "net.seven.nodebase/app" private val NODEBASE_CHANNEL = "net.seven.nodebase/nodebase" + private val EVENT_CHANNEL = "net.seven.nodebase/event" + private val eventHandler = NodeBaseEventHandler() + private val NodeBaseServiceMap = mutableMapOf() override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) @@ -64,10 +69,76 @@ class MainActivity: FlutterActivity() { } result.success(fetchAndMarkExecutable(src, dst)) } + } else if (call.method == "FetchWifiIpv4") { + result.success(fetchWifiIpv4()) } else { result.notImplemented() } } + + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, NODEBASE_CHANNEL).setMethodCallHandler { + call, result -> + if (call.method == "GetStatus") { + var app: String? = call.argument("app") + app?.let { result.success(getAppStatus(app)) } + } else if (call.method == "Start") { + var app: String? = call.argument("app") + var cmd: String? = call.argument("cmd") + app?.let { cmd?.let { result.success(startApp(app, cmd)) } } + } else if (call.method == "Stop") { + var app: String? = call.argument("app") + app?.let { result.success(stopApp(app)) } + } else { + result.notImplemented() + } + } + + EventChannel(flutterEngine.dartExecutor.binaryMessenger, EVENT_CHANNEL).setStreamHandler(eventHandler) + } + + private fun getAppStatus(app: String): String { + val m = NodeBaseServiceMap.get(app) + if (m == null) return "n/a" + if (m.isRunning) return "started" + if (m.isDead) return "stopped" + return "unknown" + } + + private fun startApp(app: String, cmd: String): Boolean { + val m = NodeBaseServiceMap.get(app) + if (m != null) { + if (!m.isDead) return true + } + val cmdarr = StringUtils.parseArgv(cmd) + val exec = NodeMonitor(app, cmdarr) + val handler = Handler() + val evt = object: NodeMonitorEvent { + override fun before(cmd: Array) {} + override fun started(cmd: Array, process: Process) { + handler.post(object: Runnable { override fun run() { eventHandler.send(app + "\nstart") } }); + } + override fun error(cmd: Array, process: Process) {} + override fun after(cmd: Array, process: Process) { + handler.post(object: Runnable { override fun run() { eventHandler.send(app + "\nstop") } }); + } + } + exec.setEvent(evt) + NodeBaseServiceMap[app] = exec + exec.start() + return true + } + + private fun stopApp(app: String): Boolean { + val m = NodeBaseServiceMap.get(app) + if (m == null) return true + if (m.isDead) return true + m.stopService() + NodeBaseServiceMap.remove(app) + return true + } + + private fun fetchWifiIpv4(): String { + return Network.getWifiIpv4(this) } private fun fetchAndMarkExecutable(src: String, dst: String): Int { diff --git a/android/app/src/main/kotlin/net/seven/nodebase/StringUtils.kt b/android/app/src/main/kotlin/net/seven/nodebase/StringUtils.kt index 6f0082c..3e90adc 100644 --- a/android/app/src/main/kotlin/net/seven/nodebase/StringUtils.kt +++ b/android/app/src/main/kotlin/net/seven/nodebase/StringUtils.kt @@ -1,11 +1,9 @@ package net.seven.nodebase -import java.util.ArrayList - object StringUtils { - fun parseArgv(argv: String?): Array? { - val r = ArrayList() - if (argv == null) return null + fun parseArgv(argv: String?): Array { + var r = arrayOf() + if (argv == null) return r var buf = StringBuffer() var state = 0 var last = ' ' @@ -17,7 +15,7 @@ object StringUtils { continue@loop } if (buf.length > 0) { - r.add(String(buf)) + r += String(buf) buf = StringBuffer() } last = ch @@ -58,8 +56,8 @@ object StringUtils { } } if (buf.length > 0) { - r.add(String(buf)) + r += String(buf) } - return r.toTypedArray() + return r } } diff --git a/lib/api.dart b/lib/api.dart index 79dbb01..15d8b43 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -4,6 +4,9 @@ import './io.dart'; class NodeBaseApi { static final batteryApi = const MethodChannel('net.seven.nodebase/battery'); static final appApi = const MethodChannel('net.seven.nodebase/app'); + static final nodebaseApi = const MethodChannel('net.seven.nodebase/nodebase'); + + static final eventApi = const EventChannel('net.seven.nodebase/event'); static Future getBatteryLevel() async { String batteryLevel; @@ -24,6 +27,9 @@ class NodeBaseApi { if (url == null || url == "") return null; final name = url.split("/").last; final dst = (await getAppFileReference('/bin/${name}')).path; + // e.g. node -> /path/to/app/bin/node + if (url.indexOf("://") < 0) return dst; + // e.g. file://.../node http://.../node https://.../node -> /path/to/app/bin/node try { appApi.invokeMethod('FetchExecutable', { "url": url, @@ -34,4 +40,41 @@ class NodeBaseApi { return null; } } + + static Future fetchWifiIpv4 () async { + try { + return appApi.invokeMethod('FetchWifiIpv4'); + } catch (e) { + return "0.0.0.0"; + } + } + + static Future appStatus (String app) async { + try { + return nodebaseApi.invokeMethod('GetStatus', { + "app": app + }); + } catch (e) { + return "error"; + } + } + + static Future appStart (String app, String cmd) async { + try { + nodebaseApi.invokeMethod('Start', { + "app": app, + "cmd": cmd + }); + } catch (e) { + } + } + + static Future appStop (String app) async { + try { + nodebaseApi.invokeMethod('Stop', { + "app": app + }); + } catch (e) { + } + } } diff --git a/lib/page_app_home.dart b/lib/page_app_home.dart index f69446f..4091f7f 100644 --- a/lib/page_app_home.dart +++ b/lib/page_app_home.dart @@ -1,30 +1,129 @@ import 'package:flutter/material.dart'; +import './app_model.dart'; import './api.dart'; class NodeBaseAppHome extends StatefulWidget { - NodeBaseAppHome({Key key}): super(key: key); + final NodeBaseApp item; + + NodeBaseAppHome({Key key, this.item}): super(key: key); @override _NodeBaseAppHomeState createState() => _NodeBaseAppHomeState(); } class _NodeBaseAppHomeState extends State { + bool isRunning = false; + String wifiIp = "0.0.0.0"; + final ctrlParams = TextEditingController(); + var eventSub = null; + + appStopped() { + setState(() { isRunning = false; }); + } + + appStarted() { + setState(() { isRunning = true; }); + } + @override void initState () { super.initState(); + NodeBaseApi.fetchWifiIpv4().then((ip) { + setState(() { wifiIp = ip; }); + }); + if (eventSub == null) { + eventSub = NodeBaseApi.eventApi.receiveBroadcastStream().listen( + (m) { + // \n + if (m == null) return; + final parts = m.split("\n"); + if (parts.length < 1) return; + final appname = parts[0]; + final appstat = parts[1]; + if (appname != widget.item.name) return; + switch (appstat) { + case "start": { + appStarted(); + } break; + case "stop": { + appStopped(); + } break; + } + }, + onError: (err) {}, + cancelOnError: true + ); + } + NodeBaseApi.appStatus(widget.item.name).then((status) { + switch(status) { + case "started": { + appStarted(); + } break; + case "stopped": + default: { + appStopped(); + } break; + } + }); + } + + @override + void dispose () { + ctrlParams.dispose(); + eventSub.cancel(); + super.dispose(); } @override Widget build(BuildContext context) { + if (widget.item == null || widget.item.name == null || widget.item.name == "") { + Navigator.pop(context); + return null; + } return Scaffold( appBar: AppBar( - title: Text('Application'), + title: Text('Application - ${widget.item.name}'), leading: IconButton( icon: Icon(Icons.arrow_back), onPressed: () { Navigator.pop(context); } ) ), - body: Center( child: Text('AppHome') ) + body: ListView( + children: [ + ListTile( title: Text('Network: ${wifiIp}') ), + ListTile( title: Text('Platform: ${widget.item.platform}') ), + ListTile( title: TextField( + controller: ctrlParams, + decoration: InputDecoration( labelText: 'Params' ) + ) ), + ListTile( title: Row( + children: [ + IconButton( + icon: Icon(Icons.play_arrow), + onPressed: isRunning ? null : () { + // TODO: read platform config + // TODO: generate command line string + final cmd = "/system/bin/id"; + NodeBaseApi.appStart(widget.item.name, cmd); + } + ), + SizedBox( width: 15 ), + IconButton( + icon: Icon(Icons.stop), + onPressed: isRunning ? () { + NodeBaseApi.appStop(widget.item.name); + } : null + ), + IconButton( + icon: Icon(Icons.open_in_browser), + onPressed: isRunning ? () { + // TODO: open webview? + } : null + ) + ] + ) ) // Row, ListTile + ] + ) // ListView ); } } diff --git a/lib/page_apps.dart b/lib/page_apps.dart index fb55785..a4289e9 100644 --- a/lib/page_apps.dart +++ b/lib/page_apps.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'package:flutter/material.dart'; +import './page_app_home.dart'; import './io.dart'; import './app_model.dart'; import './api.dart'; @@ -93,7 +94,7 @@ class _NodeBaseAppItemState extends State { return Card( child: Column( children: entities - ) // ListView + ) ); } var name = widget.item.name == null?"":widget.item.name; @@ -121,7 +122,12 @@ class _NodeBaseAppItemState extends State { const PopupMenuItem( value: 101, child: Text('Edit') ), const PopupMenuItem( value: 102, child: Text('Delete') ) ] - ) // PopupMenuButton + ), // PopupMenuButton + onTap: () { + Navigator.push(context, MaterialPageRoute( + builder: (context) => NodeBaseAppHome(item: widget.item)) + ); + } ) ); } From 772849063d8ebc65c9b165a4735b189572b10931 Mon Sep 17 00:00:00 2001 From: Seven Lju Date: Tue, 8 Sep 2020 10:41:31 +0800 Subject: [PATCH 07/24] add webview for open app in browser --- .../kotlin/net/seven/nodebase/NodeMonitor.kt | 15 +- lib/page_app_home.dart | 41 ++- lib/page_app_webview.dart | 330 ++++++++++++++++++ 3 files changed, 369 insertions(+), 17 deletions(-) create mode 100644 lib/page_app_webview.dart diff --git a/android/app/src/main/kotlin/net/seven/nodebase/NodeMonitor.kt b/android/app/src/main/kotlin/net/seven/nodebase/NodeMonitor.kt index f97a225..75e6aad 100644 --- a/android/app/src/main/kotlin/net/seven/nodebase/NodeMonitor.kt +++ b/android/app/src/main/kotlin/net/seven/nodebase/NodeMonitor.kt @@ -38,24 +38,17 @@ class NodeMonitor(val serviceName: String, val command: Array) : Thread( state = STATE.READY if (event != null) event!!.before(command) Log.i("NodeService:NodeMonitor", String.format("node process starting - %s", *command)) + node_process = Runtime.getRuntime().exec(command) state = STATE.RUNNING if (event != null) event!!.started(command, node_process!!) Log.i("NodeService:NodeMonitor", "node process running ...") node_process!!.waitFor() /* - BufferedReader reader = new BufferedReader( - new InputStreamReader(_process.getInputStream())); - String line = null; - while ((line = reader.readLine()) != null) { - Log.d("NodeMonitor", line); - } + for (x in command) { System.out.println(" - $x"); } + node_process!!.inputStream.bufferedReader().use { Log.d("NodeMonitor", it.readText()) } Log.d("-----", "=========================="); - reader = new BufferedReader( - new InputStreamReader(_process.getErrorStream())); - while ((line = reader.readLine()) != null) { - Log.d("NodeMonitor", line); - } + node_process!!.errorStream.bufferedReader().use { Log.d("NodeMonitor", it.readText()) } */ } catch (e: IOException) { Log.e("NodeService:NodeMonitor", "node process error", e) diff --git a/lib/page_app_home.dart b/lib/page_app_home.dart index 4091f7f..073d462 100644 --- a/lib/page_app_home.dart +++ b/lib/page_app_home.dart @@ -1,6 +1,9 @@ +import 'dart:convert'; import 'package:flutter/material.dart'; import './app_model.dart'; +import './io.dart'; import './api.dart'; +import './page_app_webview.dart'; class NodeBaseAppHome extends StatefulWidget { final NodeBaseApp item; @@ -25,6 +28,24 @@ class _NodeBaseAppHomeState extends State { setState(() { isRunning = true; }); } + Future loadPlatform(String name) async { + var config = await readAppFileAsString("/platform.json"); + final List list = []; + if (config != "") { + final data = jsonDecode(config); + data['platforms'].toList().forEach((x) { + final item = NodeBasePlatform(name: x['name']); + item.path = x['path']; + item.updateUrl = x['url']; + list.add(item); + }); + if (list.length <= 0) return null; + return list[0]; + } + return null; + } + + @override void initState () { super.initState(); @@ -101,10 +122,15 @@ class _NodeBaseAppHomeState extends State { IconButton( icon: Icon(Icons.play_arrow), onPressed: isRunning ? null : () { - // TODO: read platform config - // TODO: generate command line string - final cmd = "/system/bin/id"; - NodeBaseApi.appStart(widget.item.name, cmd); + setState(() { isRunning = true; }); + loadPlatform(widget.item.platform).then((p) { + if (p == null || p.path == null || p.path == "") { + setState(() { isRunning = false; }); + return; + } + final cmd = "${p.path} ${ctrlParams.text}"; + NodeBaseApi.appStart(widget.item.name, cmd); + }); } ), SizedBox( width: 15 ), @@ -116,8 +142,11 @@ class _NodeBaseAppHomeState extends State { ), IconButton( icon: Icon(Icons.open_in_browser), - onPressed: isRunning ? () { - // TODO: open webview? + onPressed: isRunning == isRunning ? () { + // open webview? + Navigator.push(context, MaterialPageRoute( + builder: (context) => NodeBaseAppWebview() + ) ); } : null ) ] diff --git a/lib/page_app_webview.dart b/lib/page_app_webview.dart new file mode 100644 index 0000000..4ad4812 --- /dev/null +++ b/lib/page_app_webview.dart @@ -0,0 +1,330 @@ +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 NodeBaseAppWebview extends StatefulWidget { + @override + _NodeBaseAppWebviewState createState() => _NodeBaseAppWebviewState(); +} + +class _NodeBaseAppWebviewState 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: 'about:blank', + 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: false, + ); + }), + 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(); + }, + ), + ], + ); + }, + ); + } +} From 06407e46b1f8cd1d5496a100c0d858da67050ace Mon Sep 17 00:00:00 2001 From: Seven Lju Date: Tue, 8 Sep 2020 13:26:31 +0800 Subject: [PATCH 08/24] start/stop/open app - start app and stop app - connect app to platform - import app from a zip file - config.json: { host, port, entry, home } - open app in webview -> host:port/home --- android/app/src/main/AndroidManifest.xml | 1 + .../kotlin/net/seven/nodebase/MainActivity.kt | 28 ++++++- .../main/kotlin/net/seven/nodebase/Storage.kt | 6 ++ lib/api.dart | 12 +++ lib/app_model.dart | 12 +++ lib/io.dart | 6 ++ lib/page_app_home.dart | 81 ++++++++++++++++--- lib/page_app_webview.dart | 30 +++---- 8 files changed, 145 insertions(+), 31 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index cc57187..dc513a5 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -9,6 +9,7 @@ appUnpack(String app, String zipfile) async { + final appBaseDir = await ioGetAppBaseDir(app); + try { + nodebaseApi.invokeMethod('Unpack', { + "app": app, + "path": appBaseDir, + "zipfile": zipfile + }); + } catch (e) { + } + } } diff --git a/lib/app_model.dart b/lib/app_model.dart index 377ae54..acfd61e 100644 --- a/lib/app_model.dart +++ b/lib/app_model.dart @@ -39,3 +39,15 @@ class NodeBaseApp { String path; String platform; } + +class NodeBaseAppDetails { + String path; + // e.g. 127.0.0.1, 0.0.0.0 + String host; + // e.g. 9090 + int port; + // e.g. index.js + String entry; + // e.g. /index.html + String home; +} diff --git a/lib/io.dart b/lib/io.dart index 84fabfa..2eafbc2 100644 --- a/lib/io.dart +++ b/lib/io.dart @@ -70,3 +70,9 @@ Future> ioLs(filepath) async { } return list; } + +Future ioGetAppBaseDir(String app) async { + final path = await _appPath; + final appBaseDir = '${path}/apps/${app}'; + return appBaseDir; +} diff --git a/lib/page_app_home.dart b/lib/page_app_home.dart index 073d462..0e2ee73 100644 --- a/lib/page_app_home.dart +++ b/lib/page_app_home.dart @@ -15,9 +15,11 @@ class NodeBaseAppHome extends StatefulWidget { class _NodeBaseAppHomeState extends State { + bool loading = true; bool isRunning = false; String wifiIp = "0.0.0.0"; final ctrlParams = TextEditingController(); + final ctrlDownload = TextEditingController(); var eventSub = null; appStopped() { @@ -45,6 +47,20 @@ class _NodeBaseAppHomeState extends State { return null; } + Future loadAppDetails(String name) async { + var config = await readAppFileAsString("/apps/${name}/config.json"); + if (config != "") { + final data = jsonDecode(config); + final item = NodeBaseAppDetails(); + item.host = data['host']; + item.port = data['port']; + item.entry = data['entry']; + item.home = data['home']; + item.path = await ioGetAppBaseDir(name); + return item; + } + return null; + } @override void initState () { @@ -85,12 +101,14 @@ class _NodeBaseAppHomeState extends State { appStopped(); } break; } + setState(() { loading = false; }); }); } @override void dispose () { ctrlParams.dispose(); + ctrlDownload.dispose(); eventSub.cancel(); super.dispose(); } @@ -101,6 +119,13 @@ class _NodeBaseAppHomeState extends State { Navigator.pop(context); return null; } + if (loading) { + return Scaffold( + body: Center( child: CircularProgressIndicator( + semanticsLabel: "Loading ..." + ) ) + ); + } return Scaffold( appBar: AppBar( title: Text('Application - ${widget.item.name}'), @@ -122,14 +147,22 @@ class _NodeBaseAppHomeState extends State { IconButton( icon: Icon(Icons.play_arrow), onPressed: isRunning ? null : () { - setState(() { isRunning = true; }); + setState(() { loading = true; }); loadPlatform(widget.item.platform).then((p) { if (p == null || p.path == null || p.path == "") { - setState(() { isRunning = false; }); + setState(() { loading = false; }); return; } - final cmd = "${p.path} ${ctrlParams.text}"; - NodeBaseApi.appStart(widget.item.name, cmd); + loadAppDetails(widget.item.name).then((info) { + if (info == null) { + // no config.json + return; + } + final entry = info.entry == null?"":info.entry; + final cmd = "${p.path} ${info.path}/${entry} ${ctrlParams.text}"; + NodeBaseApi.appStart(widget.item.name, cmd); + setState(() { loading = false; }); + }); }); } ), @@ -142,15 +175,45 @@ class _NodeBaseAppHomeState extends State { ), IconButton( icon: Icon(Icons.open_in_browser), - onPressed: isRunning == isRunning ? () { + onPressed: isRunning ? () { // open webview? - Navigator.push(context, MaterialPageRoute( - builder: (context) => NodeBaseAppWebview() - ) ); + setState(() { loading = true; }); + loadAppDetails(widget.item.name).then((info) { + if (info == null) { + // no config.json + return; + } + setState(() { loading = false; }); + var homeUrl = info.host; + if (info.port > 0) homeUrl += ":${info.port}"; + homeUrl += info.home; + Navigator.push(context, MaterialPageRoute( + builder: (context) => NodeBaseAppWebview( + name: widget.item.name, + home: homeUrl + ) + ) ); + }); } : null ) ] - ) ) // Row, ListTile + ) ), // Row, ListTile + ListTile( + leading: IconButton( + icon: Icon(Icons.file_download), + onPressed: () { + // TODO: if url, download zip to tmp folder and unpack + setState(() { loading = true; }); + NodeBaseApi.appUnpack(widget.item.name, ctrlDownload.text).then((ok) { + setState(() { loading = false; }); + }); + } + ), + title: TextField( + controller: ctrlDownload, + decoration: InputDecoration( labelText: 'Import' ) + ) + ), // Row, ListTile ] ) // ListView ); diff --git a/lib/page_app_webview.dart b/lib/page_app_webview.dart index 4ad4812..2c2688e 100644 --- a/lib/page_app_webview.dart +++ b/lib/page_app_webview.dart @@ -3,22 +3,12 @@ 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 NodeBaseAppWebview extends StatefulWidget { + String name; + String home; + + NodeBaseAppWebview({Key key, this.name, this.home}): super(key: key); + @override _NodeBaseAppWebviewState createState() => _NodeBaseAppWebviewState(); } @@ -31,8 +21,7 @@ class _NodeBaseAppWebviewState extends State { 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. + title: Text(widget.name), actions: [ NavigationControls(_controller.future), SampleMenu(_controller.future), @@ -42,7 +31,7 @@ class _NodeBaseAppWebviewState extends State { // to allow calling Scaffold.of(context) so we can show a snackbar. body: Builder(builder: (BuildContext context) { return WebView( - initialUrl: 'about:blank', + initialUrl: widget.home, // 'about:blank', javascriptMode: JavascriptMode.unrestricted, onWebViewCreated: (WebViewController webViewController) { _controller.complete(webViewController); @@ -246,8 +235,9 @@ class SampleMenu extends StatelessWidget { void _onNavigationDelegateExample( WebViewController controller, BuildContext context) async { - final String contentBase64 = - base64Encode(const Utf8Encoder().convert(kNavigationExamplePage)); + //final String contentBase64 = + // base64Encode(const Utf8Encoder().convert(kNavigationExamplePage)); + String contentBase64 = ''; await controller.loadUrl('data:text/html;base64,$contentBase64'); } From c44391aae957cd072aa67afaa73d9aaf43d20ead Mon Sep 17 00:00:00 2001 From: Seven Lju Date: Tue, 8 Sep 2020 17:01:14 +0800 Subject: [PATCH 09/24] support remove and pack app - remove app and also remove its app folder - pack app to a zip file --- README.md | 36 ++++++++++++++ .../kotlin/net/seven/nodebase/MainActivity.kt | 13 +++++ .../main/kotlin/net/seven/nodebase/Storage.kt | 48 +++++++++++++++++++ lib/api.dart | 12 +++++ lib/io.dart | 21 ++++++++ lib/page_app_home.dart | 13 ++++- lib/page_apps.dart | 6 ++- 7 files changed, 145 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e28ae01..542e63b 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,42 @@ For previous mature version, please explore source code on /static/index.html +[...] source code frontend client + +//index.js +[...] source code for backend server +[...] hook `/index.html` to load `/app/static/index.html` +``` + ## Getting Started This project is a starting point for a Flutter application. diff --git a/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt b/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt index 943634e..266951b 100644 --- a/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt +++ b/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt @@ -99,6 +99,13 @@ class MainActivity: FlutterActivity() { } result.success(fetchAndUnzip(zipfile, path)) } } } + } else if (call.method == "Pack") { + var app: String? = call.argument("app") + var zipfile: String? = call.argument("zipfile") + var path: String? = call.argument("path") + app?.let { zipfile?.let { path?.let { + result.success(fetchAndZip(path, zipfile)) + } } } } else { result.notImplemented() } @@ -107,7 +114,13 @@ class MainActivity: FlutterActivity() { EventChannel(flutterEngine.dartExecutor.binaryMessenger, EVENT_CHANNEL).setStreamHandler(eventHandler) } + private fun fetchAndZip(target_dir: String, zipfile: String): Boolean { + // TODO: wrap a thread instead of running on main thread + return Storage.zip(target_dir, zipfile) + } + private fun fetchAndUnzip(zipfile: String, target_dir: String): Boolean { + // TODO: wrap a thread instead of running on main thread return Storage.unzip(zipfile, target_dir) } diff --git a/android/app/src/main/kotlin/net/seven/nodebase/Storage.kt b/android/app/src/main/kotlin/net/seven/nodebase/Storage.kt index 0e48f10..f8d5053 100644 --- a/android/app/src/main/kotlin/net/seven/nodebase/Storage.kt +++ b/android/app/src/main/kotlin/net/seven/nodebase/Storage.kt @@ -1,5 +1,6 @@ package net.seven.nodebase +import java.io.BufferedInputStream import java.io.BufferedOutputStream import java.io.File import java.io.FileInputStream @@ -15,6 +16,7 @@ import java.nio.charset.Charset import java.util.ArrayList import java.util.zip.ZipEntry import java.util.zip.ZipInputStream +import java.util.zip.ZipOutputStream object Storage { @@ -232,6 +234,52 @@ object Storage { return true } + fun zip(target_dir: String, zipfile: String): Boolean { + val files = ArrayList() + val todos = ArrayList() + val dir = File(target_dir) + if (!dir.exists()) return false + todos.add(dir) + while (todos.size > 0) { + val cur = todos.removeAt(0); + val list = cur.listFiles() + for (f in list) { + if (f.isDirectory) { + todos.add(f) + } else { + files.add(f) + } + } + } + return zip(files.toTypedArray(), target_dir, zipfile) + } + + fun zip(target_files: Array, base_dir: String, zipfile: String): Boolean { + val zipout = ZipOutputStream(BufferedOutputStream(FileOutputStream(zipfile))); + zipout.use { out -> + for (file in target_files) { + val filename = file.getAbsolutePath() + var zipname = filename + if (zipname.startsWith(base_dir)) { + zipname = zipname.substring(base_dir.length) + } + if (zipname.startsWith("/")) { + zipname = zipname.substring(1) + } + System.out.println(filename) + FileInputStream(filename).use { fi -> + BufferedInputStream(fi).use { origin -> + val entry = ZipEntry(zipname) + out.putNextEntry(entry) + origin.copyTo(out, 1024) + zipout.closeEntry() + } + } + } + } + return true + } + fun exists(infile: String): Boolean { return File(infile).exists() } diff --git a/lib/api.dart b/lib/api.dart index a20f5fc..9dbc806 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -89,4 +89,16 @@ class NodeBaseApi { } catch (e) { } } + + static Future appPack(String app, String zipfile) async { + final appBaseDir = await ioGetAppBaseDir(app); + try { + nodebaseApi.invokeMethod('Pack', { + "app": app, + "path": appBaseDir, + "zipfile": zipfile + }); + } catch (e) { + } + } } diff --git a/lib/io.dart b/lib/io.dart index 2eafbc2..32cc6d5 100644 --- a/lib/io.dart +++ b/lib/io.dart @@ -76,3 +76,24 @@ Future ioGetAppBaseDir(String app) async { final appBaseDir = '${path}/apps/${app}'; return appBaseDir; } + +Future ioRemoveApp(String app) async { + final path = await _appPath; + final appBaseDir = '${path}/apps/${app}'; + final dir = Directory(appBaseDir); + if (await dir.exists()) { + await dir.delete(recursive: true); + } + return true; +} + +Future ioMoveApp(String app, String newname) async { + final path = await _appPath; + final appBaseDir = '${path}/apps/${app}'; + final newBaseDir = '${path}/apps/${newname}'; + final dir = Directory(appBaseDir); + if (await dir.exists()) { + await dir.rename(newBaseDir); + } + return true; +} diff --git a/lib/page_app_home.dart b/lib/page_app_home.dart index 0e2ee73..45d043c 100644 --- a/lib/page_app_home.dart +++ b/lib/page_app_home.dart @@ -200,7 +200,7 @@ class _NodeBaseAppHomeState extends State { ) ), // Row, ListTile ListTile( leading: IconButton( - icon: Icon(Icons.file_download), + icon: Icon(Icons.file_upload), onPressed: () { // TODO: if url, download zip to tmp folder and unpack setState(() { loading = true; }); @@ -209,9 +209,18 @@ class _NodeBaseAppHomeState extends State { }); } ), + trailing: IconButton( + icon: Icon(Icons.file_download), + onPressed: () { + setState(() { loading = true; }); + NodeBaseApi.appPack(widget.item.name, ctrlDownload.text).then((ok) { + setState(() { loading = false; }); + }); + } + ), title: TextField( controller: ctrlDownload, - decoration: InputDecoration( labelText: 'Import' ) + decoration: InputDecoration( labelText: 'Import / Export' ) ) ), // Row, ListTile ] diff --git a/lib/page_apps.dart b/lib/page_apps.dart index a4289e9..601cfe9 100644 --- a/lib/page_apps.dart +++ b/lib/page_apps.dart @@ -68,6 +68,9 @@ class _NodeBaseAppItemState extends State { label: Text("Save"), onPressed: () { if (ctrlName.text == "") return; + if (widget.item.name != ctrlName.text) { + ioMoveApp(widget.item.name, ctrlName.text); + } setState(() { widget.item.name = ctrlName.text; widget.item.platform = ctrlPlatform.text; @@ -111,8 +114,7 @@ class _NodeBaseAppItemState extends State { setState(() { widget.isEdit = true; }); } break; case 102: { - // TODO: if we remove this item, do we need also remove the file at - // widget.item.path? + ioRemoveApp(widget.item.name); widget.fnRemove(widget); widget.fnSaveConfig(); } break; From 58a9d5b34709d911fac13cb13e6c4247535ccd7b Mon Sep 17 00:00:00 2001 From: Seven Lju Date: Sun, 20 Sep 2020 19:08:30 +0800 Subject: [PATCH 10/24] polish app home page - add open in external browser button - group operations - basic info - import/export --- .../kotlin/net/seven/nodebase/MainActivity.kt | 10 ++++ lib/api.dart | 9 ++++ lib/page_app_home.dart | 54 +++++++++++++++++-- 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt b/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt index 266951b..8254c0f 100644 --- a/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt +++ b/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt @@ -106,6 +106,11 @@ class MainActivity: FlutterActivity() { app?.let { zipfile?.let { path?.let { result.success(fetchAndZip(path, zipfile)) } } } + } else if (call.method == "Browser") { + var url: String? = call.argument("url") + url?.let { + result.success(openInExternalBrowser(url)) + } } else { result.notImplemented() } @@ -114,6 +119,11 @@ class MainActivity: FlutterActivity() { EventChannel(flutterEngine.dartExecutor.binaryMessenger, EVENT_CHANNEL).setStreamHandler(eventHandler) } + private fun openInExternalBrowser(url: String): Boolean { + External.openBrowser(this, url) + return true + } + private fun fetchAndZip(target_dir: String, zipfile: String): Boolean { // TODO: wrap a thread instead of running on main thread return Storage.zip(target_dir, zipfile) diff --git a/lib/api.dart b/lib/api.dart index 9dbc806..6a12f6e 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -101,4 +101,13 @@ class NodeBaseApi { } catch (e) { } } + + static Future appBrowser(String url) async { + try { + nodebaseApi.invokeMethod('Browser', { + "url": url, + }); + } catch (e) { + } + } } diff --git a/lib/page_app_home.dart b/lib/page_app_home.dart index 45d043c..b8de846 100644 --- a/lib/page_app_home.dart +++ b/lib/page_app_home.dart @@ -18,6 +18,8 @@ class _NodeBaseAppHomeState extends State { bool loading = true; bool isRunning = false; String wifiIp = "0.0.0.0"; + String appHomeUrl = ""; + String appHomePath = ""; final ctrlParams = TextEditingController(); final ctrlDownload = TextEditingController(); var eventSub = null; @@ -66,7 +68,29 @@ class _NodeBaseAppHomeState extends State { void initState () { super.initState(); NodeBaseApi.fetchWifiIpv4().then((ip) { - setState(() { wifiIp = ip; }); + setState(() { + wifiIp = ip; + }); + loadAppDetails(widget.item.name).then((item) { + setState(() { + if (item.host != "") { + final parts = item.host.split("://"); + if (parts.length > 1) { + appHomeUrl = parts[0]; + } else { + appHomeUrl = 'http'; + } + appHomeUrl = '${appHomeUrl}://${ip}'; + if (item.port > 0) { + appHomeUrl = '${appHomeUrl}:${item.port}'; + } + appHomeUrl = '${appHomeUrl}${item.home}'; + } else { + appHomeUrl = ""; + } + appHomePath = item.path; + }); + }); }); if (eventSub == null) { eventSub = NodeBaseApi.eventApi.receiveBroadcastStream().listen( @@ -136,8 +160,19 @@ class _NodeBaseAppHomeState extends State { ), body: ListView( children: [ - ListTile( title: Text('Network: ${wifiIp}') ), + const ListTile( + title: Text('Basic Info'), + dense: true + ), ListTile( title: Text('Platform: ${widget.item.platform}') ), + ListTile( title: SelectableText( + appHomeUrl == ""?'Network: ${wifiIp}':'Home: ${appHomeUrl}', + maxLines: 1 + ) ), + ListTile( title: SelectableText( + 'Location: ${appHomePath}', + maxLines: 1 + ) ), ListTile( title: TextField( controller: ctrlParams, decoration: InputDecoration( labelText: 'Params' ) @@ -176,7 +211,7 @@ class _NodeBaseAppHomeState extends State { IconButton( icon: Icon(Icons.open_in_browser), onPressed: isRunning ? () { - // open webview? + // open webview setState(() { loading = true; }); loadAppDetails(widget.item.name).then((info) { if (info == null) { @@ -195,9 +230,20 @@ class _NodeBaseAppHomeState extends State { ) ); }); } : null + ), + IconButton( + icon: Icon(Icons.open_in_new), + onPressed: (appHomeUrl != "" && isRunning) ? () { + NodeBaseApi.appBrowser(appHomeUrl); + } : null ) ] ) ), // Row, ListTile + const Divider(), + const ListTile( + title: Text('Import/Export'), + dense: true + ), ListTile( leading: IconButton( icon: Icon(Icons.file_upload), @@ -220,7 +266,7 @@ class _NodeBaseAppHomeState extends State { ), title: TextField( controller: ctrlDownload, - decoration: InputDecoration( labelText: 'Import / Export' ) + decoration: InputDecoration( labelText: 'ZIP file path' ) ) ), // Row, ListTile ] From 3a5c1eaeecb52ef2d218e52d15f3e59f10be5733 Mon Sep 17 00:00:00 2001 From: Seven Lju Date: Fri, 25 Sep 2020 13:54:38 +0800 Subject: [PATCH 11/24] bug fix: optimize app process management - bug fix: get platform by name not work - bug fix: make sure sub process can be killed - recently we tested on golang http server it can be started but cannot be killed by `p.destroy()` here we use a hack way to get process id and use `android.os.Process#killProcess` to kill it --- .../kotlin/net/seven/nodebase/NodeMonitor.kt | 28 +++++++++++++++++++ lib/page_app_home.dart | 1 + lib/page_app_webview.dart | 3 +- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/kotlin/net/seven/nodebase/NodeMonitor.kt b/android/app/src/main/kotlin/net/seven/nodebase/NodeMonitor.kt index 75e6aad..83a065d 100644 --- a/android/app/src/main/kotlin/net/seven/nodebase/NodeMonitor.kt +++ b/android/app/src/main/kotlin/net/seven/nodebase/NodeMonitor.kt @@ -66,6 +66,34 @@ class NodeMonitor(val serviceName: String, val command: Array) : Thread( fun stopService(): Boolean { if (state == STATE.RUNNING) node_process!!.destroy() + state = STATE.DEAD + val p = node_process!! + // to make sure the sub process is killed eventually + android.os.Handler().postDelayed({ + if (p.isAlive()) { + val klass = p.javaClass + if (klass.getName().equals("java.lang.UNIXProcess")) { + Log.d("NodeMonitor", "force terminate sub process ..") + try { + var pid = -1; + val f = klass.getDeclaredField("pid"); + f.setAccessible(true); + // this try to make sure if getInt throw an error, + // `setAccessible(false)` can be executed + // so that `pid` is protected after this access + try { pid = f.getInt(p); } catch (e: Exception) { } + f.setAccessible(false); + if (pid > 0) android.os.Process.killProcess(pid); + Log.d("NodeMonitor", "force terminating done.") + } catch (e: Exception) { + Log.d("NodeMonitor", "force terminating failed.") + } + } else { + Log.d("NodeMonitor", p.javaClass.getName()) + Log.d("NodeMonitor", "force terminating not supported.") + } + } + }, 1000) return true } diff --git a/lib/page_app_home.dart b/lib/page_app_home.dart index b8de846..8da6c1c 100644 --- a/lib/page_app_home.dart +++ b/lib/page_app_home.dart @@ -38,6 +38,7 @@ class _NodeBaseAppHomeState extends State { if (config != "") { final data = jsonDecode(config); data['platforms'].toList().forEach((x) { + if (x['name'] != name) return; final item = NodeBasePlatform(name: x['name']); item.path = x['path']; item.updateUrl = x['url']; diff --git a/lib/page_app_webview.dart b/lib/page_app_webview.dart index 2c2688e..3a2b53c 100644 --- a/lib/page_app_webview.dart +++ b/lib/page_app_webview.dart @@ -237,7 +237,8 @@ class SampleMenu extends StatelessWidget { WebViewController controller, BuildContext context) async { //final String contentBase64 = // base64Encode(const Utf8Encoder().convert(kNavigationExamplePage)); - String contentBase64 = ''; + String contentBase64 = base64Encode(const Utf8Encoder().convert('')); + //String contentBase64 = ''; await controller.loadUrl('data:text/html;base64,$contentBase64'); } From ab64796172639e1541661691c158772be62a71e3 Mon Sep 17 00:00:00 2001 From: Seven Lju Date: Fri, 25 Sep 2020 16:12:21 +0800 Subject: [PATCH 12/24] bug fix: destroy one level children when destroy subprocess --- .../kotlin/net/seven/nodebase/NodeMonitor.kt | 81 ++++++++++++------- 1 file changed, 53 insertions(+), 28 deletions(-) diff --git a/android/app/src/main/kotlin/net/seven/nodebase/NodeMonitor.kt b/android/app/src/main/kotlin/net/seven/nodebase/NodeMonitor.kt index 83a065d..a791473 100644 --- a/android/app/src/main/kotlin/net/seven/nodebase/NodeMonitor.kt +++ b/android/app/src/main/kotlin/net/seven/nodebase/NodeMonitor.kt @@ -64,36 +64,61 @@ class NodeMonitor(val serviceName: String, val command: Array) : Thread( } } - fun stopService(): Boolean { - if (state == STATE.RUNNING) node_process!!.destroy() - state = STATE.DEAD + fun pidService(): Int { val p = node_process!! - // to make sure the sub process is killed eventually - android.os.Handler().postDelayed({ - if (p.isAlive()) { - val klass = p.javaClass - if (klass.getName().equals("java.lang.UNIXProcess")) { - Log.d("NodeMonitor", "force terminate sub process ..") - try { - var pid = -1; - val f = klass.getDeclaredField("pid"); - f.setAccessible(true); - // this try to make sure if getInt throw an error, - // `setAccessible(false)` can be executed - // so that `pid` is protected after this access - try { pid = f.getInt(p); } catch (e: Exception) { } - f.setAccessible(false); - if (pid > 0) android.os.Process.killProcess(pid); - Log.d("NodeMonitor", "force terminating done.") - } catch (e: Exception) { - Log.d("NodeMonitor", "force terminating failed.") - } - } else { - Log.d("NodeMonitor", p.javaClass.getName()) - Log.d("NodeMonitor", "force terminating not supported.") - } + if (p == null) return -1 + if (!p.isAlive()) return -1 + val klass = p.javaClass + if ("java.lang.UNIXProcess".equals(klass.getName())) { + try { + var pid = -1 + val f = klass.getDeclaredField("pid"); + f.setAccessible(true); + // this try to make sure if getInt throw an error, + // `setAccessible(false)` can be executed + // so that `pid` is protected after this access + try { pid = f.getInt(p); } catch (e: Exception) { } + f.setAccessible(false); + return pid + } catch (e: Exception) { } + } + return -1 + } + + fun childrenProcesses(pid: Int): Array { + var children = arrayOf() + val output = NodeService.checkOutput(arrayOf("/system/bin/ps", "-o", "pid=", "--ppid", pid.toString())) + if (output == "") return children + val lines = output!!.split("\n") + lines.forEach { + if (it != "") { + try { + children += it.toInt() + } catch(e: Exception) {} } - }, 1000) + } + return children + } + + fun stopService(): Boolean { + val pid = pidService(); + if (pid > 0) { + // XXX: we only make sure one level children processes can be cleaned up + // for example `go run test.go` -> `test` + // we reap `test` first and then kill `go run test.go` + // we do not guarantee `test` children are killed + // another example, if we use `sh -c "go run test.go"` -> `go run test.go` -> `test` + // when kill, we merely kill `go` and `sh` but no `test` + Log.d("NodeMonitor", NodeService.checkOutput(arrayOf("/system/bin/ps", "-ef"))) + val children = childrenProcesses(pid) + children.forEach { + if (it > 0) { + Log.d("NodeMonitor", String.format("kill %d | parent=%d", it, pid)) + android.os.Process.killProcess(it) + } + } + } + if (state == STATE.RUNNING) node_process!!.destroy() return true } From fe5c025c962a55d8d46364e9cd35ead1cf9470ef Mon Sep 17 00:00:00 2001 From: Seven Lju Date: Mon, 28 Sep 2020 00:22:16 +0800 Subject: [PATCH 13/24] update README.md --- README.md | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 542e63b..a0ce9aa 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Currently we are redesigning whole NodeBase based on Flutter. - fill node url like `file:///sdcard/bin-node-v10.10.0` or `https://example.com/latest/arm/node` - click download button and wait for task complete - (NodeBase will copy the binary to its app zone and make it executable) + - Wow; now we not only support node binary but also customized exectuables. - Create a new app, for example named `test` and its platform is `node` - click into the new app - download an app zip into for example `/sdcard/test.zip` @@ -23,7 +24,7 @@ Currently we are redesigning whole NodeBase based on Flutter. - (NodeBase will extract zip app as a app folder into app zone) - fill `Params` text field (for example, file manager need to config target folder as first param) - click `play` button to start node app - - click `open in browser` button to open the app in a webview + - click `open in browser` button to open the app in a webview / `pop-out` button to open in external browser - click `stop` button to stop node app ### App folder structure @@ -41,11 +42,13 @@ Currently we are redesigning whole NodeBase based on Flutter. [...] source code frontend client //index.js +ref: https://github.com/stallpool/halfbase/tree/master/nodejs/tinyserver/index.js [...] source code for backend server [...] hook `/index.html` to load `/app/static/index.html` ``` -## Getting Started + +## Development This project is a starting point for a Flutter application. @@ -57,3 +60,51 @@ A few resources to get you started if this is your first Flutter project: For help getting started with Flutter, view our [online documentation](https://flutter.dev/docs), which offers tutorials, samples, guidance on mobile development, and a full API reference. + +##### NodeJS/Python binary for ARM + +https://github.com/dna2github/dna2oslab/releases/tag/0.2.0-android-gt6-arm + +##### Golang binary + +``` +# download go source package and extract +cd src +GOOS=android GOARCH=arm64 ./bootstrap.bash + +tar zcf go-android-arm64-bootstrap.tar.gz go-android-arm64-bootstrap +adb push go-android-arm64-bootstrap.tar.gz /sdcard/ +# we suggest write a javascript script to set up golang environment on your Android +# to extract tar package to NodeBase app zone /data/user/0/net.seven.nodebase/ +# e.g. /data/user/0/net.seven.nodebase/go-android-arm64-bootstrap +``` + +write a shell script `go` and `adb push go /sdcard` + +``` +#/system/bin/sh + +SELF=$(cd `dirname $0`; pwd) +BASE=/data/user/0/net.seven.nodebase/go-android-arm64-bootstrap +CACHEBASE=${BASE}/cache +mkdir -p ${CACHEBASE}/{cache,tmp,local} +export GOROOT=${BASE} +export GOPATH=${CACHEBASE}/golang/local +export GOCACHE=${CACHEBASE}/golang/cache +export GOTMPDIR=${CACHEBASE}/golang/tmp +export CGO_ENABLED=0 +exec ${BASE}/bin/go run $@ +``` + +create a new platform in NodeBase and download go wrapper from `file:///sdcard/go`; + +then write a tiny server to have a try. ref: https://github.com/stallpool/halfbase/blob/master/golang/tinyserver/main.go + +##### Notice + +currently NodeBase support kill a program with 1-level children, for example `go run main.go` will spawn a child process `main`; +if click on `stop` button, NodeBase can kill the `go run` and its child `main`. + +if remove `exec` in the `go` wrapper shell script, the shell script will run in `sh`, it spawn `go run` and the `go run` spawn `main`; +when `stop` the application, NodeBase will merely kill `sh` and its child `go run`; but `main` will still be running there, +which may cause next `start` failure (like port has already been used) and need to kill whole NodeBase for cleanup. From f908df350cc4b71d39c7772486e2307a75e45d09 Mon Sep 17 00:00:00 2001 From: Seven Lju Date: Mon, 12 Oct 2020 19:23:02 +0800 Subject: [PATCH 14/24] update README.md: add guidance for run dalvikvm java command --- README.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a0ce9aa..d204342 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,20 @@ samples, guidance on mobile development, and a full API reference. ##### NodeJS/Python binary for ARM -https://github.com/dna2github/dna2oslab/releases/tag/0.2.0-android-gt6-arm +ref: https://github.com/dna2github/dna2oslab/releases/tag/0.2.0-android-gt6-arm + +##### Java binary + +write a shell script `java` and `adb push java /sdcard` +``` +#!/system/bin/sh + +dalvikvm $@ +``` + +create a new platform in NodeBase and download java wrapper from `file:///sdcard/java` + +then write a command line tool to have a try. ref: https://github.com/dna2github/dna2sevord/tree/master/past/others/walkserver/javacmd ##### Golang binary @@ -82,7 +95,7 @@ adb push go-android-arm64-bootstrap.tar.gz /sdcard/ write a shell script `go` and `adb push go /sdcard` ``` -#/system/bin/sh +#!/system/bin/sh SELF=$(cd `dirname $0`; pwd) BASE=/data/user/0/net.seven.nodebase/go-android-arm64-bootstrap From c0df85613873c8c5371f86b83dcde90bd5eb5854 Mon Sep 17 00:00:00 2001 From: Seven Lju Date: Wed, 11 Nov 2020 22:33:40 +0800 Subject: [PATCH 15/24] update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d204342..414a123 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,8 @@ ref: https://github.com/stallpool/halfbase/tree/master/nodejs/tinyserver/index.j [...] hook `/index.html` to load `/app/static/index.html` ``` +App examples: [https://github.com/nodebase0](https://github.com/nodebase0), includes file-viewer-uploader, nodepad, ... + ## Development @@ -71,7 +73,7 @@ write a shell script `java` and `adb push java /sdcard` ``` #!/system/bin/sh -dalvikvm $@ +exec dalvikvm $@ ``` create a new platform in NodeBase and download java wrapper from `file:///sdcard/java` From 9a2875cd632d7bb7dcc2fd555e3288f2a4e8005d Mon Sep 17 00:00:00 2001 From: Seven Lju Date: Fri, 2 Apr 2021 23:46:09 +0800 Subject: [PATCH 16/24] minor: fix warnings --- .gitignore | 1 + android/app/src/main/kotlin/net/seven/nodebase/Download.kt | 6 ++++-- android/app/src/main/kotlin/net/seven/nodebase/External.kt | 2 +- .../app/src/main/kotlin/net/seven/nodebase/MainActivity.kt | 2 +- android/app/src/main/kotlin/net/seven/nodebase/Network.kt | 2 +- .../app/src/main/kotlin/net/seven/nodebase/NodeMonitor.kt | 1 - .../app/src/main/kotlin/net/seven/nodebase/NodeService.kt | 5 ++--- 7 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 9b5dec2..8a85989 100644 --- a/.gitignore +++ b/.gitignore @@ -44,4 +44,5 @@ app.*.map.json !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages android/gradle* +.gradle local diff --git a/android/app/src/main/kotlin/net/seven/nodebase/Download.kt b/android/app/src/main/kotlin/net/seven/nodebase/Download.kt index 6f1d65d..0caee69 100644 --- a/android/app/src/main/kotlin/net/seven/nodebase/Download.kt +++ b/android/app/src/main/kotlin/net/seven/nodebase/Download.kt @@ -1,5 +1,7 @@ package net.seven.nodebase +// TODO: deprecated, solution ref: +// https://stackoverflow.com/questions/45373007/progressdialog-is-deprecated-what-is-the-alternate-one-to-use import android.app.ProgressDialog import android.content.Context import android.os.AsyncTask @@ -50,10 +52,10 @@ class Download(private val context: Context, private val callback: Runnable?) { read_size += " / " + Storage.readableSize(file_len) } publishProgress(read_size) - read_len = download_stream!!.read(buf) + read_len = download_stream.read(buf) } output_stream.close() - download_stream!!.close() + download_stream.close() publishProgress("Finishing ...") } catch (e: MalformedURLException) { e.printStackTrace() diff --git a/android/app/src/main/kotlin/net/seven/nodebase/External.kt b/android/app/src/main/kotlin/net/seven/nodebase/External.kt index df831b0..4a45cff 100644 --- a/android/app/src/main/kotlin/net/seven/nodebase/External.kt +++ b/android/app/src/main/kotlin/net/seven/nodebase/External.kt @@ -22,7 +22,7 @@ object External { intent.type = "text/plain" } else { val f = File(imgFilePath) - if (f != null && f.exists() && f.isFile) { + if (f.exists() && f.isFile) { intent.type = "image/jpg" val u = Uri.fromFile(f) intent.putExtra(Intent.EXTRA_STREAM, u) diff --git a/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt b/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt index 8254c0f..8c76084 100644 --- a/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt +++ b/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt @@ -180,7 +180,7 @@ class MainActivity: FlutterActivity() { } private fun fetchAndMarkExecutable(src: String, dst: String): Int { - if (src == null) return -1 + if (src == "") return -1 if (src.startsWith("file://")) { Permission.request(this) var final_src = src diff --git a/android/app/src/main/kotlin/net/seven/nodebase/Network.kt b/android/app/src/main/kotlin/net/seven/nodebase/Network.kt index 3617cc5..e54a086 100644 --- a/android/app/src/main/kotlin/net/seven/nodebase/Network.kt +++ b/android/app/src/main/kotlin/net/seven/nodebase/Network.kt @@ -16,7 +16,7 @@ object Network { for (nic in Collections.list(NetworkInterface.getNetworkInterfaces())) { val nic_addr = nic.interfaceAddresses if (nic_addr.size == 0) continue - val ips = Array(nic_addr.size, { it -> "" }); + val ips = Array(nic_addr.size, { _ -> "" }); val name = nic.name var index = 0 for (ia in nic_addr) { diff --git a/android/app/src/main/kotlin/net/seven/nodebase/NodeMonitor.kt b/android/app/src/main/kotlin/net/seven/nodebase/NodeMonitor.kt index a791473..c176b62 100644 --- a/android/app/src/main/kotlin/net/seven/nodebase/NodeMonitor.kt +++ b/android/app/src/main/kotlin/net/seven/nodebase/NodeMonitor.kt @@ -66,7 +66,6 @@ class NodeMonitor(val serviceName: String, val command: Array) : Thread( fun pidService(): Int { val p = node_process!! - if (p == null) return -1 if (!p.isAlive()) return -1 val klass = p.javaClass if ("java.lang.UNIXProcess".equals(klass.getName())) { diff --git a/android/app/src/main/kotlin/net/seven/nodebase/NodeService.kt b/android/app/src/main/kotlin/net/seven/nodebase/NodeService.kt index dc606c7..950e459 100644 --- a/android/app/src/main/kotlin/net/seven/nodebase/NodeService.kt +++ b/android/app/src/main/kotlin/net/seven/nodebase/NodeService.kt @@ -65,8 +65,7 @@ class NodeService : Service() { } private fun stopNodeApps() { - val n = services.keys.size - val keys = arrayOfNulls(n) + // val n = services.keys.size for (name in services.keys.iterator()) { stopNodeApp(name.orEmpty()) } @@ -92,7 +91,7 @@ class NodeService : Service() { stopNodeApp(name) Log.d("NodeService:Command", String.format("%s", cmd)) val exec = StringUtils.parseArgv(cmd) - val monitor = NodeMonitor(name, exec!!) + val monitor = NodeMonitor(name, exec) services[name] = monitor monitor.start() } From 78ba6c933193b2b781b387e1bc62d73a7be6cfb1 Mon Sep 17 00:00:00 2001 From: Seven Lju Date: Fri, 14 May 2021 16:27:27 +0800 Subject: [PATCH 17/24] format with dart fmt command --- lib/api.dart | 54 +++---- lib/app_model.dart | 25 +-- lib/homepage.dart | 84 +++++----- lib/io.dart | 3 +- lib/page_app_home.dart | 327 ++++++++++++++++++++------------------ lib/page_app_webview.dart | 5 +- lib/page_apps.dart | 266 +++++++++++++++---------------- lib/page_environment.dart | 27 ++-- lib/page_platform.dart | 289 ++++++++++++++++----------------- lib/search.dart | 41 ++--- pubspec.lock | 40 ++--- 11 files changed, 593 insertions(+), 568 deletions(-) diff --git a/lib/api.dart b/lib/api.dart index 6a12f6e..dfab6bd 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -19,11 +19,13 @@ class NodeBaseApi { return batteryLevel; } - static Future requestExternalStoragePermission () async { - try { appApi.invokeMethod('RequestExternalStoragePermission'); } catch (e) {} + static Future requestExternalStoragePermission() async { + try { + appApi.invokeMethod('RequestExternalStoragePermission'); + } catch (e) {} } - static Future fetchExecutable (String url) async { + static Future fetchExecutable(String url) async { if (url == null || url == "") return null; final name = url.split("/").last; final dst = (await getAppFileReference('/bin/${name}')).path; @@ -31,17 +33,15 @@ class NodeBaseApi { if (url.indexOf("://") < 0) return dst; // e.g. file://.../node http://.../node https://.../node -> /path/to/app/bin/node try { - appApi.invokeMethod('FetchExecutable', { - "url": url, - "target": dst - }); + appApi.invokeMethod( + 'FetchExecutable', {"url": url, "target": dst}); return dst; - } catch(e) { + } catch (e) { return null; } } - static Future fetchWifiIpv4 () async { + static Future fetchWifiIpv4() async { try { return appApi.invokeMethod('FetchWifiIpv4'); } catch (e) { @@ -49,33 +49,26 @@ class NodeBaseApi { } } - static Future appStatus (String app) async { + static Future appStatus(String app) async { try { - return nodebaseApi.invokeMethod('GetStatus', { - "app": app - }); + return nodebaseApi + .invokeMethod('GetStatus', {"app": app}); } catch (e) { return "error"; } } - static Future appStart (String app, String cmd) async { + static Future appStart(String app, String cmd) async { try { - nodebaseApi.invokeMethod('Start', { - "app": app, - "cmd": cmd - }); - } catch (e) { - } + nodebaseApi + .invokeMethod('Start', {"app": app, "cmd": cmd}); + } catch (e) {} } - static Future appStop (String app) async { + static Future appStop(String app) async { try { - nodebaseApi.invokeMethod('Stop', { - "app": app - }); - } catch (e) { - } + nodebaseApi.invokeMethod('Stop', {"app": app}); + } catch (e) {} } static Future appUnpack(String app, String zipfile) async { @@ -86,8 +79,7 @@ class NodeBaseApi { "path": appBaseDir, "zipfile": zipfile }); - } catch (e) { - } + } catch (e) {} } static Future appPack(String app, String zipfile) async { @@ -98,8 +90,7 @@ class NodeBaseApi { "path": appBaseDir, "zipfile": zipfile }); - } catch (e) { - } + } catch (e) {} } static Future appBrowser(String url) async { @@ -107,7 +98,6 @@ class NodeBaseApi { nodebaseApi.invokeMethod('Browser', { "url": url, }); - } catch (e) { - } + } catch (e) {} } } diff --git a/lib/app_model.dart b/lib/app_model.dart index acfd61e..274cb8e 100644 --- a/lib/app_model.dart +++ b/lib/app_model.dart @@ -3,24 +3,27 @@ import 'package:flutter/material.dart'; class NodeBaseAppModule { NodeBaseAppModule({Key key, this.id, this.icon, this.name, this.desc}) {} - final int id; + final int id; final String name; final String desc; - final Icon icon; + final Icon icon; static final List list = [ NodeBaseAppModule( - id: 101, icon: Icon(Icons.settings), name: "Environment", - desc: "application configurations." - ), + id: 101, + icon: Icon(Icons.settings), + name: "Environment", + desc: "application configurations."), NodeBaseAppModule( - id: 102, icon: Icon(Icons.settings), name: "Platform", - desc: "platform management, like node, go, python, ..." - ), + id: 102, + icon: Icon(Icons.settings), + name: "Platform", + desc: "platform management, like node, go, python, ..."), NodeBaseAppModule( - id: 102, icon: Icon(Icons.apps), name: "Application", - desc: "application management, like running, developing, sharing, ..." - ) + id: 102, + icon: Icon(Icons.apps), + name: "Application", + desc: "application management, like running, developing, sharing, ...") ]; } diff --git a/lib/homepage.dart b/lib/homepage.dart index 8ddadd0..67e612a 100644 --- a/lib/homepage.dart +++ b/lib/homepage.dart @@ -7,7 +7,6 @@ import './page_environment.dart'; import './page_platform.dart'; import './page_apps.dart'; - class NodeBaseHomePage extends StatefulWidget { NodeBaseHomePage({Key key, this.title}) : super(key: key); @@ -18,13 +17,12 @@ class NodeBaseHomePage extends StatefulWidget { } class _NodeBaseHomePageState extends State { - @override void initState() { super.initState(); readAppFileAsString("/config.json").then((config) { if (config != "") { - onReady(jsonDecode(config)); + onReady(jsonDecode(config)); } }); } @@ -38,15 +36,24 @@ class _NodeBaseHomePageState extends State { onNavigate(NodeBaseAppModule module) { var route; switch (module.name) { - case "Environment": { - route = MaterialPageRoute(builder: (context) => NodeBaseEnvironmentSettings()); - } break; - case "Platform": { - route = MaterialPageRoute(builder: (context) => NodeBasePlatformSettings()); - } break; - case "Application": { - route = MaterialPageRoute(builder: (context) => NodeBaseApplications()); - } break; + case "Environment": + { + route = MaterialPageRoute( + builder: (context) => NodeBaseEnvironmentSettings()); + } + break; + case "Platform": + { + route = MaterialPageRoute( + builder: (context) => NodeBasePlatformSettings()); + } + break; + case "Application": + { + route = + MaterialPageRoute(builder: (context) => NodeBaseApplications()); + } + break; } Navigator.push(context, route); } @@ -54,33 +61,32 @@ class _NodeBaseHomePageState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: Text(widget.title), - actions: [ + appBar: AppBar(title: Text(widget.title), actions: [ IconButton( - onPressed: () { showSearch(context: context, delegate: NodeBaseSearch()); }, - icon: Icon(Icons.search) - ) - ] - ), - body: ListView.builder( - padding: const EdgeInsets.all(8), - itemCount: NodeBaseAppModule.list.length, - itemBuilder: (BuildContext context, int index) { - return Container( - child: Card( - child: ListTile( - onTap: () => onNavigate(NodeBaseAppModule.list[index]), - title: Text('${NodeBaseAppModule.list[index].name}'), - subtitle: Text('${NodeBaseAppModule.list[index].desc}'), - leading: IconButton( - icon: NodeBaseAppModule.list[index].icon, - ) // IconButton - ) // ListTile - ) // Card - ); - } - ) // body - ); + onPressed: () { + showSearch(context: context, delegate: NodeBaseSearch()); + }, + icon: Icon(Icons.search)) + ]), + body: ListView.builder( + padding: const EdgeInsets.all(8), + itemCount: NodeBaseAppModule.list.length, + itemBuilder: (BuildContext context, int index) { + return Container( + child: Card( + child: ListTile( + onTap: () => + onNavigate(NodeBaseAppModule.list[index]), + title: Text('${NodeBaseAppModule.list[index].name}'), + subtitle: + Text('${NodeBaseAppModule.list[index].desc}'), + leading: IconButton( + icon: NodeBaseAppModule.list[index].icon, + ) // IconButton + ) // ListTile + ) // Card + ); + }) // body + ); } } diff --git a/lib/io.dart b/lib/io.dart index 32cc6d5..f456e79 100644 --- a/lib/io.dart +++ b/lib/io.dart @@ -63,7 +63,8 @@ Future> ioLs(filepath) async { list.add(File(filename)); } else { final dir = Directory(filename); - final entities = await dir.list(recursive: false, followLinks: false).toList(); + final entities = + await dir.list(recursive: false, followLinks: false).toList(); entities.forEach((FileSystemEntity entity) { list.add(entity); }); diff --git a/lib/page_app_home.dart b/lib/page_app_home.dart index 8da6c1c..0068e83 100644 --- a/lib/page_app_home.dart +++ b/lib/page_app_home.dart @@ -8,13 +8,12 @@ import './page_app_webview.dart'; class NodeBaseAppHome extends StatefulWidget { final NodeBaseApp item; - NodeBaseAppHome({Key key, this.item}): super(key: key); + NodeBaseAppHome({Key key, this.item}) : super(key: key); @override _NodeBaseAppHomeState createState() => _NodeBaseAppHomeState(); } class _NodeBaseAppHomeState extends State { - bool loading = true; bool isRunning = false; String wifiIp = "0.0.0.0"; @@ -25,11 +24,15 @@ class _NodeBaseAppHomeState extends State { var eventSub = null; appStopped() { - setState(() { isRunning = false; }); + setState(() { + isRunning = false; + }); } appStarted() { - setState(() { isRunning = true; }); + setState(() { + isRunning = true; + }); } Future loadPlatform(String name) async { @@ -66,7 +69,7 @@ class _NodeBaseAppHomeState extends State { } @override - void initState () { + void initState() { super.initState(); NodeBaseApi.fetchWifiIpv4().then((ip) { setState(() { @@ -94,44 +97,50 @@ class _NodeBaseAppHomeState extends State { }); }); if (eventSub == null) { - eventSub = NodeBaseApi.eventApi.receiveBroadcastStream().listen( - (m) { - // \n - if (m == null) return; - final parts = m.split("\n"); - if (parts.length < 1) return; - final appname = parts[0]; - final appstat = parts[1]; - if (appname != widget.item.name) return; - switch (appstat) { - case "start": { + eventSub = NodeBaseApi.eventApi.receiveBroadcastStream().listen((m) { + // \n + if (m == null) return; + final parts = m.split("\n"); + if (parts.length < 1) return; + final appname = parts[0]; + final appstat = parts[1]; + if (appname != widget.item.name) return; + switch (appstat) { + case "start": + { appStarted(); - } break; - case "stop": { + } + break; + case "stop": + { appStopped(); - } break; - } - }, - onError: (err) {}, - cancelOnError: true - ); + } + break; + } + }, onError: (err) {}, cancelOnError: true); } NodeBaseApi.appStatus(widget.item.name).then((status) { - switch(status) { - case "started": { - appStarted(); - } break; + switch (status) { + case "started": + { + appStarted(); + } + break; case "stopped": - default: { - appStopped(); - } break; + default: + { + appStopped(); + } + break; } - setState(() { loading = false; }); + setState(() { + loading = false; + }); }); } @override - void dispose () { + void dispose() { ctrlParams.dispose(); ctrlDownload.dispose(); eventSub.cancel(); @@ -140,138 +149,150 @@ class _NodeBaseAppHomeState extends State { @override Widget build(BuildContext context) { - if (widget.item == null || widget.item.name == null || widget.item.name == "") { + if (widget.item == null || + widget.item.name == null || + widget.item.name == "") { Navigator.pop(context); return null; } if (loading) { return Scaffold( - body: Center( child: CircularProgressIndicator( - semanticsLabel: "Loading ..." - ) ) - ); + body: Center( + child: CircularProgressIndicator(semanticsLabel: "Loading ..."))); } return Scaffold( - appBar: AppBar( - title: Text('Application - ${widget.item.name}'), - leading: IconButton( - icon: Icon(Icons.arrow_back), - onPressed: () { Navigator.pop(context); } - ) - ), - body: ListView( - children: [ - const ListTile( - title: Text('Basic Info'), - dense: true - ), - ListTile( title: Text('Platform: ${widget.item.platform}') ), - ListTile( title: SelectableText( - appHomeUrl == ""?'Network: ${wifiIp}':'Home: ${appHomeUrl}', - maxLines: 1 - ) ), - ListTile( title: SelectableText( - 'Location: ${appHomePath}', - maxLines: 1 - ) ), - ListTile( title: TextField( - controller: ctrlParams, - decoration: InputDecoration( labelText: 'Params' ) - ) ), - ListTile( title: Row( - children: [ - IconButton( + appBar: AppBar( + title: Text('Application - ${widget.item.name}'), + leading: IconButton( + icon: Icon(Icons.arrow_back), + onPressed: () { + Navigator.pop(context); + })), + body: ListView(children: [ + const ListTile(title: Text('Basic Info'), dense: true), + ListTile(title: Text('Platform: ${widget.item.platform}')), + ListTile( + title: SelectableText( + appHomeUrl == "" + ? 'Network: ${wifiIp}' + : 'Home: ${appHomeUrl}', + maxLines: 1)), + ListTile( + title: SelectableText('Location: ${appHomePath}', maxLines: 1)), + ListTile( + title: TextField( + controller: ctrlParams, + decoration: InputDecoration(labelText: 'Params'))), + ListTile( + title: Row(children: [ + IconButton( icon: Icon(Icons.play_arrow), - onPressed: isRunning ? null : () { - setState(() { loading = true; }); - loadPlatform(widget.item.platform).then((p) { - if (p == null || p.path == null || p.path == "") { - setState(() { loading = false; }); - return; - } - loadAppDetails(widget.item.name).then((info) { - if (info == null) { - // no config.json - return; - } - final entry = info.entry == null?"":info.entry; - final cmd = "${p.path} ${info.path}/${entry} ${ctrlParams.text}"; - NodeBaseApi.appStart(widget.item.name, cmd); - setState(() { loading = false; }); - }); - }); - } - ), - SizedBox( width: 15 ), - IconButton( + onPressed: isRunning + ? null + : () { + setState(() { + loading = true; + }); + loadPlatform(widget.item.platform).then((p) { + if (p == null || p.path == null || p.path == "") { + setState(() { + loading = false; + }); + return; + } + loadAppDetails(widget.item.name).then((info) { + if (info == null) { + // no config.json + return; + } + final entry = info.entry == null ? "" : info.entry; + final cmd = + "${p.path} ${info.path}/${entry} ${ctrlParams.text}"; + NodeBaseApi.appStart(widget.item.name, cmd); + setState(() { + loading = false; + }); + }); + }); + }), + SizedBox(width: 15), + IconButton( icon: Icon(Icons.stop), - onPressed: isRunning ? () { - NodeBaseApi.appStop(widget.item.name); - } : null - ), - IconButton( + onPressed: isRunning + ? () { + NodeBaseApi.appStop(widget.item.name); + } + : null), + IconButton( icon: Icon(Icons.open_in_browser), - onPressed: isRunning ? () { - // open webview - setState(() { loading = true; }); - loadAppDetails(widget.item.name).then((info) { - if (info == null) { - // no config.json - return; - } - setState(() { loading = false; }); - var homeUrl = info.host; - if (info.port > 0) homeUrl += ":${info.port}"; - homeUrl += info.home; - Navigator.push(context, MaterialPageRoute( - builder: (context) => NodeBaseAppWebview( - name: widget.item.name, - home: homeUrl - ) - ) ); - }); - } : null - ), - IconButton( + onPressed: isRunning + ? () { + // open webview + setState(() { + loading = true; + }); + loadAppDetails(widget.item.name).then((info) { + if (info == null) { + // no config.json + return; + } + setState(() { + loading = false; + }); + var homeUrl = info.host; + if (info.port > 0) homeUrl += ":${info.port}"; + homeUrl += info.home; + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => NodeBaseAppWebview( + name: widget.item.name, home: homeUrl))); + }); + } + : null), + IconButton( icon: Icon(Icons.open_in_new), - onPressed: (appHomeUrl != "" && isRunning) ? () { - NodeBaseApi.appBrowser(appHomeUrl); - } : null - ) - ] - ) ), // Row, ListTile + onPressed: (appHomeUrl != "" && isRunning) + ? () { + NodeBaseApi.appBrowser(appHomeUrl); + } + : null) + ])), // Row, ListTile const Divider(), - const ListTile( - title: Text('Import/Export'), - dense: true - ), + const ListTile(title: Text('Import/Export'), dense: true), ListTile( - leading: IconButton( - icon: Icon(Icons.file_upload), - onPressed: () { - // TODO: if url, download zip to tmp folder and unpack - setState(() { loading = true; }); - NodeBaseApi.appUnpack(widget.item.name, ctrlDownload.text).then((ok) { - setState(() { loading = false; }); - }); - } - ), - trailing: IconButton( - icon: Icon(Icons.file_download), - onPressed: () { - setState(() { loading = true; }); - NodeBaseApi.appPack(widget.item.name, ctrlDownload.text).then((ok) { - setState(() { loading = false; }); - }); - } - ), - title: TextField( - controller: ctrlDownload, - decoration: InputDecoration( labelText: 'ZIP file path' ) - ) - ), // Row, ListTile - ] - ) // ListView - ); + leading: IconButton( + icon: Icon(Icons.file_upload), + onPressed: () { + // TODO: if url, download zip to tmp folder and unpack + setState(() { + loading = true; + }); + NodeBaseApi.appUnpack(widget.item.name, ctrlDownload.text) + .then((ok) { + setState(() { + loading = false; + }); + }); + }), + trailing: IconButton( + icon: Icon(Icons.file_download), + onPressed: () { + setState(() { + loading = true; + }); + NodeBaseApi.appPack(widget.item.name, ctrlDownload.text) + .then((ok) { + setState(() { + loading = false; + }); + }); + }), + title: TextField( + controller: ctrlDownload, + decoration: InputDecoration( + labelText: 'ZIP file path'))), // Row, ListTile + ]) // ListView + ); } } diff --git a/lib/page_app_webview.dart b/lib/page_app_webview.dart index 3a2b53c..527d0d0 100644 --- a/lib/page_app_webview.dart +++ b/lib/page_app_webview.dart @@ -7,7 +7,7 @@ class NodeBaseAppWebview extends StatefulWidget { String name; String home; - NodeBaseAppWebview({Key key, this.name, this.home}): super(key: key); + NodeBaseAppWebview({Key key, this.name, this.home}) : super(key: key); @override _NodeBaseAppWebviewState createState() => _NodeBaseAppWebviewState(); @@ -237,7 +237,8 @@ class SampleMenu extends StatelessWidget { WebViewController controller, BuildContext context) async { //final String contentBase64 = // base64Encode(const Utf8Encoder().convert(kNavigationExamplePage)); - String contentBase64 = base64Encode(const Utf8Encoder().convert('')); + String contentBase64 = base64Encode( + const Utf8Encoder().convert('')); //String contentBase64 = ''; await controller.loadUrl('data:text/html;base64,$contentBase64'); } diff --git a/lib/page_apps.dart b/lib/page_apps.dart index 601cfe9..8614e05 100644 --- a/lib/page_apps.dart +++ b/lib/page_apps.dart @@ -12,13 +12,14 @@ class NodeBaseAppItem extends StatefulWidget { bool isEdit = false; bool isCreated = false; - NodeBaseAppItem({Key key, this.item, this.fnRemove, this.fnSaveConfig}): super(key: key); + NodeBaseAppItem({Key key, this.item, this.fnRemove, this.fnSaveConfig}) + : super(key: key); @override _NodeBaseAppItemState createState() => _NodeBaseAppItemState(); } -class _NodeBaseAppItemState extends State { +class _NodeBaseAppItemState extends State { final ctrlName = TextEditingController(); final ctrlPlatform = TextEditingController(); bool _initialized = false; @@ -40,118 +41,114 @@ class _NodeBaseAppItemState extends State { } final entities = [ ListTile( - leading: Icon(Icons.bookmark), - title: TextField( - controller: ctrlName, - decoration: InputDecoration( labelText: 'Name' ) - ) - ), + leading: Icon(Icons.bookmark), + title: TextField( + controller: ctrlName, + decoration: InputDecoration(labelText: 'Name'))), ListTile( - leading: Icon(Icons.cloud_queue), - title: TextField( - controller: ctrlPlatform, - decoration: InputDecoration( labelText: 'Platform' ) - ) - ) // ListTile + leading: Icon(Icons.cloud_queue), + title: TextField( + controller: ctrlPlatform, + decoration: InputDecoration(labelText: 'Platform'))) // ListTile ]; if (widget.item.path != null && widget.item.path != "") { entities.add(ListTile( - leading: SizedBox(width: 5), - title: Text(widget.item.path) - )); + leading: SizedBox(width: 5), title: Text(widget.item.path))); } - entities.add( - Row( - children: [ - FlatButton.icon( - icon: Icon(Icons.check), - label: Text("Save"), - onPressed: () { - if (ctrlName.text == "") return; - if (widget.item.name != ctrlName.text) { - ioMoveApp(widget.item.name, ctrlName.text); - } + entities.add(Row(children: [ + FlatButton.icon( + icon: Icon(Icons.check), + label: Text("Save"), + onPressed: () { + if (ctrlName.text == "") return; + if (widget.item.name != ctrlName.text) { + ioMoveApp(widget.item.name, ctrlName.text); + } + setState(() { + widget.item.name = ctrlName.text; + widget.item.platform = ctrlPlatform.text; + widget.isCreated = false; + widget.isEdit = false; + widget.fnSaveConfig(); + }); + }), + FlatButton.icon( + icon: Icon(Icons.close), + label: Text("Cancel"), + onPressed: () { + if (widget.isCreated) { + widget.fnRemove(widget); + } else { setState(() { - widget.item.name = ctrlName.text; - widget.item.platform = ctrlPlatform.text; - widget.isCreated = false; widget.isEdit = false; - widget.fnSaveConfig(); }); } - ), - FlatButton.icon( - icon: Icon(Icons.close), - label: Text("Cancel"), - onPressed: () { - if (widget.isCreated) { - widget.fnRemove(widget); - } else { - setState(() { widget.isEdit = false; }); - } - } - ) - ] - ) // Row - ); - return Card( - child: Column( - children: entities - ) - ); + }) + ]) // Row + ); + return Card(child: Column(children: entities)); } - var name = widget.item.name == null?"":widget.item.name; - var platform = widget.item.platform == null?"":widget.item.platform; + var name = widget.item.name == null ? "" : widget.item.name; + var platform = widget.item.platform == null ? "" : widget.item.platform; return Card( - child: ListTile( - title: Text(name), - subtitle: Text(platform), - trailing: PopupMenuButton( - icon: Icon(Icons.more_vert), - onSelected: (int result) { - switch(result) { - case 101: { - setState(() { widget.isEdit = true; }); - } break; - case 102: { - ioRemoveApp(widget.item.name); - widget.fnRemove(widget); - widget.fnSaveConfig(); - } break; - } - }, - itemBuilder: (BuildContext context) => >[ - const PopupMenuItem( value: 101, child: Text('Edit') ), - const PopupMenuItem( value: 102, child: Text('Delete') ) - ] - ), // PopupMenuButton - onTap: () { - Navigator.push(context, MaterialPageRoute( - builder: (context) => NodeBaseAppHome(item: widget.item)) - ); - } - ) - ); + child: ListTile( + title: Text(name), + subtitle: Text(platform), + trailing: PopupMenuButton( + icon: Icon(Icons.more_vert), + onSelected: (int result) { + switch (result) { + case 101: + { + setState(() { + widget.isEdit = true; + }); + } + break; + case 102: + { + ioRemoveApp(widget.item.name); + widget.fnRemove(widget); + widget.fnSaveConfig(); + } + break; + } + }, + itemBuilder: (BuildContext context) => >[ + const PopupMenuItem(value: 101, child: Text('Edit')), + const PopupMenuItem( + value: 102, child: Text('Delete')) + ]), // PopupMenuButton + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + NodeBaseAppHome(item: widget.item))); + })); } } class NodeBaseApplications extends StatefulWidget { - NodeBaseApplications({Key key}): super(key: key); + NodeBaseApplications({Key key}) : super(key: key); @override _NodeBaseApplicationsState createState() => _NodeBaseApplicationsState(); } + class _NodeBaseApplicationsState extends State { List entities = []; var loading = true; @override - void initState () { + void initState() { super.initState(); loadConfig(); } loadConfig() async { - setState(() { loading = true; }); + setState(() { + loading = true; + }); var config = await readAppFileAsString("/apps.json"); final List list = []; if (config != "") { @@ -172,20 +169,24 @@ class _NodeBaseApplicationsState extends State { } saveConfig() async { - setState(() { loading = true; }); - await writeAppFileAsString("/apps.json", JsonEncoder((x) { - if (x is NodeBaseAppItem) { - return { - "name": x.item.name, - "path": x.item.path, - "platform": x.item.platform - }; - } - return null; - }).convert({ - "apps": entities - })); - setState(() { loading = false; }); + setState(() { + loading = true; + }); + await writeAppFileAsString( + "/apps.json", + JsonEncoder((x) { + if (x is NodeBaseAppItem) { + return { + "name": x.item.name, + "path": x.item.path, + "platform": x.item.platform + }; + } + return null; + }).convert({"apps": entities})); + setState(() { + loading = false; + }); } removeItem(NodeBaseAppItem item) { @@ -197,47 +198,44 @@ class _NodeBaseApplicationsState extends State { } makeItem(NodeBaseApp item) { - return NodeBaseAppItem(item: item, fnRemove: removeItem, fnSaveConfig: saveConfig); + return NodeBaseAppItem( + item: item, fnRemove: removeItem, fnSaveConfig: saveConfig); } @override Widget build(BuildContext context) { if (loading) { return Scaffold( - body: Center( child: CircularProgressIndicator( - semanticsLabel: "Loading ..." - ) ) - ); + body: Center( + child: CircularProgressIndicator(semanticsLabel: "Loading ..."))); } return Scaffold( - appBar: AppBar( - title: Text('Applications'), - leading: IconButton( - icon: Icon(Icons.arrow_back), - onPressed: () { Navigator.pop(context); } - ) - ), - body: (entities.length == 0) - ? Center( child: Text('No application.') ) - : ListView.builder( - padding: const EdgeInsets.all(8), - itemCount: entities.length, - itemBuilder: (BuildContext context, int index) { - return Container( child: entities[index] ); - }), // body - floatingActionButton: FloatingActionButton( - tooltip: 'Add Application', - child: Icon(Icons.add), - onPressed: () { - final item = NodeBaseApp(name: ""); - final entity = makeItem(item); - setState(() { - entity.isEdit = true; - entity.isCreated = true; - entities.add(entity); - }); - } - ) - ); + appBar: AppBar( + title: Text('Applications'), + leading: IconButton( + icon: Icon(Icons.arrow_back), + onPressed: () { + Navigator.pop(context); + })), + body: (entities.length == 0) + ? Center(child: Text('No application.')) + : ListView.builder( + padding: const EdgeInsets.all(8), + itemCount: entities.length, + itemBuilder: (BuildContext context, int index) { + return Container(child: entities[index]); + }), // body + floatingActionButton: FloatingActionButton( + tooltip: 'Add Application', + child: Icon(Icons.add), + onPressed: () { + final item = NodeBaseApp(name: ""); + final entity = makeItem(item); + setState(() { + entity.isEdit = true; + entity.isCreated = true; + entities.add(entity); + }); + })); } } diff --git a/lib/page_environment.dart b/lib/page_environment.dart index c0c8a3d..08a9d36 100644 --- a/lib/page_environment.dart +++ b/lib/page_environment.dart @@ -2,16 +2,18 @@ import 'package:flutter/material.dart'; import './api.dart'; class NodeBaseEnvironmentSettings extends StatefulWidget { - NodeBaseEnvironmentSettings({Key key}): super(key: key); + NodeBaseEnvironmentSettings({Key key}) : super(key: key); @override - _NodeBaseEnvironmentSettingsState createState() => _NodeBaseEnvironmentSettingsState(); + _NodeBaseEnvironmentSettingsState createState() => + _NodeBaseEnvironmentSettingsState(); } -class _NodeBaseEnvironmentSettingsState extends State { +class _NodeBaseEnvironmentSettingsState + extends State { String _batteryLevel = 'Unknown'; @override - void initState () { + void initState() { _getBatteryLevel(); } @@ -25,14 +27,13 @@ class _NodeBaseEnvironmentSettingsState extends State _NodeBasePlatformItemState(); } -class _NodeBasePlatformItemState extends State { +class _NodeBasePlatformItemState extends State { final ctrlName = TextEditingController(); final ctrlDownloadUrl = TextEditingController(); bool _initialized = false; @@ -39,128 +40,127 @@ class _NodeBasePlatformItemState extends State { } final entities = [ ListTile( - leading: Icon(Icons.call_to_action), - title: TextField( - controller: ctrlName, - decoration: InputDecoration( labelText: 'Name' ) - ) - ), + leading: Icon(Icons.call_to_action), + title: TextField( + controller: ctrlName, + decoration: InputDecoration(labelText: 'Name'))), ListTile( - leading: Icon(Icons.attachment), - title: TextField( - controller: ctrlDownloadUrl, - decoration: InputDecoration( labelText: 'Download URL' ) - ), - trailing: IconButton( - icon: Icon(Icons.file_download), - onPressed: () { - if (ctrlDownloadUrl.text == "") return; - NodeBaseApi.fetchExecutable(ctrlDownloadUrl.text).then((dst) { - setState(() { - widget.item.path = dst; - if (!widget.isCreated) widget.fnSaveConfig(); - }); - }); - } - ) // trailing - ) // ListTile + leading: Icon(Icons.attachment), + title: TextField( + controller: ctrlDownloadUrl, + decoration: InputDecoration(labelText: 'Download URL')), + trailing: IconButton( + icon: Icon(Icons.file_download), + onPressed: () { + if (ctrlDownloadUrl.text == "") return; + NodeBaseApi.fetchExecutable(ctrlDownloadUrl.text).then((dst) { + setState(() { + widget.item.path = dst; + if (!widget.isCreated) widget.fnSaveConfig(); + }); + }); + }) // trailing + ) // ListTile ]; if (widget.item.path != null && widget.item.path != "") { entities.add(ListTile( - leading: SizedBox(width: 5), - title: Text(widget.item.path) - )); + leading: SizedBox(width: 5), title: Text(widget.item.path))); } - entities.add( - Row( - children: [ - FlatButton.icon( - icon: Icon(Icons.check), - label: Text("Save"), - onPressed: () { - if (ctrlName.text == "") return; + entities.add(Row(children: [ + FlatButton.icon( + icon: Icon(Icons.check), + label: Text("Save"), + onPressed: () { + if (ctrlName.text == "") return; + setState(() { + widget.item.name = ctrlName.text; + widget.item.updateUrl = ctrlDownloadUrl.text; + widget.isCreated = false; + widget.isEdit = false; + widget.fnSaveConfig(); + }); + }), + FlatButton.icon( + icon: Icon(Icons.close), + label: Text("Cancel"), + onPressed: () { + if (widget.isCreated) { + widget.fnRemove(widget); + } else { setState(() { - widget.item.name = ctrlName.text; - widget.item.updateUrl = ctrlDownloadUrl.text; - widget.isCreated = false; widget.isEdit = false; - widget.fnSaveConfig(); }); } - ), - FlatButton.icon( - icon: Icon(Icons.close), - label: Text("Cancel"), - onPressed: () { - if (widget.isCreated) { - widget.fnRemove(widget); - } else { - setState(() { widget.isEdit = false; }); - } - } - ) - ] - ) // Row - ); - return Card( - child: Column( - children: entities - ) // ListView - ); + }) + ]) // Row + ); + return Card(child: Column(children: entities) // ListView + ); } - var name = widget.item.name == null?"":widget.item.name; - var path = widget.item.path == null?"":widget.item.path; + var name = widget.item.name == null ? "" : widget.item.name; + var path = widget.item.path == null ? "" : widget.item.path; if (path == "") { - var url = widget.item.updateUrl == null?"":widget.item.updateUrl; - if (url != "") path = "Remotely available @ ${url}"; - else path = "Not yet configured."; + var url = widget.item.updateUrl == null ? "" : widget.item.updateUrl; + if (url != "") + path = "Remotely available @ ${url}"; + else + path = "Not yet configured."; } return Card( - child: ListTile( - title: Text(name), - subtitle: Text(path), - trailing: PopupMenuButton( - icon: Icon(Icons.more_vert), - onSelected: (int result) { - switch(result) { - case 101: { - setState(() { widget.isEdit = true; }); - } break; - case 102: { - // TODO: if we remove this item, do we need also remove the file at - // widget.item.path? - widget.fnRemove(widget); - widget.fnSaveConfig(); - } break; - } - }, - itemBuilder: (BuildContext context) => >[ - const PopupMenuItem( value: 101, child: Text('Edit') ), - const PopupMenuItem( value: 102, child: Text('Delete') ) - ] - ) // PopupMenuButton - ) - ); + child: ListTile( + title: Text(name), + subtitle: Text(path), + trailing: PopupMenuButton( + icon: Icon(Icons.more_vert), + onSelected: (int result) { + switch (result) { + case 101: + { + setState(() { + widget.isEdit = true; + }); + } + break; + case 102: + { + // TODO: if we remove this item, do we need also remove the file at + // widget.item.path? + widget.fnRemove(widget); + widget.fnSaveConfig(); + } + break; + } + }, + itemBuilder: (BuildContext context) => >[ + const PopupMenuItem(value: 101, child: Text('Edit')), + const PopupMenuItem( + value: 102, child: Text('Delete')) + ]) // PopupMenuButton + )); } } class NodeBasePlatformSettings extends StatefulWidget { - NodeBasePlatformSettings({Key key}): super(key: key); + NodeBasePlatformSettings({Key key}) : super(key: key); @override - _NodeBasePlatformSettingsState createState() => _NodeBasePlatformSettingsState(); + _NodeBasePlatformSettingsState createState() => + _NodeBasePlatformSettingsState(); } + class _NodeBasePlatformSettingsState extends State { List entities = []; var loading = true; @override - void initState () { + void initState() { super.initState(); loadConfig(); } loadConfig() async { - setState(() { loading = true; }); + setState(() { + loading = true; + }); var config = await readAppFileAsString("/platform.json"); final List list = []; if (config != "") { @@ -181,20 +181,24 @@ class _NodeBasePlatformSettingsState extends State { } saveConfig() async { - setState(() { loading = true; }); - await writeAppFileAsString("/platform.json", JsonEncoder((x) { - if (x is NodeBasePlatformItem) { - return { - "name": x.item.name, - "path": x.item.path, - "url": x.item.updateUrl - }; - } - return null; - }).convert({ - "platforms": entities - })); - setState(() { loading = false; }); + setState(() { + loading = true; + }); + await writeAppFileAsString( + "/platform.json", + JsonEncoder((x) { + if (x is NodeBasePlatformItem) { + return { + "name": x.item.name, + "path": x.item.path, + "url": x.item.updateUrl + }; + } + return null; + }).convert({"platforms": entities})); + setState(() { + loading = false; + }); } removeItem(NodeBasePlatformItem item) { @@ -206,47 +210,46 @@ class _NodeBasePlatformSettingsState extends State { } makeItem(NodeBasePlatform item) { - return NodeBasePlatformItem(item: item, fnRemove: removeItem, fnSaveConfig: saveConfig); + return NodeBasePlatformItem( + item: item, fnRemove: removeItem, fnSaveConfig: saveConfig); } @override Widget build(BuildContext context) { if (loading) { return Scaffold( - body: Center( child: CircularProgressIndicator( - semanticsLabel: "Loading ..." - ) ) - ); + body: Center( + child: CircularProgressIndicator(semanticsLabel: "Loading ..."))); } return Scaffold( - appBar: AppBar( - title: Text('Platform Settings'), - leading: IconButton( - icon: Icon(Icons.arrow_back), - onPressed: () { Navigator.pop(context); } - ) - ), - body: (entities.length == 0) - ? Center( child: Text('No platform item.') ) - : ListView.builder( - padding: const EdgeInsets.all(8), - itemCount: entities.length, - itemBuilder: (BuildContext context, int index) { - return Container( child: entities[index] ); - }), // body - floatingActionButton: FloatingActionButton( - tooltip: 'Add Platform', - child: Icon(Icons.add), - onPressed: () { - final item = NodeBasePlatform(name: ""); - final entity = makeItem(item); - setState(() { - entity.isEdit = true; - entity.isCreated = true; - entities.add(entity); - }); - } - ) - ); + appBar: AppBar( + title: Text('Platform Settings'), + leading: IconButton( + icon: Icon(Icons.arrow_back), + onPressed: () { + Navigator.pop(context); + }), + actions: [], + ), + body: (entities.length == 0) + ? Center(child: Text('No platform item.')) + : ListView.builder( + padding: const EdgeInsets.all(8), + itemCount: entities.length, + itemBuilder: (BuildContext context, int index) { + return Container(child: entities[index]); + }), // body + floatingActionButton: FloatingActionButton( + tooltip: 'Add Platform', + child: Icon(Icons.add), + onPressed: () { + final item = NodeBasePlatform(name: ""); + final entity = makeItem(item); + setState(() { + entity.isEdit = true; + entity.isCreated = true; + entities.add(entity); + }); + })); } } diff --git a/lib/search.dart b/lib/search.dart index e767054..2e96473 100644 --- a/lib/search.dart +++ b/lib/search.dart @@ -4,28 +4,30 @@ class NodeBaseSearch extends SearchDelegate { @override List buildActions(BuildContext context) { return [ - IconButton( icon: Icon(Icons.close), onPressed: () { - if (query == "") { - Navigator.pop(context); - } else { - query = ""; - } - } ) + IconButton( + icon: Icon(Icons.close), + onPressed: () { + if (query == "") { + Navigator.pop(context); + } else { + query = ""; + } + }) ]; } @override Widget buildLeading(BuildContext context) { - return IconButton( icon: Icon(Icons.arrow_back), onPressed: () { Navigator.pop(context); } ); + return IconButton( + icon: Icon(Icons.arrow_back), + onPressed: () { + Navigator.pop(context); + }); } @override Widget buildResults(BuildContext context) { - return Container( - child: Center( - child: Text("hello") - ) - ); + return Container(child: Center(child: Text("hello"))); } @override @@ -33,13 +35,12 @@ class NodeBaseSearch extends SearchDelegate { List candidateList = ["a", "b", "c"]; List suggestionList = []; query.isEmpty - ? suggestionList = candidateList - : suggestionList.addAll(candidateList.where((x) => x.contains(query))); + ? suggestionList = candidateList + : suggestionList.addAll(candidateList.where((x) => x.contains(query))); return ListView.builder( - itemCount: suggestionList.length, - itemBuilder: (context, index) { - return ListTile( title: Text(suggestionList[index]), onTap: () { } ); - } - ); + itemCount: suggestionList.length, + itemBuilder: (context, index) { + return ListTile(title: Text(suggestionList[index]), onTap: () {}); + }); } } diff --git a/pubspec.lock b/pubspec.lock index 9632367..cdd55bb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,42 +7,42 @@ packages: name: async url: "https://pub.flutter-io.cn" source: hosted - version: "2.4.2" + version: "2.5.0" boolean_selector: dependency: transitive description: name: boolean_selector url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.0" + version: "2.1.0" characters: dependency: transitive description: name: characters url: "https://pub.flutter-io.cn" source: hosted - version: "1.0.0" + version: "1.1.0" charcode: dependency: transitive description: name: charcode url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.3" + version: "1.2.0" clock: dependency: transitive description: name: clock url: "https://pub.flutter-io.cn" source: hosted - version: "1.0.1" + version: "1.1.0" collection: dependency: transitive description: name: collection url: "https://pub.flutter-io.cn" source: hosted - version: "1.14.13" + version: "1.15.0" cupertino_icons: dependency: "direct main" description: @@ -56,7 +56,7 @@ packages: name: fake_async url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.0" + version: "1.2.0" file: dependency: transitive description: @@ -87,21 +87,21 @@ packages: name: matcher url: "https://pub.flutter-io.cn" source: hosted - version: "0.12.8" + version: "0.12.10" meta: dependency: transitive description: name: meta url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.8" + version: "1.3.0" path: dependency: transitive description: name: path url: "https://pub.flutter-io.cn" source: hosted - version: "1.7.0" + version: "1.8.0" path_provider: dependency: "direct main" description: @@ -162,56 +162,56 @@ packages: name: source_span url: "https://pub.flutter-io.cn" source: hosted - version: "1.7.0" + version: "1.8.0" stack_trace: dependency: transitive description: name: stack_trace url: "https://pub.flutter-io.cn" source: hosted - version: "1.9.5" + version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.0" + version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner url: "https://pub.flutter-io.cn" source: hosted - version: "1.0.5" + version: "1.1.0" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.0" + version: "1.2.0" test_api: dependency: transitive description: name: test_api url: "https://pub.flutter-io.cn" source: hosted - version: "0.2.17" + version: "0.2.19" typed_data: dependency: transitive description: name: typed_data url: "https://pub.flutter-io.cn" source: hosted - version: "1.2.0" + version: "1.3.0" vector_math: dependency: transitive description: name: vector_math url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.8" + version: "2.1.0" webview_flutter: dependency: "direct main" description: @@ -227,5 +227,5 @@ packages: source: hosted version: "0.1.0" sdks: - dart: ">=2.9.0-14.0.dev <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + dart: ">=2.12.0-0.0 <3.0.0" + flutter: ">=1.12.13+hotfix.5" From 725cb8359be66c5ffbf543764abdc7b4e8f0a43d Mon Sep 17 00:00:00 2001 From: Seven Lju Date: Fri, 14 May 2021 22:42:01 +0800 Subject: [PATCH 18/24] init platform market - add a button to platform market in platform page - list available platform for different arch --- lib/page_platform.dart | 14 +++- lib/page_platform_market.dart | 136 ++++++++++++++++++++++++++++++++++ pubspec.lock | 30 +++++++- pubspec.yaml | 2 + 4 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 lib/page_platform_market.dart diff --git a/lib/page_platform.dart b/lib/page_platform.dart index 526562b..852aedd 100644 --- a/lib/page_platform.dart +++ b/lib/page_platform.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import './io.dart'; import './app_model.dart'; import './api.dart'; +import './page_platform_market.dart'; class NodeBasePlatformItem extends StatefulWidget { final Function(NodeBasePlatformItem item) fnRemove; @@ -67,7 +68,7 @@ class _NodeBasePlatformItemState extends State { leading: SizedBox(width: 5), title: Text(widget.item.path))); } entities.add(Row(children: [ - FlatButton.icon( + TextButton.icon( icon: Icon(Icons.check), label: Text("Save"), onPressed: () { @@ -80,7 +81,7 @@ class _NodeBasePlatformItemState extends State { widget.fnSaveConfig(); }); }), - FlatButton.icon( + TextButton.icon( icon: Icon(Icons.close), label: Text("Cancel"), onPressed: () { @@ -229,7 +230,14 @@ class _NodeBasePlatformSettingsState extends State { onPressed: () { Navigator.pop(context); }), - actions: [], + actions: [ + IconButton( + icon: Icon(Icons.add_shopping_cart), + onPressed: () { + var route = MaterialPageRoute(builder: (context) => NodeBasePlatformMarket()); + Navigator.push(context, route); + }) + ], ), body: (entities.length == 0) ? Center(child: Text('No platform item.')) diff --git a/lib/page_platform_market.dart b/lib/page_platform_market.dart new file mode 100644 index 0000000..ad5ef49 --- /dev/null +++ b/lib/page_platform_market.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import './api.dart'; + +String BASE_HOST = 'raw.githubusercontent.com'; +String BASE_URL = '/wiki/dna2github/NodeBase'; + +class PlatformItem { + String arch; + String name; + bool zip; +} + +class NodeBasePlatformMarketItem extends StatefulWidget { + final Function(PlatformItem item) fnInstall; + final Function(PlatformItem item) fnUninstall; + final PlatformItem item; + + NodeBasePlatformMarketItem( + {Key key, this.item, this.fnInstall, this.fnUninstall}) + : super(key: key); + + @override + _NodeBasePlatformMarketItemState createState() => + _NodeBasePlatformMarketItemState(); +} + +class _NodeBasePlatformMarketItemState + extends State { + @override + Widget build(BuildContext context) { + return Card( + child: ListTile( + title: Text(widget.item.name), + subtitle: Text(widget.item.arch), + trailing: PopupMenuButton( + icon: Icon(Icons.more_vert), + onSelected: (int result) { + switch (result) { + case 101: + { + widget.fnInstall(widget.item); + } + break; + } + }, + itemBuilder: (BuildContext context) => >[ + const PopupMenuItem( + value: 101, child: Text('Install')), + ]) // PopupMenuButton + )); + } +} + +class NodeBasePlatformMarket extends StatefulWidget { + NodeBasePlatformMarket({Key key}) : super(key: key); + @override + _NodeBasePlatformMarketState createState() => _NodeBasePlatformMarketState(); +} + +class _NodeBasePlatformMarketState extends State { + List entities = []; + bool loading = true; + + @override + void initState() { + _showPlatformItems(); + } + + Future fetchPlatformList() async { + return http + .get(Uri.https(BASE_HOST, BASE_URL + '/quick/platform/meta.list')); + } + + Future downloadFile(String uri) async { + return http.get(Uri.https(BASE_HOST, BASE_URL + uri)); + } + + Future> _getPlatformItems() async { + final res = await fetchPlatformList(); + List r = []; + for (String line in res.body.split("\n")) { + if (line.length == 0) continue; + final parts = line.split(" "); + // arch name zip? + final one = PlatformItem(); + one.arch = parts[0]; + one.name = parts[1]; + if (parts.length == 3 && parts[2] == "zip") { + one.zip = true; + } + r.add(one); + } + return r; + } + + installPlatformItem (PlatformItem item) {} + + uninstallPlatformItem (PlatformItem item) {} + + Future _showPlatformItems() async { + final items = await _getPlatformItems(); + final List list = []; + for (PlatformItem item in items) { + final tile = NodeBasePlatformMarketItem( + item: item, + fnInstall: installPlatformItem, + fnUninstall: uninstallPlatformItem, + ); + list.add(tile); + } + setState(() { + entities.clear(); + entities.addAll(list); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Platform Market'), + leading: IconButton( + icon: Icon(Icons.arrow_back), + onPressed: () { + Navigator.pop(context); + })), + body: ListView.builder( + padding: const EdgeInsets.all(8), + itemCount: entities.length, + itemBuilder: (BuildContext context, int index) { + return Container(child: entities[index]); + }) + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index cdd55bb..2118c9a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -69,11 +69,32 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_archive: + dependency: "direct main" + description: + name: flutter_archive + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.1" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + http: + dependency: "direct main" + description: + name: http + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.13.3" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.0" intl: dependency: transitive description: @@ -130,6 +151,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.0.3" + pedantic: + dependency: transitive + description: + name: pedantic + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.11.0" platform: dependency: transitive description: @@ -227,5 +255,5 @@ packages: source: hosted version: "0.1.0" sdks: - dart: ">=2.12.0-0.0 <3.0.0" + dart: ">=2.12.0 <3.0.0" flutter: ">=1.12.13+hotfix.5" diff --git a/pubspec.yaml b/pubspec.yaml index 4d24852..947631a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,6 +28,8 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^0.1.3 + http: ^0.13.3 + flutter_archive: ^4.0.1 dev_dependencies: flutter_test: From 3f3dfd90dce35b908667dff7c7e99f2555d67926 Mon Sep 17 00:00:00 2001 From: Seven Lju Date: Sat, 15 May 2021 00:20:51 +0800 Subject: [PATCH 19/24] implement platform market install - click to install a platform - download binary - if zip, extract --- .../kotlin/net/seven/nodebase/MainActivity.kt | 20 +++++++-- .../main/kotlin/net/seven/nodebase/Storage.kt | 10 +++-- lib/page_platform_market.dart | 41 +++++++++++++++---- 3 files changed, 56 insertions(+), 15 deletions(-) diff --git a/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt b/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt index 8c76084..02719d0 100644 --- a/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt +++ b/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt @@ -131,7 +131,8 @@ class MainActivity: FlutterActivity() { private fun fetchAndUnzip(zipfile: String, target_dir: String): Boolean { // TODO: wrap a thread instead of running on main thread - return Storage.unzip(zipfile, target_dir) + Storage.unzip(zipfile, target_dir) + return true } private fun getAppStatus(app: String): String { @@ -179,6 +180,19 @@ class MainActivity: FlutterActivity() { return Network.getWifiIpv4(this) } + private fun _markExecutable(dst: String): Boolean { + val isZip = dst.endsWith(".zip") + if (isZip) { + val f = File(dst) + for (one in Storage.unzip(dst, f.getParentFile().getAbsolutePath())) { + Storage.executablize(one.getAbsolutePath()) + } + return Storage.unlink(dst) + } else { + return Storage.executablize(dst) + } + } + private fun fetchAndMarkExecutable(src: String, dst: String): Int { if (src == "") return -1 if (src.startsWith("file://")) { @@ -191,7 +205,7 @@ class MainActivity: FlutterActivity() { Alarm.showToast(this, "Copy failed: cannot copy origin") return -2 } - if (!Storage.executablize(dst)) { + if (!_markExecutable(dst)) { Alarm.showToast(this, "Copy failed: cannot set binary executable") return -3 } @@ -201,7 +215,7 @@ class MainActivity: FlutterActivity() { // download Download(this, Runnable() { fun run() { - Storage.executablize(dst) + _markExecutable(dst) } }).act("fetch", src, dst) } diff --git a/android/app/src/main/kotlin/net/seven/nodebase/Storage.kt b/android/app/src/main/kotlin/net/seven/nodebase/Storage.kt index f8d5053..6b03fc1 100644 --- a/android/app/src/main/kotlin/net/seven/nodebase/Storage.kt +++ b/android/app/src/main/kotlin/net/seven/nodebase/Storage.kt @@ -190,7 +190,8 @@ object Storage { return filtered.toTypedArray() } - fun unzip(zipfile: String, target_dir: String): Boolean { + fun unzip(zipfile: String, target_dir: String): Array { + val unzipFiles = ArrayList() try { val `in` = FileInputStream(zipfile) val zip = ZipInputStream(`in`) @@ -218,6 +219,7 @@ object Storage { writer.close() out.close() zip.closeEntry() + unzipFiles.add(File(target_filename)) } entry = zip.nextEntry } @@ -225,13 +227,13 @@ object Storage { `in`.close() } catch (e: FileNotFoundException) { e.printStackTrace() - return false + return unzipFiles.toTypedArray() } catch (e: IOException) { e.printStackTrace() - return false + return unzipFiles.toTypedArray() } - return true + return unzipFiles.toTypedArray() } fun zip(target_dir: String, zipfile: String): Boolean { diff --git a/lib/page_platform_market.dart b/lib/page_platform_market.dart index ad5ef49..49bf4e6 100644 --- a/lib/page_platform_market.dart +++ b/lib/page_platform_market.dart @@ -1,14 +1,18 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import './api.dart'; +import './io.dart'; +import './app_model.dart'; String BASE_HOST = 'raw.githubusercontent.com'; String BASE_URL = '/wiki/dna2github/NodeBase'; class PlatformItem { - String arch; - String name; - bool zip; + String arch = ""; + String name = ""; + bool zip = false; } class NodeBasePlatformMarketItem extends StatefulWidget { @@ -47,6 +51,7 @@ class _NodeBasePlatformMarketItemState itemBuilder: (BuildContext context) => >[ const PopupMenuItem( value: 101, child: Text('Install')), + // TODO: add Uninstall ]) // PopupMenuButton )); } @@ -72,10 +77,6 @@ class _NodeBasePlatformMarketState extends State { .get(Uri.https(BASE_HOST, BASE_URL + '/quick/platform/meta.list')); } - Future downloadFile(String uri) async { - return http.get(Uri.https(BASE_HOST, BASE_URL + uri)); - } - Future> _getPlatformItems() async { final res = await fetchPlatformList(); List r = []; @@ -88,13 +89,37 @@ class _NodeBasePlatformMarketState extends State { one.name = parts[1]; if (parts.length == 3 && parts[2] == "zip") { one.zip = true; + } else { + one.zip = false; } r.add(one); } return r; } - installPlatformItem (PlatformItem item) {} + installPlatformItem (PlatformItem item) async { + final url = "https://" + BASE_HOST + BASE_URL + "/quick/platform/" + item.arch + "/" + item.name + ( item.zip?".zip":"" ); + print(url); + var dst = await NodeBaseApi.fetchExecutable(url); + if (dst.endsWith(".zip")) { + dst = dst.substring(0, dst.length - 4); + } + // TODO: handle download exception + var config = await readAppFileAsString("/platform.json"); + if (config == "") config = "{\"platforms\": []}"; + final data = jsonDecode(config); + final list = data["platforms"].toList(); + list.add({ + "name": item.name, + "path": dst, + "url": url + }); + await writeAppFileAsString( + "/platform.json", + JsonEncoder((x) { + return x; + }).convert({"platforms": list})); + } uninstallPlatformItem (PlatformItem item) {} From a02e465194ab428e2b7880a5f009351f0bdc2466 Mon Sep 17 00:00:00 2001 From: Seven Lju Date: Sat, 15 May 2021 11:23:04 +0800 Subject: [PATCH 20/24] bugfix: fetch zip file, extract and set up executable from platform market --- .../kotlin/net/seven/nodebase/Download.kt | 4 +- .../kotlin/net/seven/nodebase/MainActivity.kt | 12 ++++-- lib/api.dart | 1 + lib/page_platform_market.dart | 39 ++++++++++--------- 4 files changed, 33 insertions(+), 23 deletions(-) diff --git a/android/app/src/main/kotlin/net/seven/nodebase/Download.kt b/android/app/src/main/kotlin/net/seven/nodebase/Download.kt index 0caee69..45e0f50 100644 --- a/android/app/src/main/kotlin/net/seven/nodebase/Download.kt +++ b/android/app/src/main/kotlin/net/seven/nodebase/Download.kt @@ -5,6 +5,7 @@ package net.seven.nodebase import android.app.ProgressDialog import android.content.Context import android.os.AsyncTask +import android.os.Handler import java.io.FileOutputStream import java.io.IOException @@ -91,9 +92,10 @@ class Download(private val context: Context, private val callback: Runnable?) { } override fun onPostExecute(result: String?) { + super.onPostExecute(result); downloader.progress.setMessage("do post actions ...") if (downloader.callback != null) { - downloader.callback.run() + Handler(downloader.context.getMainLooper()).post(downloader.callback) } downloader.progress.dismiss() if (result == null) { diff --git a/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt b/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt index 02719d0..1592c75 100644 --- a/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt +++ b/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt @@ -184,7 +184,10 @@ class MainActivity: FlutterActivity() { val isZip = dst.endsWith(".zip") if (isZip) { val f = File(dst) - for (one in Storage.unzip(dst, f.getParentFile().getAbsolutePath())) { + val t = f.getParentFile().getAbsolutePath() + android.util.Log.i("NodeBase", String.format("extracting %s -> %s ...", dst, t)) + for (one in Storage.unzip(dst, t)) { + android.util.Log.i("NodeBase", String.format(" %s", one.getAbsolutePath())) Storage.executablize(one.getAbsolutePath()) } return Storage.unlink(dst) @@ -213,11 +216,12 @@ class MainActivity: FlutterActivity() { return 0 } else { // download - Download(this, Runnable() { - fun run() { + val postAction = object : Runnable { + override fun run() { _markExecutable(dst) } - }).act("fetch", src, dst) + } + Download(this, postAction).act("fetch", src, dst) } return 0 } diff --git a/lib/api.dart b/lib/api.dart index dfab6bd..8334767 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -35,6 +35,7 @@ class NodeBaseApi { try { appApi.invokeMethod( 'FetchExecutable', {"url": url, "target": dst}); + if (dst.endsWith(".zip")) return dst.substring(0, dst.length - 4); return dst; } catch (e) { return null; diff --git a/lib/page_platform_market.dart b/lib/page_platform_market.dart index 49bf4e6..fe63576 100644 --- a/lib/page_platform_market.dart +++ b/lib/page_platform_market.dart @@ -100,25 +100,28 @@ class _NodeBasePlatformMarketState extends State { installPlatformItem (PlatformItem item) async { final url = "https://" + BASE_HOST + BASE_URL + "/quick/platform/" + item.arch + "/" + item.name + ( item.zip?".zip":"" ); print(url); - var dst = await NodeBaseApi.fetchExecutable(url); - if (dst.endsWith(".zip")) { - dst = dst.substring(0, dst.length - 4); + try { + var dst = await NodeBaseApi.fetchExecutable(url); + if (dst.endsWith(".zip")) { + dst = dst.substring(0, dst.length - 4); + } + var config = await readAppFileAsString("/platform.json"); + if (config == "") config = "{\"platforms\": []}"; + final data = jsonDecode(config); + final list = data["platforms"].toList(); + list.add({ + "name": item.name, + "path": dst, + "url": url + }); + await writeAppFileAsString( + "/platform.json", + JsonEncoder((x) { + return x; + }).convert({"platforms": list})); + } catch(e) { + // TODO: handle exception } - // TODO: handle download exception - var config = await readAppFileAsString("/platform.json"); - if (config == "") config = "{\"platforms\": []}"; - final data = jsonDecode(config); - final list = data["platforms"].toList(); - list.add({ - "name": item.name, - "path": dst, - "url": url - }); - await writeAppFileAsString( - "/platform.json", - JsonEncoder((x) { - return x; - }).convert({"platforms": list})); } uninstallPlatformItem (PlatformItem item) {} From 6be66532bb6ad51c363772ab91fdf72c1d1c83c5 Mon Sep 17 00:00:00 2001 From: Seven Lju Date: Sat, 15 May 2021 13:10:54 +0800 Subject: [PATCH 21/24] support app market - add market button to app page - download and install app from center site --- .../kotlin/net/seven/nodebase/MainActivity.kt | 57 +++++++ lib/api.dart | 16 ++ lib/page_app_market.dart | 159 ++++++++++++++++++ lib/page_apps.dart | 25 ++- lib/page_platform_market.dart | 1 - 5 files changed, 250 insertions(+), 8 deletions(-) create mode 100644 lib/page_app_market.dart diff --git a/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt b/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt index 1592c75..5f78b52 100644 --- a/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt +++ b/android/app/src/main/kotlin/net/seven/nodebase/MainActivity.kt @@ -69,6 +69,18 @@ class MainActivity: FlutterActivity() { } result.success(fetchAndMarkExecutable(src, dst)) } + } else if (call.method == "FetchApp") { + var src: String? = call.argument("url") + var dst: String? = call.argument("target") + if (src == null || dst == null) { + result.error("INVALID_PARAMS", "invalid parameter.", null) + } else { + val dir = File(dst) + if (!dir.exists()) { + Storage.makeDirectory(dir.getAbsolutePath()) + } + result.success(fetchApp(src, dst)) + } } else if (call.method == "FetchWifiIpv4") { result.success(fetchWifiIpv4()) } else { @@ -226,6 +238,51 @@ class MainActivity: FlutterActivity() { return 0 } + private fun _unpackApp(dst: String): Boolean { + // dst is a zip file path + val f = File(dst) + val t = f.getParentFile().getAbsolutePath() + android.util.Log.i("NodeBase", String.format("extracting %s -> %s ...", dst, t)) + for (one in Storage.unzip(dst, t)) { + android.util.Log.i("NodeBase", String.format(" %s", one.getAbsolutePath())) + } + return Storage.unlink(dst) + } + + private fun fetchApp(src: String, dst: String): Int { + if (src == "") return -1 + if (!src.endsWith(".zip")) return -1 + Storage.makeDirectory(dst) + val src_name = File(src).getName() + var dst_zip = dst + "/" + src_name + if (src.startsWith("file://")) { + Permission.request(this) + var final_src = src + final_src = final_src.substring("file://".length) + // Add Alarm to align with Download() + // XXX: but how about we move Alarm out of Download() and use call back to do alarm? + if (!Storage.copy(final_src, dst_zip)) { + Alarm.showToast(this, "Copy failed: cannot copy origin") + return -2 + } + if (!_unpackApp(dst_zip)) { + Alarm.showToast(this, "Copy failed: cannot set binary executable") + return -3 + } + Alarm.showToast(this, "Copy successful") + return 0 + } else { + // download + val postAction = object : Runnable { + override fun run() { + _unpackApp(dst_zip) + } + } + Download(this, postAction).act("fetch", src, dst_zip) + } + return 0 + } + private fun requestExternalStoragePermission(): Int { Permission.request(this) return 0 diff --git a/lib/api.dart b/lib/api.dart index 8334767..aad5beb 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -42,6 +42,22 @@ class NodeBaseApi { } } + static Future fetchApp(String url) async { + if (url == null || url == "") return null; + var name = url.split("/").last; + if (name.endsWith(".zip")) name = name.substring(0, name.length - 4); + final dst = (await getAppFileReference('/apps/${name}')).path; + // e.g. node -> /path/to/app/apps/node + if (url.indexOf("://") < 0) return dst; + try { + appApi.invokeMethod( + 'FetchApp', {"url": url, "target": dst}); + return dst; + } catch (e) { + return null; + } + } + static Future fetchWifiIpv4() async { try { return appApi.invokeMethod('FetchWifiIpv4'); diff --git a/lib/page_app_market.dart b/lib/page_app_market.dart new file mode 100644 index 0000000..d727068 --- /dev/null +++ b/lib/page_app_market.dart @@ -0,0 +1,159 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import './api.dart'; +import './io.dart'; + +String BASE_HOST = 'raw.githubusercontent.com'; +String BASE_URL = '/wiki/dna2github/NodeBase'; + +class AppItem { + String platform = ""; + String name = ""; + bool zip = false; +} + +class NodeBaseAppMarketItem extends StatefulWidget { + final Function(AppItem item) fnInstall; + final Function(AppItem item) fnUninstall; + final AppItem item; + + NodeBaseAppMarketItem({Key key, this.item, this.fnInstall, this.fnUninstall}) + : super(key: key); + + @override + _NodeBaseAppMarketItemState createState() => + _NodeBaseAppMarketItemState(); +} + +class _NodeBaseAppMarketItemState extends State { + @override + Widget build(BuildContext context) { + return Card( + child: ListTile( + title: Text(widget.item.name), + subtitle: Text(widget.item.platform), + trailing: PopupMenuButton( + icon: Icon(Icons.more_vert), + onSelected: (int result) { + switch (result) { + case 101: + { + widget.fnInstall(widget.item); + } + break; + } + }, + itemBuilder: (BuildContext context) => >[ + const PopupMenuItem( + value: 101, child: Text('Install')), + // TODO: add Uninstall + ]) // PopupMenuButton + )); + } +} + +class NodeBaseAppMarket extends StatefulWidget { + NodeBaseAppMarket({Key key}) : super(key: key); + @override + _NodeBaseAppMarketState createState() => _NodeBaseAppMarketState(); +} + +class _NodeBaseAppMarketState extends State { + List entities = []; + bool loading = true; + + @override + void initState() { + _showAppItems(); + } + + Future fetchAppList() async { + return http.get(Uri.https(BASE_HOST, BASE_URL + '/quick/app/meta.list')); + } + + Future> _getAppItems() async { + final res = await fetchAppList(); + List r = []; + for (String line in res.body.split("\n")) { + if (line.length == 0) continue; + final parts = line.split(" "); + // platform name zip? + final one = AppItem(); + one.platform = parts[0]; + one.name = parts[1]; + if (parts.length == 3 && parts[2] == "zip") { + one.zip = true; + } else { + one.zip = false; + } + r.add(one); + } + return r; + } + + installAppItem(AppItem item) async { + final url = "https://" + + BASE_HOST + + BASE_URL + + "/quick/app/" + + item.platform + + "/" + + item.name + + (item.zip ? ".zip" : ""); + print(url); + try { + var dst = await NodeBaseApi.fetchApp(url); + var config = await readAppFileAsString("/apps.json"); + if (config == "") config = "{\"apps\": []}"; + final data = jsonDecode(config); + final list = data["apps"].toList(); + list.add({"name": item.name, "path": dst, "platform": item.platform}); + await writeAppFileAsString( + "/apps.json", + JsonEncoder((x) { + return x; + }).convert({"apps": list})); + } catch (e) { + // TODO: handle exception + } + } + + uninstallAppItem(AppItem item) {} + + Future _showAppItems() async { + final items = await _getAppItems(); + final List list = []; + for (AppItem item in items) { + final tile = NodeBaseAppMarketItem( + item: item, + fnInstall: installAppItem, + fnUninstall: uninstallAppItem, + ); + list.add(tile); + } + setState(() { + entities.clear(); + entities.addAll(list); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Application Market'), + leading: IconButton( + icon: Icon(Icons.arrow_back), + onPressed: () { + Navigator.pop(context); + })), + body: ListView.builder( + padding: const EdgeInsets.all(8), + itemCount: entities.length, + itemBuilder: (BuildContext context, int index) { + return Container(child: entities[index]); + })); + } +} diff --git a/lib/page_apps.dart b/lib/page_apps.dart index 8614e05..c4eddf2 100644 --- a/lib/page_apps.dart +++ b/lib/page_apps.dart @@ -4,6 +4,7 @@ import './page_app_home.dart'; import './io.dart'; import './app_model.dart'; import './api.dart'; +import './page_app_market.dart'; class NodeBaseAppItem extends StatefulWidget { final Function(NodeBaseAppItem item) fnRemove; @@ -56,7 +57,7 @@ class _NodeBaseAppItemState extends State { leading: SizedBox(width: 5), title: Text(widget.item.path))); } entities.add(Row(children: [ - FlatButton.icon( + TextButton.icon( icon: Icon(Icons.check), label: Text("Save"), onPressed: () { @@ -72,7 +73,7 @@ class _NodeBaseAppItemState extends State { widget.fnSaveConfig(); }); }), - FlatButton.icon( + TextButton.icon( icon: Icon(Icons.close), label: Text("Cancel"), onPressed: () { @@ -211,12 +212,22 @@ class _NodeBaseApplicationsState extends State { } return Scaffold( appBar: AppBar( - title: Text('Applications'), - leading: IconButton( - icon: Icon(Icons.arrow_back), + title: Text('Applications'), + leading: IconButton( + icon: Icon(Icons.arrow_back), + onPressed: () { + Navigator.pop(context); + }), + actions: [ + IconButton( + icon: Icon(Icons.add_shopping_cart), onPressed: () { - Navigator.pop(context); - })), + var route = MaterialPageRoute( + builder: (context) => NodeBaseAppMarket()); + Navigator.push(context, route); + }) + ], + ), body: (entities.length == 0) ? Center(child: Text('No application.')) : ListView.builder( diff --git a/lib/page_platform_market.dart b/lib/page_platform_market.dart index fe63576..64589b8 100644 --- a/lib/page_platform_market.dart +++ b/lib/page_platform_market.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import './api.dart'; import './io.dart'; -import './app_model.dart'; String BASE_HOST = 'raw.githubusercontent.com'; String BASE_URL = '/wiki/dna2github/NodeBase'; From 230fa42583bf306510297c0c7636171834906e49 Mon Sep 17 00:00:00 2001 From: Seven Lju Date: Thu, 17 Jun 2021 09:17:59 +0800 Subject: [PATCH 22/24] update README for simple platform/app market --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 414a123..9be0a51 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,14 @@ Currently we are redesigning whole NodeBase based on Flutter. ## How to use +- Platform/App market is online + - click into platform or application page + - click on the top-right cart icon button + - select what you want to download + - e.g. download platform `node-10.10.0` + - download app `file-transfer` + - edit app `file-transfer` platform value to `node-10.10.0` + - start `file-transfer` by clicking play icon button - Create a new platform, for example named `node` - fill node url like `file:///sdcard/bin-node-v10.10.0` or `https://example.com/latest/arm/node` - click download button and wait for task complete From c4839b215158fbf887e0f3e04cd476e0e5a0b060 Mon Sep 17 00:00:00 2001 From: Seven Lju Date: Sun, 14 Jan 2024 21:52:54 +0800 Subject: [PATCH 23/24] improve get children pids via checkoutput - ensure checkout to get correct full output - ensure get children correct pids --- .../kotlin/net/seven/nodebase/NodeMonitor.kt | 6 ++-- .../kotlin/net/seven/nodebase/NodeService.kt | 31 ++++++++++--------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/android/app/src/main/kotlin/net/seven/nodebase/NodeMonitor.kt b/android/app/src/main/kotlin/net/seven/nodebase/NodeMonitor.kt index c176b62..a22439b 100644 --- a/android/app/src/main/kotlin/net/seven/nodebase/NodeMonitor.kt +++ b/android/app/src/main/kotlin/net/seven/nodebase/NodeMonitor.kt @@ -87,8 +87,8 @@ class NodeMonitor(val serviceName: String, val command: Array) : Thread( fun childrenProcesses(pid: Int): Array { var children = arrayOf() val output = NodeService.checkOutput(arrayOf("/system/bin/ps", "-o", "pid=", "--ppid", pid.toString())) - if (output == "") return children - val lines = output!!.split("\n") + if (output == null || output == "") return children + val lines = output.trim().split("\n") lines.forEach { if (it != "") { try { @@ -108,7 +108,7 @@ class NodeMonitor(val serviceName: String, val command: Array) : Thread( // we do not guarantee `test` children are killed // another example, if we use `sh -c "go run test.go"` -> `go run test.go` -> `test` // when kill, we merely kill `go` and `sh` but no `test` - Log.d("NodeMonitor", NodeService.checkOutput(arrayOf("/system/bin/ps", "-ef"))) + Log.d("NodeMonitor", NodeService.checkOutput(arrayOf("/system/bin/ps", "-ef")) ?: "") val children = childrenProcesses(pid) children.forEach { if (it > 0) { diff --git a/android/app/src/main/kotlin/net/seven/nodebase/NodeService.kt b/android/app/src/main/kotlin/net/seven/nodebase/NodeService.kt index 950e459..da1c12c 100644 --- a/android/app/src/main/kotlin/net/seven/nodebase/NodeService.kt +++ b/android/app/src/main/kotlin/net/seven/nodebase/NodeService.kt @@ -6,6 +6,9 @@ import android.content.Context import android.content.Intent import android.os.IBinder import android.util.Log +import java.lang.Process +import java.lang.ProcessBuilder +import java.lang.StringBuffer import java.util.HashMap import java.util.UUID @@ -19,7 +22,7 @@ class NodeService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { while (intent != null) { val argv = intent.getStringArrayExtra(ARGV) - if (argv.size < 3) break + if (argv == null || argv.size < 3) break val auth_token = argv[0] var cmd = argv[1] val first = argv[2] @@ -106,25 +109,23 @@ class NodeService : Service() { return uuid.toString() } - fun checkOutput(cmd: Array): String? { + fun checkOutput(cmd: Array, joinStderr: Boolean = false): String? { try { - val p = Runtime.getRuntime().exec(cmd) - p.waitFor() - val `is` = p.inputStream - var len = `is`.available() - var b: ByteArray? = null - if (len > 0) { - b = ByteArray(len) - len = `is`.read(b) + val p = if (joinStderr) ProcessBuilder(cmd.asList()).redirectErrorStream(true).start() + else ProcessBuilder(cmd.asList()).start() + val reader = java.io.BufferedReader(java.io.InputStreamReader(p.inputStream)) + var sb = StringBuffer() + var line: String? = null + while (reader.readLine().also { line = it } != null) { + sb.append(line) + sb.append('\n') } - `is`.close() - return if (b == null) { - null - } else String(b, 0, len) + p.waitFor() + reader.close() + return sb.toString().substring(0, sb.length - 1) } catch (e: Exception) { return null } - } From 78f227907fe612d0fff89c4a6d8969cc24561f78 Mon Sep 17 00:00:00 2001 From: Seven Lju Date: Sun, 14 Jan 2024 21:56:46 +0800 Subject: [PATCH 24/24] upgrade flutter to latest version - upgrade flutter - upgrade gradle - upgrade dart - upgrade all depedencies - webview - remove cookie manager code - rewrite in new version style - fix null-safe code - fix android sdk compatible issues --- android/app/build.gradle | 25 ++- android/app/src/main/AndroidManifest.xml | 6 +- android/build.gradle | 6 +- lib/api.dart | 18 +-- lib/app_model.dart | 33 ++-- lib/homepage.dart | 3 +- lib/io.dart | 6 +- lib/page_app_home.dart | 11 +- lib/page_app_market.dart | 10 +- lib/page_app_webview.dart | 179 ++++++++++------------ lib/page_apps.dart | 10 +- lib/page_environment.dart | 2 +- lib/page_platform.dart | 10 +- lib/page_platform_market.dart | 11 +- pubspec.lock | 186 +++++++++++++++-------- pubspec.yaml | 12 +- 16 files changed, 308 insertions(+), 220 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 1f1647a..5da3833 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -26,24 +26,33 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 28 + namespace "net.seven.nodebase" + compileSdkVersion 33 - sourceSets { - main.java.srcDirs += 'src/main/kotlin' + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } - lintOptions { - disable 'InvalidPackage' + kotlinOptions { + jvmTarget = '17' } - defaultConfig { applicationId "net.seven.nodebase" - minSdkVersion 16 - targetSdkVersion 28 + targetSdkVersion 33 + minSdkVersion flutter.minSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName } + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + lintOptions { + disable 'InvalidPackage' + } + buildTypes { release { // TODO: Add your own signing config for the release build. diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index dc513a5..e2b3070 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -8,11 +8,12 @@ + fetchExecutable(String url) async { - if (url == null || url == "") return null; + if (url == "") return ""; final name = url.split("/").last; final dst = (await getAppFileReference('/bin/${name}')).path; // e.g. node -> /path/to/app/bin/node @@ -38,12 +38,12 @@ class NodeBaseApi { if (dst.endsWith(".zip")) return dst.substring(0, dst.length - 4); return dst; } catch (e) { - return null; + return ""; } } static Future fetchApp(String url) async { - if (url == null || url == "") return null; + if (url == "") return ""; var name = url.split("/").last; if (name.endsWith(".zip")) name = name.substring(0, name.length - 4); final dst = (await getAppFileReference('/apps/${name}')).path; @@ -54,11 +54,11 @@ class NodeBaseApi { 'FetchApp', {"url": url, "target": dst}); return dst; } catch (e) { - return null; + return ""; } } - static Future fetchWifiIpv4() async { + static Future fetchWifiIpv4() async { try { return appApi.invokeMethod('FetchWifiIpv4'); } catch (e) { @@ -66,7 +66,7 @@ class NodeBaseApi { } } - static Future appStatus(String app) async { + static Future appStatus(String app) async { try { return nodebaseApi .invokeMethod('GetStatus', {"app": app}); @@ -88,7 +88,7 @@ class NodeBaseApi { } catch (e) {} } - static Future appUnpack(String app, String zipfile) async { + static Future appUnpack(String app, String zipfile) async { final appBaseDir = await ioGetAppBaseDir(app); try { nodebaseApi.invokeMethod('Unpack', { @@ -99,7 +99,7 @@ class NodeBaseApi { } catch (e) {} } - static Future appPack(String app, String zipfile) async { + static Future appPack(String app, String zipfile) async { final appBaseDir = await ioGetAppBaseDir(app); try { nodebaseApi.invokeMethod('Pack', { @@ -110,7 +110,7 @@ class NodeBaseApi { } catch (e) {} } - static Future appBrowser(String url) async { + static Future appBrowser(String url) async { try { nodebaseApi.invokeMethod('Browser', { "url": url, diff --git a/lib/app_model.dart b/lib/app_model.dart index 274cb8e..eba7d38 100644 --- a/lib/app_model.dart +++ b/lib/app_model.dart @@ -1,7 +1,12 @@ import 'package:flutter/material.dart'; class NodeBaseAppModule { - NodeBaseAppModule({Key key, this.id, this.icon, this.name, this.desc}) {} + NodeBaseAppModule({ + required this.id, + required this.icon, + required this.name, + required this.desc, + }) {} final int id; final String name; @@ -28,29 +33,29 @@ class NodeBaseAppModule { } class NodeBasePlatform { - NodeBasePlatform({Key key, this.name}) {} + NodeBasePlatform({required this.name}) {} - String name; - String path; - String updateUrl; + String name = ""; + String path = ""; + String updateUrl = ""; } class NodeBaseApp { - NodeBaseApp({Key key, this.name}) {} + NodeBaseApp({required this.name}) {} - String name; - String path; - String platform; + String name = ""; + String path = ""; + String platform = ""; } class NodeBaseAppDetails { - String path; + String path = ""; // e.g. 127.0.0.1, 0.0.0.0 - String host; + String host = ""; // e.g. 9090 - int port; + int port = 0; // e.g. index.js - String entry; + String entry = ""; // e.g. /index.html - String home; + String home = ""; } diff --git a/lib/homepage.dart b/lib/homepage.dart index 67e612a..93bacf1 100644 --- a/lib/homepage.dart +++ b/lib/homepage.dart @@ -8,7 +8,7 @@ import './page_platform.dart'; import './page_apps.dart'; class NodeBaseHomePage extends StatefulWidget { - NodeBaseHomePage({Key key, this.title}) : super(key: key); + NodeBaseHomePage({required this.title, super.key}); final String title; @@ -82,6 +82,7 @@ class _NodeBaseHomePageState extends State { Text('${NodeBaseAppModule.list[index].desc}'), leading: IconButton( icon: NodeBaseAppModule.list[index].icon, + onPressed: () => {}, ) // IconButton ) // ListTile ) // Card diff --git a/lib/io.dart b/lib/io.dart index f456e79..8458ebf 100644 --- a/lib/io.dart +++ b/lib/io.dart @@ -24,17 +24,17 @@ Future readAppFileAsString(filepath) async { } } -Future writeAppFileAsString(filepath, contents) async { +Future writeAppFileAsString(filepath, contents) async { final file = await getAppFileReference(filepath); file.writeAsString(contents); } -Future ioGetEntity(filepath) async { +Future ioGetEntity(filepath) async { final path = await _appPath; final filename = '$path$filepath'; final T = await FileSystemEntity.type(filename); if (T == FileSystemEntityType.notFound) { - return null; + return Object(); } if (T == FileSystemEntityType.link) { return Link(filepath); diff --git a/lib/page_app_home.dart b/lib/page_app_home.dart index 0068e83..9d816cd 100644 --- a/lib/page_app_home.dart +++ b/lib/page_app_home.dart @@ -8,7 +8,7 @@ import './page_app_webview.dart'; class NodeBaseAppHome extends StatefulWidget { final NodeBaseApp item; - NodeBaseAppHome({Key key, this.item}) : super(key: key); + NodeBaseAppHome({required this.item, super.key}); @override _NodeBaseAppHomeState createState() => _NodeBaseAppHomeState(); } @@ -35,7 +35,7 @@ class _NodeBaseAppHomeState extends State { }); } - Future loadPlatform(String name) async { + Future loadPlatform(String name) async { var config = await readAppFileAsString("/platform.json"); final List list = []; if (config != "") { @@ -53,7 +53,7 @@ class _NodeBaseAppHomeState extends State { return null; } - Future loadAppDetails(String name) async { + Future loadAppDetails(String name) async { var config = await readAppFileAsString("/apps/${name}/config.json"); if (config != "") { final data = jsonDecode(config); @@ -73,10 +73,11 @@ class _NodeBaseAppHomeState extends State { super.initState(); NodeBaseApi.fetchWifiIpv4().then((ip) { setState(() { - wifiIp = ip; + wifiIp = ip == null ? "0.0.0.0" : ip; }); loadAppDetails(widget.item.name).then((item) { setState(() { + if (item == null) return; if (item.host != "") { final parts = item.host.split("://"); if (parts.length > 1) { @@ -153,7 +154,7 @@ class _NodeBaseAppHomeState extends State { widget.item.name == null || widget.item.name == "") { Navigator.pop(context); - return null; + return Scaffold(); } if (loading) { return Scaffold( diff --git a/lib/page_app_market.dart b/lib/page_app_market.dart index d727068..aefd5a0 100644 --- a/lib/page_app_market.dart +++ b/lib/page_app_market.dart @@ -19,8 +19,12 @@ class NodeBaseAppMarketItem extends StatefulWidget { final Function(AppItem item) fnUninstall; final AppItem item; - NodeBaseAppMarketItem({Key key, this.item, this.fnInstall, this.fnUninstall}) - : super(key: key); + NodeBaseAppMarketItem({ + required this.item, + required this.fnInstall, + required this.fnUninstall, + super.key + }); @override _NodeBaseAppMarketItemState createState() => @@ -55,7 +59,7 @@ class _NodeBaseAppMarketItemState extends State { } class NodeBaseAppMarket extends StatefulWidget { - NodeBaseAppMarket({Key key}) : super(key: key); + NodeBaseAppMarket({super.key}); @override _NodeBaseAppMarketState createState() => _NodeBaseAppMarketState(); } diff --git a/lib/page_app_webview.dart b/lib/page_app_webview.dart index 527d0d0..d2b7a3f 100644 --- a/lib/page_app_webview.dart +++ b/lib/page_app_webview.dart @@ -7,15 +7,55 @@ class NodeBaseAppWebview extends StatefulWidget { String name; String home; - NodeBaseAppWebview({Key key, this.name, this.home}) : super(key: key); + NodeBaseAppWebview({ + required this.name, + required this.home, + super.key + }); @override _NodeBaseAppWebviewState createState() => _NodeBaseAppWebviewState(); } class _NodeBaseAppWebviewState extends State { - final Completer _controller = - Completer(); + late WebViewController _controller; + + @override + void initState() { + super.initState(); + _controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate( + NavigationDelegate( + onProgress: (int progress) {}, + onPageStarted: (String url) { + print('- Page started loading: $url'); + }, + onPageFinished: (String url) { + print('- Page finished loading: $url'); + }, + onWebResourceError: (WebResourceError error) {}, + onNavigationRequest: (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; + } + ) + ) + ..loadRequest(Uri.parse(widget.home)); + + _controller.addJavaScriptChannel( + "Toaster", + onMessageReceived: (JavaScriptMessage message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message.message)), + ); + } + ); + } @override Widget build(BuildContext context) { @@ -23,65 +63,30 @@ class _NodeBaseAppWebviewState extends State { appBar: AppBar( title: Text(widget.name), actions: [ - NavigationControls(_controller.future), - SampleMenu(_controller.future), + NavigationControls(Future(() => _controller)), + SampleMenu(Future(() => _controller)), ], ), // 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: widget.home, // 'about:blank', - 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: false, - ); + return WebViewWidget(controller: _controller); }), 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, + future: Future(() => _controller), builder: (BuildContext context, AsyncSnapshot controller) { if (controller.hasData) { return FloatingActionButton( onPressed: () async { - final String url = await controller.data.currentUrl(); - Scaffold.of(context).showSnackBar( + final String? url = await controller.data?.currentUrl(); + if (url == null) return; + ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Favorited $url')), ); }, @@ -107,7 +112,6 @@ class SampleMenu extends StatelessWidget { SampleMenu(this.controller); final Future controller; - final CookieManager cookieManager = CookieManager(); @override Widget build(BuildContext context) { @@ -124,9 +128,6 @@ class SampleMenu extends StatelessWidget { case MenuOptions.listCookies: _onListCookies(controller.data, context); break; - case MenuOptions.clearCookies: - _onClearCookies(context); - break; case MenuOptions.addToCache: _onAddToCache(controller.data, context); break; @@ -151,10 +152,6 @@ class SampleMenu extends StatelessWidget { 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'), @@ -178,69 +175,61 @@ class SampleMenu extends StatelessWidget { } void _onShowUserAgent( - WebViewController controller, BuildContext context) async { + 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( + if (controller == null) return; + await controller.runJavaScript( '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( + WebViewController? controller, BuildContext context) async { + if (controller == null) return; + final Object cookies = + await controller.runJavaScriptReturningResult('document.cookie'); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Column( mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.min, children: [ const Text('Cookies:'), - _getCookieList(cookies), + _getCookieList(cookies.toString()), ], ), )); } - void _onAddToCache(WebViewController controller, BuildContext context) async { - await controller.evaluateJavascript( + void _onAddToCache(WebViewController? controller, BuildContext context) async { + if (controller == null) return; + await controller.runJavaScript( 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";'); - Scaffold.of(context).showSnackBar(const SnackBar( + ScaffoldMessenger.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()' + void _onListCache(WebViewController? controller, BuildContext context) async { + if (controller == null) return; + await controller.runJavaScript('caches.keys()' '.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))' '.then((caches) => Toaster.postMessage(caches))'); } - void _onClearCache(WebViewController controller, BuildContext context) async { + void _onClearCache(WebViewController? controller, BuildContext context) async { + if (controller == null) return; await controller.clearCache(); - Scaffold.of(context).showSnackBar(const SnackBar( + ScaffoldMessenger.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)); - String contentBase64 = base64Encode( - const Utf8Encoder().convert('')); - //String contentBase64 = ''; - await controller.loadUrl('data:text/html;base64,$contentBase64'); + WebViewController? controller, BuildContext context) async { + if (controller == null) return; + await controller.loadRequest( + Uri.dataFromString( + '', mimeType: 'text/html', encoding: Encoding.getByName('utf-8'))); } Widget _getCookieList(String cookies) { @@ -259,9 +248,7 @@ class SampleMenu extends StatelessWidget { } class NavigationControls extends StatelessWidget { - const NavigationControls(this._webViewControllerFuture) - : assert(_webViewControllerFuture != null); - + const NavigationControls(this._webViewControllerFuture); final Future _webViewControllerFuture; @override @@ -272,7 +259,7 @@ class NavigationControls extends StatelessWidget { (BuildContext context, AsyncSnapshot snapshot) { final bool webViewReady = snapshot.connectionState == ConnectionState.done; - final WebViewController controller = snapshot.data; + final WebViewController? controller = snapshot.data; return Row( children: [ IconButton( @@ -280,10 +267,10 @@ class NavigationControls extends StatelessWidget { onPressed: !webViewReady ? null : () async { - if (await controller.canGoBack()) { - await controller.goBack(); + if (true == await controller?.canGoBack()) { + await controller?.goBack(); } else { - Scaffold.of(context).showSnackBar( + ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text("No back history item")), ); return; @@ -295,10 +282,10 @@ class NavigationControls extends StatelessWidget { onPressed: !webViewReady ? null : () async { - if (await controller.canGoForward()) { - await controller.goForward(); + if (true == await controller?.canGoForward()) { + await controller?.goForward(); } else { - Scaffold.of(context).showSnackBar( + ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text("No forward history item")), ); @@ -311,7 +298,7 @@ class NavigationControls extends StatelessWidget { onPressed: !webViewReady ? null : () { - controller.reload(); + controller?.reload(); }, ), ], diff --git a/lib/page_apps.dart b/lib/page_apps.dart index c4eddf2..2fe53e9 100644 --- a/lib/page_apps.dart +++ b/lib/page_apps.dart @@ -13,8 +13,12 @@ class NodeBaseAppItem extends StatefulWidget { bool isEdit = false; bool isCreated = false; - NodeBaseAppItem({Key key, this.item, this.fnRemove, this.fnSaveConfig}) - : super(key: key); + NodeBaseAppItem({ + required this.item, + required this.fnRemove, + required this.fnSaveConfig, + super.key + }); @override _NodeBaseAppItemState createState() => _NodeBaseAppItemState(); @@ -131,7 +135,7 @@ class _NodeBaseAppItemState extends State { } class NodeBaseApplications extends StatefulWidget { - NodeBaseApplications({Key key}) : super(key: key); + NodeBaseApplications({super.key}); @override _NodeBaseApplicationsState createState() => _NodeBaseApplicationsState(); } diff --git a/lib/page_environment.dart b/lib/page_environment.dart index 08a9d36..b357f36 100644 --- a/lib/page_environment.dart +++ b/lib/page_environment.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import './api.dart'; class NodeBaseEnvironmentSettings extends StatefulWidget { - NodeBaseEnvironmentSettings({Key key}) : super(key: key); + NodeBaseEnvironmentSettings({super.key}); @override _NodeBaseEnvironmentSettingsState createState() => _NodeBaseEnvironmentSettingsState(); diff --git a/lib/page_platform.dart b/lib/page_platform.dart index 852aedd..c6523ab 100644 --- a/lib/page_platform.dart +++ b/lib/page_platform.dart @@ -12,8 +12,12 @@ class NodeBasePlatformItem extends StatefulWidget { bool isEdit = false; bool isCreated = false; - NodeBasePlatformItem({Key key, this.item, this.fnRemove, this.fnSaveConfig}) - : super(key: key); + NodeBasePlatformItem({ + required this.item, + required this.fnRemove, + required this.fnSaveConfig, + super.key + }); @override _NodeBasePlatformItemState createState() => _NodeBasePlatformItemState(); @@ -142,7 +146,7 @@ class _NodeBasePlatformItemState extends State { } class NodeBasePlatformSettings extends StatefulWidget { - NodeBasePlatformSettings({Key key}) : super(key: key); + NodeBasePlatformSettings({super.key}); @override _NodeBasePlatformSettingsState createState() => _NodeBasePlatformSettingsState(); diff --git a/lib/page_platform_market.dart b/lib/page_platform_market.dart index 64589b8..f8469aa 100644 --- a/lib/page_platform_market.dart +++ b/lib/page_platform_market.dart @@ -19,9 +19,12 @@ class NodeBasePlatformMarketItem extends StatefulWidget { final Function(PlatformItem item) fnUninstall; final PlatformItem item; - NodeBasePlatformMarketItem( - {Key key, this.item, this.fnInstall, this.fnUninstall}) - : super(key: key); + NodeBasePlatformMarketItem({ + required this.item, + required this.fnInstall, + required this.fnUninstall, + super.key + }); @override _NodeBasePlatformMarketItemState createState() => @@ -57,7 +60,7 @@ class _NodeBasePlatformMarketItemState } class NodeBasePlatformMarket extends StatefulWidget { - NodeBasePlatformMarket({Key key}) : super(key: key); + NodeBasePlatformMarket({super.key}); @override _NodeBasePlatformMarketState createState() => _NodeBasePlatformMarketState(); } diff --git a/pubspec.lock b/pubspec.lock index 2118c9a..cc31797 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,65 +5,66 @@ packages: dependency: transitive description: name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" url: "https://pub.flutter-io.cn" source: hosted - version: "2.5.0" + version: "2.11.0" boolean_selector: dependency: transitive description: name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" url: "https://pub.flutter-io.cn" source: hosted - version: "2.1.0" + version: "2.1.1" characters: dependency: transitive description: name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.2.0" + version: "1.3.0" clock: dependency: transitive description: name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.0" + version: "1.1.1" collection: dependency: transitive description: name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.flutter-io.cn" source: hosted - version: "1.15.0" + version: "1.18.0" cupertino_icons: dependency: "direct main" description: name: cupertino_icons + sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d url: "https://pub.flutter-io.cn" source: hosted - version: "0.1.3" + version: "1.0.6" fake_async: dependency: transitive description: name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" url: "https://pub.flutter-io.cn" source: hosted - version: "1.2.0" - file: + version: "1.3.1" + ffi: dependency: transitive description: - name: file + name: ffi + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" url: "https://pub.flutter-io.cn" source: hosted - version: "5.2.1" + version: "2.1.0" flutter: dependency: "direct main" description: flutter @@ -73,9 +74,10 @@ packages: dependency: "direct main" description: name: flutter_archive + sha256: "004132780d382df5171589ab793e2efc9c3eef570fe72d78b4ccfbfbe52762ae" url: "https://pub.flutter-io.cn" source: hosted - version: "4.0.1" + version: "6.0.0" flutter_test: dependency: "direct dev" description: flutter @@ -85,100 +87,114 @@ packages: dependency: "direct main" description: name: http + sha256: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139 url: "https://pub.flutter-io.cn" source: hosted - version: "0.13.3" + version: "1.1.2" http_parser: dependency: transitive description: name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" url: "https://pub.flutter-io.cn" source: hosted - version: "4.0.0" - intl: + version: "4.0.2" + matcher: dependency: transitive description: - name: intl + name: matcher + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.flutter-io.cn" source: hosted - version: "0.16.1" - matcher: + version: "0.12.16" + material_color_utilities: dependency: transitive description: - name: matcher + name: material_color_utilities + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.flutter-io.cn" source: hosted - version: "0.12.10" + version: "0.5.0" meta: dependency: transitive description: name: meta + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e url: "https://pub.flutter-io.cn" source: hosted - version: "1.3.0" + version: "1.10.0" path: dependency: transitive description: name: path + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" url: "https://pub.flutter-io.cn" source: hosted - version: "1.8.0" + version: "1.8.3" path_provider: dependency: "direct main" description: name: path_provider + sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b url: "https://pub.flutter-io.cn" source: hosted - version: "1.6.14" - path_provider_linux: + version: "2.1.2" + path_provider_android: dependency: transitive description: - name: path_provider_linux + name: path_provider_android + sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" url: "https://pub.flutter-io.cn" source: hosted - version: "0.0.1+2" - path_provider_macos: + version: "2.2.2" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.2" + path_provider_linux: dependency: transitive description: - name: path_provider_macos + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 url: "https://pub.flutter-io.cn" source: hosted - version: "0.0.4+3" + version: "2.2.1" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.flutter-io.cn" source: hosted - version: "1.0.3" - pedantic: + version: "2.1.2" + path_provider_windows: dependency: transitive description: - name: pedantic + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" url: "https://pub.flutter-io.cn" source: hosted - version: "1.11.0" + version: "2.2.1" platform: dependency: transitive description: name: platform + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" url: "https://pub.flutter-io.cn" source: hosted - version: "2.2.1" + version: "3.1.4" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.flutter-io.cn" source: hosted - version: "1.0.2" - process: - dependency: transitive - description: - name: process - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.0.13" + version: "2.1.8" sky_engine: dependency: transitive description: flutter @@ -188,72 +204,122 @@ packages: dependency: transitive description: name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.flutter-io.cn" source: hosted - version: "1.8.0" + version: "1.10.0" stack_trace: dependency: transitive description: name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.flutter-io.cn" source: hosted - version: "1.10.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.flutter-io.cn" source: hosted - version: "2.1.0" + version: "2.1.2" string_scanner: dependency: transitive description: name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.0" + version: "1.2.0" term_glyph: dependency: transitive description: name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 url: "https://pub.flutter-io.cn" source: hosted - version: "1.2.0" + version: "1.2.1" test_api: dependency: transitive description: name: test_api + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.flutter-io.cn" source: hosted - version: "0.2.19" + version: "0.6.1" typed_data: dependency: transitive description: name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c url: "https://pub.flutter-io.cn" source: hosted - version: "1.3.0" + version: "1.3.2" vector_math: dependency: transitive description: name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" url: "https://pub.flutter-io.cn" source: hosted - version: "2.1.0" + version: "2.1.4" + web: + dependency: transitive + description: + name: web + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.0" webview_flutter: dependency: "direct main" description: name: webview_flutter + sha256: "71e1bfaef41016c8d5954291df5e9f8c6172f1f6ff3af01b5656456ddb11f94c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.4.4" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: "161af93c2abaf94ef2192bffb53a3658b2d721a3bf99b69aa1e47814ee18cc96" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.13.2" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: "80b40ae4fb959957eef9fa8970b6c9accda9f49fc45c2b75154696a8e8996cfe" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.9.1" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: "4d062ad505390ecef1c4bfb6001cd857a51e00912cc9dfb66edb1886a9ebd80c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.10.2" + win32: + dependency: transitive + description: + name: win32 + sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" url: "https://pub.flutter-io.cn" source: hosted - version: "0.3.22+1" + version: "5.2.0" xdg_directories: dependency: transitive description: name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d url: "https://pub.flutter-io.cn" source: hosted - version: "0.1.0" + version: "1.0.4" sdks: - dart: ">=2.12.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5" + dart: ">=3.2.0 <4.0.0" + flutter: ">=3.10.0" diff --git a/pubspec.yaml b/pubspec.yaml index 947631a..c493506 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,18 +18,18 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: ">=2.7.0 <3.0.0" + sdk: ">=2.18.0" dependencies: flutter: sdk: flutter - path_provider: ^1.6.14 - webview_flutter: ^0.3.22+1 + path_provider: ^2.1.2 + webview_flutter: ^4.4.4 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^0.1.3 - http: ^0.13.3 - flutter_archive: ^4.0.1 + cupertino_icons: ^1.0.6 + http: ^1.1.2 + flutter_archive: ^6.0.0 dev_dependencies: flutter_test: