├── .gitignore ├── .gitmodules ├── .metadata ├── README.md ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ ├── AndroidManifest.xml │ │ └── res │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-ldpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ └── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ ├── main │ │ ├── .gitignore │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── in │ │ │ │ └── canews │ │ │ │ └── pythonide │ │ │ │ ├── MainActivity.kt │ │ │ │ └── MyApplication.kt │ │ └── res │ │ │ ├── drawable-anydpi-v24 │ │ │ └── ic_bg_service_small.xml │ │ │ ├── drawable-hdpi │ │ │ └── ic_bg_service_small.png │ │ │ ├── drawable-mdpi │ │ │ └── ic_bg_service_small.png │ │ │ ├── drawable-xhdpi │ │ │ └── ic_bg_service_small.png │ │ │ ├── drawable-xxhdpi │ │ │ └── ic_bg_service_small.png │ │ │ ├── drawable │ │ │ ├── app_icon.png │ │ │ ├── launch_background.xml │ │ │ └── logo.png │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-ldpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ │ └── xml │ │ │ └── file_provider_paths.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle ├── settings_aar.gradle └── version.properties ├── assets ├── developers │ ├── canewsin.jpg │ └── pramukesh.jpg ├── fileicons │ ├── fileicons.json │ ├── git.svg │ ├── image.svg │ ├── json.svg │ ├── python.svg │ └── svg.svg ├── fonts │ └── lucidasansdemibold.ttf ├── icons │ ├── facebook.png │ ├── facebook_dark.png │ ├── github.png │ ├── github_dark.png │ ├── twitter.png │ └── twitter_dark.png ├── logo.png └── templates │ └── python │ ├── fibonacci.py │ ├── helloworld.py │ ├── listcomprehensions.py │ ├── looponlist.py │ ├── simplearithmetic.py │ └── templates.json ├── buildapk.bat ├── buildtools ├── compile.bat └── utils.dart ├── lib ├── controllers │ ├── process_controller.dart │ ├── project_controller.dart │ ├── project_file_controller.dart │ ├── project_view_controller.dart │ ├── purchases_controller.dart │ ├── ui_controller.dart │ └── var_controller.dart ├── imports.dart ├── imports_conflicts.dart ├── main.dart ├── models │ ├── enums.dart │ └── models.dart ├── others │ ├── common.dart │ ├── constants.dart │ ├── donation_const.dart │ ├── extensions.dart │ ├── native.dart │ ├── strings.dart │ └── utils.dart └── widgets │ ├── about_page.dart │ ├── common.dart │ ├── home_page.dart │ ├── loading_page.dart │ ├── log_page.dart │ ├── project_view_page.dart │ └── settings_page.dart └── pubspec.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | .vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | /build/ 32 | libOld/ 33 | .project 34 | .classpath 35 | *.prefs 36 | pubspec.lock 37 | 38 | # Web related 39 | lib/generated_plugin_registrant.dart 40 | 41 | # Exceptions to above rules. 42 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 43 | 44 | # Android related 45 | # **/android/**/gradle-wrapper.jar 46 | **/android/.gradle 47 | **/android/captures/ 48 | # **/android/gradlew 49 | # **/android/gradlew.bat 50 | **/android/local.properties 51 | **/android/key.properties 52 | **/android/**/GeneratedPluginRegistrant.java 53 | 54 | # iOS/XCode related 55 | **/ios/**/*.mode1v3 56 | **/ios/**/*.mode2v3 57 | **/ios/**/*.moved-aside 58 | **/ios/**/*.pbxuser 59 | **/ios/**/*.perspectivev3 60 | **/ios/**/*sync/ 61 | **/ios/**/.sconsign.dblite 62 | **/ios/**/.tags* 63 | **/ios/**/.vagrant/ 64 | **/ios/**/DerivedData/ 65 | **/ios/**/Icon? 66 | **/ios/**/Pods/ 67 | **/ios/**/.symlinks/ 68 | **/ios/**/profile 69 | **/ios/**/xcuserdata 70 | **/ios/.generated/ 71 | **/ios/Flutter/App.framework 72 | **/ios/Flutter/Flutter.framework 73 | **/ios/Flutter/Generated.xcconfig 74 | **/ios/Flutter/app.flx 75 | **/ios/Flutter/app.zip 76 | **/ios/Flutter/flutter_assets/ 77 | **/ios/ServiceDefinitions.json 78 | **/ios/Runner/GeneratedPluginRegistrant.* 79 | 80 | # Exceptions to above rules. 81 | !**/ios/**/default.mode1v3 82 | !**/ios/**/default.mode2v3 83 | !**/ios/**/default.pbxuser 84 | !**/ios/**/default.perspectivev3 85 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 86 | 87 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "android/arm_python"] 2 | path = android/arm_python 3 | url = https://github.com/canewsin/arm_python.git 4 | [submodule "android/arm64_python"] 5 | path = android/arm64_python 6 | url = https://github.com/canewsin/arm64_python.git 7 | [submodule "android/x86_64_python"] 8 | path = android/x86_64_python 9 | url = https://github.com/canewsin/x86_64_python.git 10 | [submodule "android/x86_python"] 11 | path = android/x86_python 12 | url = https://github.com/canewsin/x86_python.git 13 | [submodule "android/common_python"] 14 | path = android/common_python 15 | url = https://github.com/canewsin/common_python.git 16 | [submodule "android/nativelibs_python"] 17 | path = android/nativelibs_python 18 | url = https://github.com/canewsin/nativelibs_python.git 19 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 27321ebbad34b0a3fafe99fac037102196d655ff 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python IDE Mobile - IDE for Python 3 2 | 3 | 4 | Python IDE Mobile is a simple yet fully featured Python3 IDE without needing PC Setup for your python 3 projects. 5 | 6 | [Download from Google Play](https://play.google.com/store/apps/details?id=in.canews.pythonide) 9 | 10 | ## Installation 11 | 12 | ### From Google PlayStore : 13 | #### Android (arm, arm64, x86) 14 | - minimum Android version supported 16 (JellyBean). 15 | - Google Play Store Link https://play.google.com/store/apps/details?id=in.canews.pythonide 16 | 17 | #### Compiling Source : 18 | 19 | You need Flutter Framework to compile this App from Source. 20 | 21 | #### Installing Flutter : https://flutter.dev/docs/get-started/install 22 | 23 | ``` 24 | git clone https://github.com/canewsin/pythonide.git 25 | cd pythonide 26 | flutter packages get 27 | ``` 28 | 29 | After that create a file named `key.properties` in `android` directory 30 | and fill the below details, which are in capital letters, with your details. 31 | ``` 32 | storeFile=ANDROID_KEY_STORE_FILE_PATH 33 | storePassword=KEY_STORE_PASSWORD 34 | keyAlias=KEY_ALIAS 35 | keyPassword=KEY_PASSWORD 36 | ``` 37 | 38 | in root folder 39 | 40 | to build apk 41 | ``` 42 | flutter build apk --no-shrink 43 | ``` 44 | 45 | to build appbundle 46 | ``` 47 | flutter build appbundle --no-shrink 48 | ``` 49 | 50 | to run the app in Android Device / Emulator 51 | 52 | ``` 53 | flutter run 54 | ``` 55 | 56 | ## Donate 57 | BTC(Preferred) : 58 | 59 | `1ZeroNetyV5mKY9JF1gsm82TuBXHpfdLX` 60 | 61 | ETH : 62 | 63 | `0xa81a32dcce8e5bcb9792daa19ae7f964699ee536` 64 | 65 | UPI(Indian Users) : 66 | 67 | `pramukesh@upi` 68 | 69 | Liberapay : 70 | 71 | `https://liberapay.com/canews.in/donate` 72 | 73 | ## Contribute 74 | If you want to support project's further development, you can contribute your time or money, If you want to contribute money you can send bitcoin or other supported crypto currencies to above addresses or buy in-app purchases, if want to contribute translations or code. 75 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | !gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | # /gradlew 5 | # /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | /build/ 9 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def versionProperties = new Properties() 10 | def versionPropertiesFile = rootProject.file('version.properties') 11 | if (versionPropertiesFile.exists()) { 12 | versionPropertiesFile.withReader('UTF-8') { reader -> 13 | versionProperties.load(reader) 14 | } 15 | } 16 | 17 | def flutterRoot = localProperties.getProperty('flutter.sdk') 18 | if (flutterRoot == null) { 19 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 20 | } 21 | 22 | def flutterVersionCode = versionProperties.getProperty('flutter.versionCode') 23 | if (flutterVersionCode == null) { 24 | flutterVersionCode = '1' 25 | } 26 | 27 | def flutterVersionName = versionProperties.getProperty('flutter.versionName') 28 | if (flutterVersionName == null) { 29 | flutterVersionName = '1.0' 30 | } 31 | def keystoreProperties = new Properties() 32 | def keystorePropertiesFile = rootProject.file('key.properties') 33 | if (keystorePropertiesFile.exists()) { 34 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 35 | } 36 | apply plugin: 'com.android.application' 37 | apply plugin: 'kotlin-android' 38 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 39 | apply plugin: 'com.jeppeman.locallydynamic' 40 | 41 | android { 42 | compileSdkVersion 31 43 | 44 | sourceSets { 45 | main.java.srcDirs += 'src/main/kotlin' 46 | } 47 | 48 | lintOptions { 49 | disable 'InvalidPackage' 50 | } 51 | 52 | defaultConfig { 53 | applicationId "in.canews.pythonide" 54 | minSdkVersion 21 55 | targetSdkVersion 31 56 | versionCode flutterVersionCode.toInteger() 57 | versionName flutterVersionName 58 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 59 | } 60 | 61 | signingConfigs { 62 | release { 63 | keyAlias keystoreProperties['keyAlias'] 64 | keyPassword keystoreProperties['keyPassword'] 65 | storeFile file(keystoreProperties['storeFile']) 66 | storePassword keystoreProperties['storePassword'] 67 | } 68 | } 69 | 70 | buildTypes { 71 | 72 | release { 73 | signingConfig signingConfigs.release 74 | minifyEnabled = false 75 | shrinkResources = false 76 | } 77 | 78 | debug { 79 | applicationIdSuffix '.debug' 80 | signingConfig signingConfigs.release 81 | locallyDynamic { 82 | enabled = true 83 | throttleDownloadBy = 1000 84 | } 85 | defaultConfig.ndk.abiFilters 'armeabi-v7a','arm64-v8a','x86_64' 86 | } 87 | } 88 | dynamicFeatures = [ 89 | ":nativelibs_python", 90 | ":arm64_python", ":arm_python", ":common_python", ":x86_python", ":x86_64_python" 91 | ] 92 | 93 | 94 | } 95 | 96 | flutter { 97 | source '../..' 98 | } 99 | 100 | dependencies { 101 | def billing_version = "3.0.0" 102 | implementation "com.android.billingclient:billing:4.0.0" 103 | 104 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 105 | implementation 'com.google.android.gms:play-services-base:17.6.0' 106 | 107 | //Duplicate Classes Issue https://stackoverflow.com/a/60492942 108 | implementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava' 109 | 110 | debugImplementation 'com.jeppeman.locallydynamic:locallydynamic-debug:0.3' 111 | releaseImplementation 'com.jeppeman.locallydynamic:locallydynamic:0.3' 112 | 113 | testImplementation 'junit:junit:4.13.2' 114 | androidTestImplementation 'androidx.test:runner:1.3.0' 115 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' 116 | } 117 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canewsin/python_ide/3e7482fbe113ab6ee592a1768448a3b7e3947404/android/app/src/debug/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-ldpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canewsin/python_ide/3e7482fbe113ab6ee592a1768448a3b7e3947404/android/app/src/debug/res/mipmap-ldpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canewsin/python_ide/3e7482fbe113ab6ee592a1768448a3b7e3947404/android/app/src/debug/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canewsin/python_ide/3e7482fbe113ab6ee592a1768448a3b7e3947404/android/app/src/debug/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canewsin/python_ide/3e7482fbe113ab6ee592a1768448a3b7e3947404/android/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canewsin/python_ide/3e7482fbe113ab6ee592a1768448a3b7e3947404/android/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/.gitignore: -------------------------------------------------------------------------------- 1 | jniLibs/ -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 24 | 32 | 33 | 34 | 35 | 36 | 37 | 39 | 42 | 43 | 48 | 49 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/in/canews/pythonide/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package `in`.canews.pythonide 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Activity 5 | import android.app.PendingIntent 6 | import android.content.Context 7 | import android.content.Intent 8 | import android.content.pm.PackageInfo 9 | import android.graphics.Bitmap 10 | import android.graphics.BitmapFactory 11 | import android.net.Uri 12 | import android.os.Build 13 | import android.os.Bundle 14 | import android.os.PowerManager 15 | import android.provider.Settings 16 | import android.util.Log 17 | import androidx.annotation.NonNull 18 | import androidx.core.app.ShareCompat 19 | import androidx.core.content.FileProvider 20 | import androidx.core.content.pm.ShortcutInfoCompat 21 | import androidx.core.content.pm.ShortcutManagerCompat 22 | import androidx.core.graphics.drawable.IconCompat 23 | import com.google.android.gms.common.ConnectionResult 24 | import com.google.android.gms.common.GoogleApiAvailability 25 | import com.google.android.play.core.splitinstall.SplitInstallManager 26 | import com.google.android.play.core.splitinstall.SplitInstallRequest 27 | import com.google.android.play.core.splitinstall.model.SplitInstallSessionStatus 28 | import com.jeppeman.locallydynamic.LocallyDynamicSplitInstallManagerFactory 29 | import io.flutter.embedding.android.FlutterActivity 30 | import io.flutter.embedding.engine.FlutterEngine 31 | import io.flutter.plugin.common.EventChannel 32 | import io.flutter.plugin.common.EventChannel.StreamHandler 33 | import io.flutter.plugin.common.MethodChannel 34 | import io.flutter.plugins.GeneratedPluginRegistrant 35 | import java.io.File 36 | import java.io.IOException 37 | import java.io.InputStream 38 | 39 | 40 | const val BATTERY_OPTIMISATION_RESULT_CODE = 1001 41 | const val PICK_USERJSON_FILE = 1002 42 | const val SAVE_USERJSON_FILE = 1003 43 | const val PICK_ZIP_FILE = 1004 44 | const val TAG = "MainActivity" 45 | const val CHANNEL = "in.canews.pythonide" 46 | const val EVENT_CHANNEL = "in.canews.pythonide/installModules" 47 | 48 | class MainActivity : FlutterActivity() { 49 | 50 | private var archName = "" 51 | private var splitInstallManager: SplitInstallManager? = null 52 | private lateinit var result: MethodChannel.Result 53 | private var mSessionId = -1 54 | private var mLaunchShortcutUrl = "" 55 | 56 | override fun onCreate(savedInstanceState: Bundle?) { 57 | super.onCreate(savedInstanceState) 58 | } 59 | 60 | override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { 61 | GeneratedPluginRegistrant.registerWith(flutterEngine) 62 | MethodChannel(flutterEngine?.dartExecutor, CHANNEL).setMethodCallHandler { call, result -> 63 | when (call.method) { 64 | "batteryOptimisations" -> getBatteryOptimizations(result) 65 | "copyAssetsToCache" -> result.success(copyAssetsToCache()) 66 | "getAppInstallTime" -> getAppInstallTime(result) 67 | "getAppLastUpdateTime" -> getAppLastUpdateTime(result) 68 | "isBatteryOptimized" -> isBatteryOptimized(result) 69 | "isPlayStoreInstall" -> result.success(isPlayStoreInstall(this)) 70 | "initSplitInstall" -> { 71 | if (splitInstallManager == null) 72 | splitInstallManager = LocallyDynamicSplitInstallManagerFactory.create(this) 73 | result.success(true) 74 | } 75 | "isModuleInstallSupported" -> result.success(isModuleInstallSupported()) 76 | "isRequiredModulesInstalled" -> result.success(isRequiredModulesInstalled()) 77 | "moveTaskToBack" -> { 78 | moveTaskToBack(true) 79 | result.success(true) 80 | } 81 | "nativeDir" -> result.success(applicationInfo.nativeLibraryDir) 82 | "nativePrint" -> { 83 | Log.e("Flutter>nativePrint()",call.arguments()) 84 | } 85 | "openJsonFile" -> openJsonFile(result) 86 | "openZipFile" -> openZipFile(result) 87 | "readJsonFromUri" -> readJsonFromUri(call.arguments.toString(), result) 88 | "readZipFromUri" -> readZipFromUri(call.arguments.toString(), result) 89 | "uninstallModules" -> uninstallModules() 90 | } 91 | } 92 | EventChannel(flutterEngine.dartExecutor, EVENT_CHANNEL).setStreamHandler( 93 | object : StreamHandler { 94 | lateinit var events: EventChannel.EventSink 95 | override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { 96 | getArchName() 97 | loadAndLaunchModule(archName, events) 98 | } 99 | 100 | override fun onCancel(arguments: Any?) { 101 | events.endOfStream() 102 | } 103 | } 104 | ) 105 | } 106 | 107 | private fun getAppInstallTime(result: MethodChannel.Result) { 108 | val info = context.packageManager.getPackageInfo(context.packageName,0); 109 | val field = PackageInfo::class.java.getField("firstInstallTime") 110 | val timeStamp = field.getLong(info) 111 | result.success(timeStamp.toString()) 112 | } 113 | 114 | private fun getAppLastUpdateTime(result: MethodChannel.Result) { 115 | val info = context.packageManager.getPackageInfo(context.packageName,0); 116 | val field = PackageInfo::class.java.getField("lastUpdateTime") 117 | val timeStamp = field.getLong(info) 118 | result.success(timeStamp.toString()) 119 | } 120 | 121 | private fun isPlayStoreInstall(context: Context): Boolean { 122 | val validInstallers: List = listOf("com.android.vending", "com.google.android.feedback") 123 | val installer = context.packageManager.getInstallerPackageName(context.packageName) 124 | 125 | return installer != null && validInstallers.contains(installer) 126 | } 127 | 128 | private fun isBatteryOptimized(result: MethodChannel.Result) { 129 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 130 | val packageName = packageName 131 | val pm = getSystemService(Context.POWER_SERVICE) as PowerManager 132 | result.success(pm.isIgnoringBatteryOptimizations(packageName)) 133 | } else { 134 | result.success(false) 135 | } 136 | 137 | } 138 | 139 | @SuppressLint("BatteryLife") 140 | private fun getBatteryOptimizations(resultT: MethodChannel.Result) { 141 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 142 | val intent = Intent() 143 | val pm = getSystemService(Context.POWER_SERVICE) as PowerManager 144 | if (!pm.isIgnoringBatteryOptimizations(packageName)) { 145 | intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS 146 | intent.data = Uri.parse("package:$packageName") 147 | startActivityForResult(intent, BATTERY_OPTIMISATION_RESULT_CODE) 148 | result = resultT 149 | } else { 150 | resultT.success(true) 151 | } 152 | } 153 | } 154 | 155 | override fun onActivityResult( 156 | requestCode: Int, 157 | resultCode: Int, 158 | data: Intent? 159 | ) { 160 | if (requestCode == BATTERY_OPTIMISATION_RESULT_CODE) { 161 | if (resultCode == Activity.RESULT_OK) { 162 | resultSuccess(true) 163 | } else { 164 | resultSuccess(false) 165 | } 166 | } else if (requestCode == SAVE_USERJSON_FILE) { 167 | if (resultCode == Activity.RESULT_OK) { 168 | resultSuccess("successfully saved users.json file") 169 | } else { 170 | resultSuccess("failed to save file") 171 | } 172 | } else if (requestCode == PICK_USERJSON_FILE) { 173 | if (resultCode == Activity.RESULT_OK) { 174 | if (data?.data != null) { 175 | resultSuccess(data.data.toString()) 176 | } 177 | } else { 178 | result.error("526", "Error Picking User Json File", "Error Picking User Json File") 179 | } 180 | } else if (requestCode == PICK_ZIP_FILE) { 181 | if (resultCode == Activity.RESULT_OK) { 182 | if (data?.data != null) { 183 | resultSuccess(data.data.toString()) 184 | } 185 | } else { 186 | result.error("527", "Error Picking Plugin File", "Error Picking Plugin File") 187 | } 188 | } 189 | super.onActivityResult(requestCode, resultCode, data) 190 | } 191 | 192 | private fun resultSuccess(msg : Any) { 193 | result.runCatching { 194 | success(msg) 195 | }.onFailure { 196 | if (it is IllegalStateException) { 197 | Log.e("MainActivity>resultSuc>", it.message!!) 198 | } 199 | } 200 | } 201 | 202 | private fun openZipFile( 203 | resultT: MethodChannel.Result 204 | ) = 205 | openFileIntent( 206 | resultT, 207 | Intent.ACTION_OPEN_DOCUMENT, 208 | "application/zip", 209 | PICK_ZIP_FILE 210 | ) 211 | 212 | private fun openJsonFile( 213 | resultT: MethodChannel.Result 214 | ) = 215 | openFileIntent( 216 | resultT, 217 | Intent.ACTION_OPEN_DOCUMENT, 218 | "application/json", 219 | PICK_USERJSON_FILE 220 | ) 221 | 222 | private fun openFileIntent( 223 | resultT: MethodChannel.Result, 224 | intentAction: String, 225 | intentType: String, 226 | intentCode: Int 227 | ) { 228 | val intent = Intent(intentAction).apply { 229 | addCategory(Intent.CATEGORY_OPENABLE) 230 | type = intentType 231 | result = resultT 232 | } 233 | startActivityForResult(intent, intentCode) 234 | } 235 | 236 | private fun readJsonFromUri(path: String, resultT: MethodChannel.Result) = copyFileToTempPath(path, resultT, "/users.json") 237 | 238 | private fun readZipFromUri(path: String, resultT: MethodChannel.Result) = copyFileToTempPath(path, resultT, "/plugin.zip") 239 | 240 | @Throws(IOException::class) 241 | private fun copyFileToTempPath( 242 | path: String?, 243 | resultT: MethodChannel.Result? = null, 244 | filename: String, 245 | inputStreamA: InputStream? = null 246 | ) { 247 | var inputstream: InputStream? = null 248 | if (inputStreamA != null) { 249 | inputstream = inputStreamA 250 | } else { 251 | if (path != null) 252 | if (path.startsWith("content://")) { 253 | inputstream = contentResolver.openInputStream(Uri.parse(path)) 254 | } else if (path.startsWith("/")) { 255 | inputstream = File(path).inputStream() 256 | } 257 | } 258 | inputstream.use { inputStream -> 259 | val tempFilePath = cacheDir.path + "/" + filename 260 | val tempFile = File(tempFilePath) 261 | if (tempFile.exists()) tempFile.delete() 262 | tempFile.createNewFile() 263 | inputStream?.toFile(tempFilePath) 264 | resultT?.success(File(tempFilePath).absoluteFile.absolutePath) 265 | tempFile.deleteOnExit() 266 | } 267 | } 268 | 269 | private fun InputStream.toFile(path: String) { 270 | use { input -> 271 | File(path).outputStream().use { input.copyTo(it) } 272 | } 273 | } 274 | 275 | private fun copyAssetsToCache(): Boolean { 276 | getArchName() 277 | try { 278 | if (splitInstallManager?.installedModules!!.contains("common_python")) { 279 | getAssetFiles("site_packages_common.zip") 280 | } 281 | if (splitInstallManager?.installedModules!!.contains(archName + "_python")) { 282 | getAssetFiles("python38_$archName.zip") 283 | } 284 | } catch (e: IOException) { 285 | return false 286 | } 287 | return true 288 | } 289 | 290 | private fun getAssetFiles(fileName: String) { 291 | val assetManager = createPackageContext(packageName, 0).assets 292 | val assistContent = assetManager.open(fileName) 293 | copyFileToTempPath(inputStreamA = assistContent, filename = fileName, path = null) 294 | } 295 | 296 | private fun isModuleInstallSupported(): Boolean = 297 | Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP 298 | && isGooglePlayServicesAvailable(this) 299 | 300 | private fun isGooglePlayServicesAvailable(activity: Activity?): Boolean { 301 | val googleApiAvailability: GoogleApiAvailability = GoogleApiAvailability.getInstance() 302 | val status: Int = googleApiAvailability.isGooglePlayServicesAvailable(activity) 303 | if (status != ConnectionResult.SUCCESS) { 304 | // if (googleApiAvailability.isUserResolvableError(status)) { 305 | // googleApiAvailability.getErrorDialog(activity, status, 2404).show() 306 | // } 307 | return false 308 | } 309 | return true 310 | } 311 | 312 | private fun getArchName() { 313 | val arch = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 314 | Build.SUPPORTED_ABIS 315 | } else { 316 | TODO("VERSION.SDK_INT < LOLLIPOP") 317 | } 318 | if (archName.isEmpty()) 319 | archName = if (arch.contains("arm64-v8a")) { 320 | "arm64" 321 | } else if (arch.contains("armeabi-v7a")) { 322 | "arm" 323 | } else if (arch.contains("x86_64")) { 324 | "x86_64" 325 | } else { 326 | "x86" 327 | } 328 | } 329 | 330 | /** 331 | * Load a feature by module name. 332 | * @param name The name of the feature module to load. 333 | */ 334 | private fun loadAndLaunchModule(name: String, eventSink: EventChannel.EventSink?) { 335 | if (isModuleInstalled(name) == true) 336 | return 337 | val request = SplitInstallRequest.newBuilder() 338 | .addModule(name + "_python") 339 | .addModule("common_python") 340 | .build() 341 | splitInstallManager?.startInstall(request)?.addOnSuccessListener { sessionId -> 342 | mSessionId = sessionId 343 | } 344 | splitInstallManager?.registerListener { state -> 345 | if (state.sessionId() == mSessionId) { 346 | when (state.status()) { 347 | SplitInstallSessionStatus.DOWNLOADING -> { 348 | val msg = """ 349 | { 350 | "status" : ${state.status()}, 351 | "downloaded" : ${state.bytesDownloaded()}, 352 | "total" : ${state.totalBytesToDownload()} 353 | } 354 | """.trimIndent() 355 | eventSink?.success(msg) 356 | } 357 | SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION -> { 358 | startIntentSender(state.resolutionIntent()?.intentSender, null, 0, 0, 0) 359 | } 360 | else -> { 361 | val msg = """ 362 | { 363 | "status" : ${state.status()} 364 | } 365 | """.trimIndent() 366 | eventSink?.success(msg) 367 | } 368 | } 369 | } 370 | } 371 | } 372 | 373 | private fun isModuleInstalled(name: String): Boolean? = 374 | splitInstallManager?.installedModules?.contains(name) 375 | 376 | private fun isRequiredModulesInstalled(): Boolean = isModuleInstalled("common_python") == true && 377 | isModuleInstalled(archName + "_python") == true 378 | 379 | private fun uninstallModules() { 380 | val installedModules = splitInstallManager?.installedModules?.toList() 381 | splitInstallManager?.deferredUninstall(listOf("common_python", archName + "_python"))?.addOnSuccessListener { 382 | Log.d("SplitModuleUninstall:>:","Uninstalling $installedModules") 383 | } 384 | context.cacheDir.deleteRecursively() 385 | } 386 | } 387 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/in/canews/pythonide/MyApplication.kt: -------------------------------------------------------------------------------- 1 | package `in`.canews.pythonide 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import com.google.android.play.core.splitcompat.SplitCompat 6 | import com.google.android.play.core.splitcompat.SplitCompatApplication 7 | import io.flutter.FlutterInjector 8 | 9 | 10 | internal class MyApplication : SplitCompatApplication() { 11 | 12 | override fun onCreate() { 13 | super.onCreate() 14 | FlutterInjector.instance().flutterLoader().startInitialization(this) 15 | } 16 | 17 | private var mCurrentActivity: Activity? = null 18 | 19 | fun getCurrentActivity(): Activity? { 20 | return mCurrentActivity 21 | } 22 | 23 | fun setCurrentActivity(mCurrentActivity: Activity?) { 24 | this.mCurrentActivity = mCurrentActivity 25 | } 26 | 27 | override fun attachBaseContext(base: Context?) { 28 | super.attachBaseContext(base) 29 | SplitCompat.install(base!!) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-anydpi-v24/ic_bg_service_small.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 7 | 9 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_bg_service_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canewsin/python_ide/3e7482fbe113ab6ee592a1768448a3b7e3947404/android/app/src/main/res/drawable-hdpi/ic_bg_service_small.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_bg_service_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canewsin/python_ide/3e7482fbe113ab6ee592a1768448a3b7e3947404/android/app/src/main/res/drawable-mdpi/ic_bg_service_small.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_bg_service_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canewsin/python_ide/3e7482fbe113ab6ee592a1768448a3b7e3947404/android/app/src/main/res/drawable-xhdpi/ic_bg_service_small.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_bg_service_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canewsin/python_ide/3e7482fbe113ab6ee592a1768448a3b7e3947404/android/app/src/main/res/drawable-xxhdpi/ic_bg_service_small.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canewsin/python_ide/3e7482fbe113ab6ee592a1768448a3b7e3947404/android/app/src/main/res/drawable/app_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 16 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canewsin/python_ide/3e7482fbe113ab6ee592a1768448a3b7e3947404/android/app/src/main/res/drawable/logo.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canewsin/python_ide/3e7482fbe113ab6ee592a1768448a3b7e3947404/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-ldpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canewsin/python_ide/3e7482fbe113ab6ee592a1768448a3b7e3947404/android/app/src/main/res/mipmap-ldpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canewsin/python_ide/3e7482fbe113ab6ee592a1768448a3b7e3947404/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canewsin/python_ide/3e7482fbe113ab6ee592a1768448a3b7e3947404/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canewsin/python_ide/3e7482fbe113ab6ee592a1768448a3b7e3947404/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canewsin/python_ide/3e7482fbe113ab6ee592a1768448a3b7e3947404/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Arm64 Binaries 3 | Arm Binaries 4 | Common Binaries 5 | Native Binaries 6 | x86 Binaries 7 | x86_64 Binaries 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/res/xml/file_provider_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.5.0' 3 | ext.gradle_version = '4.2.0' 4 | repositories { 5 | google() 6 | mavenCentral() 7 | maven { 8 | url "https://plugins.gradle.org/m2" 9 | } 10 | } 11 | 12 | dependencies { 13 | classpath "com.android.tools.build:gradle:${gradle_version}" 14 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 15 | classpath "com.jeppeman.locallydynamic.gradle:plugin:0.2" 16 | } 17 | } 18 | 19 | allprojects { 20 | repositories { 21 | google() 22 | mavenCentral() 23 | } 24 | } 25 | 26 | rootProject.buildDir = '../build' 27 | subprojects { 28 | project.buildDir = "${rootProject.buildDir}/${project.name}" 29 | } 30 | subprojects { 31 | project.evaluationDependsOn(':app') 32 | } 33 | 34 | task clean(type: Delete) { 35 | delete rootProject.buildDir 36 | } 37 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4096M 2 | # TODO : Once Binaries are compiled with new NDK than NDK 21 RC1 3 | # we can remove below uncompressed property 4 | # https://stackoverflow.com/a/55969995 5 | android.bundle.enableUncompressedNativeLibs=false 6 | android.experimental.enableNewResourceShrinker=true 7 | android.enableR8=true 8 | android.useAndroidX=true 9 | android.enableJetifier=true 10 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canewsin/python_ide/3e7482fbe113ab6ee592a1768448a3b7e3947404/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=$((i+1)) 158 | done 159 | case $i in 160 | (0) set -- ;; 161 | (1) set -- "$args0" ;; 162 | (2) set -- "$args0" "$args1" ;; 163 | (3) set -- "$args0" "$args1" "$args2" ;; 164 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=$(save "$@") 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 184 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 185 | cd "$(dirname "$0")" 186 | fi 187 | 188 | exec "$JAVACMD" "$@" 189 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | include ':common_python' 3 | include ':nativelibs_python' 4 | include ':arm64_python' 5 | include ':arm_python' 6 | include ':x86_python' 7 | include ':x86_64_python' 8 | 9 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 10 | 11 | def plugins = new Properties() 12 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 13 | if (pluginsFile.exists()) { 14 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 15 | } 16 | 17 | plugins.each { name, path -> 18 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 19 | include ":$name" 20 | project(":$name").projectDir = pluginDirectory 21 | } 22 | -------------------------------------------------------------------------------- /android/settings_aar.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /android/version.properties: -------------------------------------------------------------------------------- 1 | flutter.versionName=v 0.1.7 2 | flutter.versionCode=17 3 | -------------------------------------------------------------------------------- /assets/developers/canewsin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canewsin/python_ide/3e7482fbe113ab6ee592a1768448a3b7e3947404/assets/developers/canewsin.jpg -------------------------------------------------------------------------------- /assets/developers/pramukesh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canewsin/python_ide/3e7482fbe113ab6ee592a1768448a3b7e3947404/assets/developers/pramukesh.jpg -------------------------------------------------------------------------------- /assets/fileicons/fileicons.json: -------------------------------------------------------------------------------- 1 | { 2 | "git": { 3 | "fileName": "git.svg", 4 | "supportedExt": [ 5 | "/.git" 6 | ] 7 | }, 8 | "image": { 9 | "fileName": "image.svg", 10 | "supportedExt": [ 11 | "jpeg", 12 | "png" 13 | ] 14 | }, 15 | "json": { 16 | "fileName": "json.svg", 17 | "supportedExt": [ 18 | "json", 19 | "props" 20 | ] 21 | }, 22 | "python": { 23 | "fileName": "python.svg", 24 | "supportedExt": [ 25 | "py" 26 | ] 27 | }, 28 | "svg": { 29 | "fileName": "svg.svg", 30 | "supportedExt": [ 31 | "svg" 32 | ] 33 | } 34 | } -------------------------------------------------------------------------------- /assets/fileicons/git.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/fileicons/image.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/fileicons/json.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/fileicons/python.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /assets/fileicons/svg.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/fonts/lucidasansdemibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canewsin/python_ide/3e7482fbe113ab6ee592a1768448a3b7e3947404/assets/fonts/lucidasansdemibold.ttf -------------------------------------------------------------------------------- /assets/icons/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canewsin/python_ide/3e7482fbe113ab6ee592a1768448a3b7e3947404/assets/icons/facebook.png -------------------------------------------------------------------------------- /assets/icons/facebook_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canewsin/python_ide/3e7482fbe113ab6ee592a1768448a3b7e3947404/assets/icons/facebook_dark.png -------------------------------------------------------------------------------- /assets/icons/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canewsin/python_ide/3e7482fbe113ab6ee592a1768448a3b7e3947404/assets/icons/github.png -------------------------------------------------------------------------------- /assets/icons/github_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canewsin/python_ide/3e7482fbe113ab6ee592a1768448a3b7e3947404/assets/icons/github_dark.png -------------------------------------------------------------------------------- /assets/icons/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canewsin/python_ide/3e7482fbe113ab6ee592a1768448a3b7e3947404/assets/icons/twitter.png -------------------------------------------------------------------------------- /assets/icons/twitter_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canewsin/python_ide/3e7482fbe113ab6ee592a1768448a3b7e3947404/assets/icons/twitter_dark.png -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canewsin/python_ide/3e7482fbe113ab6ee592a1768448a3b7e3947404/assets/logo.png -------------------------------------------------------------------------------- /assets/templates/python/fibonacci.py: -------------------------------------------------------------------------------- 1 | # Python 3: Fibonacci series up to n 2 | def fib(n): 3 | a, b = 0, 1 4 | while a < n: 5 | print(a, end=' ') 6 | a, b = b, a+b 7 | print() 8 | print(fib(1000)) -------------------------------------------------------------------------------- /assets/templates/python/helloworld.py: -------------------------------------------------------------------------------- 1 | print('Hello World!') 2 | #Output 3 | #Hello World! -------------------------------------------------------------------------------- /assets/templates/python/listcomprehensions.py: -------------------------------------------------------------------------------- 1 | # Python 3: List comprehensions 2 | fruits = ['Banana', 'Apple', 'Lime'] 3 | loud_fruits = [fruit.upper() for fruit in fruits] 4 | print(loud_fruits) 5 | #Output 6 | # ['BANANA', 'APPLE', 'LIME'] 7 | 8 | # List and the enumerate function 9 | print(list(enumerate(fruits))) 10 | #Output 11 | # [(0, 'Banana'), (1, 'Apple'), (2, 'Lime')] -------------------------------------------------------------------------------- /assets/templates/python/looponlist.py: -------------------------------------------------------------------------------- 1 | # For loop on a list 2 | numbers = [2, 4, 6, 8] 3 | product = 1 4 | for number in numbers: 5 | product = product * number 6 | 7 | print('The product is:', product) 8 | 9 | #Output 10 | #The product is: 384 -------------------------------------------------------------------------------- /assets/templates/python/simplearithmetic.py: -------------------------------------------------------------------------------- 1 | # Python 3: Simple arithmetic 2 | print(1 / 2) 3 | #Output 4 | # 0.5 5 | print(2 ** 3) 6 | #Output 7 | # 8 8 | print(17 / 3) # classic division returns a float 9 | #Output 10 | # 5.666666666666667 11 | print(17 // 3) # floor division 12 | #Output 13 | # 5 -------------------------------------------------------------------------------- /assets/templates/python/templates.json: -------------------------------------------------------------------------------- 1 | { 2 | "HelloWorld": { 3 | "fileName": "helloworld.py", 4 | "source": "Python WebSite HomePage" 5 | }, 6 | "Fibonacci": { 7 | "fileName": "fibonacci.py", 8 | "source": "Python WebSite HomePage" 9 | }, 10 | "List Comprehensions": { 11 | "fileName": "listcomprehensions.py", 12 | "source": "Python WebSite HomePage" 13 | }, 14 | "Loop On List": { 15 | "fileName": "looponlist.py", 16 | "source": "Python WebSite HomePage" 17 | }, 18 | "Simple Arithematic": { 19 | "fileName": "simplearithmetic.py", 20 | "source": "Python WebSite HomePage" 21 | } 22 | } -------------------------------------------------------------------------------- /buildapk.bat: -------------------------------------------------------------------------------- 1 | dart buildtools\utils.dart compile -------------------------------------------------------------------------------- /buildtools/compile.bat: -------------------------------------------------------------------------------- 1 | flutter build apk -------------------------------------------------------------------------------- /buildtools/utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | main(List args) { 5 | String arg = (args.isEmpty) ? '' : args[0]; 6 | switch (arg) { 7 | case 'modules': 8 | modules(); 9 | break; 10 | case 'nonpy': 11 | removeNonPy(); 12 | break; 13 | case 'compile': 14 | compile(); 15 | break; 16 | case 'update': 17 | updateZeroNetCode(); 18 | break; 19 | default: 20 | compile(); 21 | } 22 | } 23 | 24 | updateZeroNetCode() { 25 | var zeronetPath = Directory.current.path + '/build/ZeroNet-py3'; 26 | if (Directory(zeronetPath).existsSync()) { 27 | print('Deleting ZeroNet Git Repo'); 28 | Directory(zeronetPath).deleteSync(recursive: true); 29 | } 30 | var process = Process.runSync('git', [ 31 | 'clone', 32 | 'https://github.com/HelloZeroNet/ZeroNet.git', 33 | '--depth=1', 34 | zeronetPath, 35 | ]); 36 | if (process.exitCode == 0) { 37 | print('Successfully downloaded Zeronet Repo'); 38 | 39 | print('Deleting git history'); 40 | Directory(zeronetPath + '/.git').deleteSync(recursive: true); 41 | 42 | Process.runSync('cd', ['build']); 43 | Process.runSync('7z', ['a', '-tzip', 'zeronet_py3.zip', 'ZeroNet']); 44 | } 45 | } 46 | 47 | compile() { 48 | String versionProp = ''; 49 | var content = File('android/version.properties').readAsStringSync(); 50 | versionProp = 51 | content.split('\n')[0].replaceAll('flutter.versionName=', '').trim(); 52 | var result = Process.runSync('git', [ 53 | 'log', 54 | '-1', 55 | '--pretty=%B', 56 | ]); 57 | var ver = (result.stdout as String).split('\n')[0].trim(); 58 | if (ver.contains(versionProp)) { 59 | print('Repo is Clear to Compile APK For Release...'); 60 | print('Compiling APK For Release...'); 61 | Process.start('buildtools\\compile.bat', []).then((Process result) { 62 | result.stdout.listen((onData) { 63 | // print('Output : '); 64 | print(utf8.decode(onData)); 65 | }); 66 | result.stderr.listen((onData) { 67 | // print('Error : '); 68 | print(utf8.decode(onData)); 69 | }); 70 | }); 71 | } else 72 | throw "Update version.properties"; 73 | } 74 | 75 | var totalFilesList = []; 76 | var nonPyFiles = []; 77 | var pyFiles = []; 78 | var pyLibDir = '\\lib\\python3.8'; 79 | removeNonPy() { 80 | Directory dir = Directory.current; 81 | //Here 82 | recursiveHelper(Directory(dir.path + pyLibDir)); 83 | String nonPy = ''; 84 | nonPyFiles.forEach((f) => nonPy = nonPy + f + '\n'); 85 | 86 | File f = File('files-nonpy'); 87 | if (f.existsSync()) f.deleteSync(); 88 | f.createSync(); 89 | f.writeAsStringSync(nonPy); 90 | 91 | print(totalFilesList.length); 92 | print(pyFiles.length); 93 | print(nonPyFiles.length); 94 | } 95 | 96 | List ls = [ 97 | 'config-3.8', 98 | 'test', 99 | 'tests', 100 | 'ensurepip', 101 | 'idle_test', 102 | ]; 103 | 104 | recursiveHelper(Directory dir) { 105 | for (var file in dir.listSync()) { 106 | if (file is File) { 107 | var filePath = file.path.replaceAll(dir.path + '\\', ''); 108 | totalFilesList.add(filePath); 109 | if (filePath.endsWith('.py') || filePath.endsWith('.so')) { 110 | print('Python file $filePath'); 111 | pyFiles.add(filePath); 112 | } else { 113 | print('Non Python file $filePath'); 114 | if (filePath.endsWith('.exe') || 115 | filePath.endsWith('.bat') || 116 | filePath.endsWith('.ps1')) { 117 | print("Deleting File at:" + filePath); 118 | nonPyFiles.add(filePath); 119 | file.deleteSync(recursive: true); 120 | } else { 121 | print("Orphan File at " + filePath); 122 | } 123 | } 124 | } else { 125 | for (var item in ls) { 126 | if (file.path.endsWith('\\$item')) { 127 | file.deleteSync(recursive: true); 128 | } else { 129 | recursiveHelper(file); 130 | } 131 | } 132 | } 133 | } 134 | } 135 | 136 | modules() { 137 | Directory dir = Directory.current; 138 | File module = File('modules'); 139 | String modules = module.readAsStringSync(); 140 | List validModules = []; 141 | modules.split('\n').forEach((f) { 142 | if (f.contains('usr/')) { 143 | var i = f.indexOf('usr/'); 144 | validModules.add(f.substring(i + 4).replaceAll('/', '\\')); 145 | } 146 | }); 147 | var totalFilesList = []; 148 | var deletedFilesList = []; 149 | var pyLibDir = '\\lib\\python3.8'; 150 | for (var file in Directory(dir.path + pyLibDir).listSync(recursive: true)) { 151 | if (file is File) { 152 | var filePath = file.path.replaceAll(dir.path + '\\', ''); 153 | totalFilesList.add(filePath); 154 | if (validModules.indexOf(filePath) == -1) { 155 | print('deleting ${file.path}'); 156 | deletedFilesList.add(filePath); 157 | file.deleteSync(recursive: false); 158 | } 159 | } 160 | } 161 | String deleted = ''; 162 | deletedFilesList.forEach((f) => deleted = deleted + f + '\n'); 163 | 164 | File f = File('modules-deleted'); 165 | if (f.existsSync()) f.deleteSync(); 166 | f.createSync(); 167 | f.writeAsStringSync(deleted); 168 | 169 | deleteEmptyDirs(dir, pyLibDir); 170 | deleteEmptyDirs(dir, pyLibDir); 171 | print(totalFilesList.length); 172 | // print(totalFilesList[0]); 173 | print(validModules.length); 174 | // print(validModules[0]); 175 | print(deletedFilesList.length); 176 | // print(deletedFilesList[0]); 177 | } 178 | 179 | deleteEmptyDirs(Directory dir, String pyLibDir) { 180 | print('Deleting Empty Dirs'); 181 | for (var file in Directory(dir.path + pyLibDir).listSync(recursive: true)) { 182 | if (file is Directory) { 183 | if (file.listSync(recursive: true).length == 0) { 184 | file.deleteSync(recursive: true); 185 | } 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /lib/controllers/process_controller.dart: -------------------------------------------------------------------------------- 1 | import '../imports.dart'; 2 | import '../imports_conflicts.dart'; 3 | 4 | class ProcessController extends GetxController { 5 | var status = ProcessStatus.Idle.obs; 6 | var consoleLog = ''.obs; 7 | 8 | void appendConsoleLog(Object log) => consoleLog.value += log.toString(); 9 | 10 | void clearConsoleLog() => consoleLog.value = ''; 11 | 12 | void changeProcessStatus(ProcessStatus statusInput) => 13 | status.value = statusInput; 14 | 15 | void runProcess(String path) { 16 | var python = appNativeDir + '/libpython3.8.so'; 17 | if (File(python).existsSync()) { 18 | Process.start('$python', [ 19 | path, 20 | ], environment: { 21 | "LD_LIBRARY_PATH": "$libDir:$libDir64:/system/lib64", 22 | 'PYTHONHOME': '$dataDir/usr', 23 | 'PYTHONPATH': '$python', 24 | }) 25 | .then((proc) { 26 | proc.stderr.listen((onData) { 27 | appendConsoleLog(utf8.decode(onData)); 28 | }); 29 | proc.stdout.listen((onData) { 30 | appendConsoleLog(utf8.decode(onData)); 31 | }); 32 | }) 33 | .whenComplete( 34 | () => processController.changeProcessStatus(ProcessStatus.Idle), 35 | ) 36 | .catchError((e) { 37 | if (e is ProcessException) { 38 | printOut(e.toString()); 39 | } 40 | processController.changeProcessStatus(ProcessStatus.Error); 41 | appendConsoleLog({'ZeroNetStatus': 'ERROR'}); 42 | appendConsoleLog({'console': e.toString()}); 43 | }); 44 | } else { 45 | //TODO: Improve Error Trace here 46 | appendConsoleLog({'console': 'Python Binary Not Found'}); 47 | processController.changeProcessStatus(ProcessStatus.Error); 48 | var contents = Directory(appNativeDir).listSync(recursive: true); 49 | for (var item in contents) { 50 | appendConsoleLog({'console': item.name()}); 51 | appendConsoleLog({'console': item.path}); 52 | } 53 | } 54 | return; 55 | } 56 | } 57 | 58 | final processController = Get.put(ProcessController()); 59 | -------------------------------------------------------------------------------- /lib/controllers/project_controller.dart: -------------------------------------------------------------------------------- 1 | import '../imports.dart'; 2 | import '../imports_conflicts.dart'; 3 | 4 | class ProjectController extends GetxController { 5 | var projects = [Project(id: null)].obs; 6 | List templates = []; 7 | List fileIcons = []; 8 | var currentTemplate = 'helloworld.py'.obs; 9 | 10 | var createdNewProjectName = ''.obs; 11 | var createdNewProjectPath = ''.obs; 12 | 13 | void loadCodeTemplates() async { 14 | var templatesStr = await rootBundle.loadString( 15 | 'assets/templates/python/templates.json', 16 | ); 17 | var templateMap = json.decode(templatesStr); 18 | 19 | for (var item in (templateMap as Map).keys) { 20 | var codeTemplate = CodeTemplate().fromMap(item, templateMap[item]); 21 | templates.add(codeTemplate); 22 | } 23 | } 24 | 25 | void loadFileIcons() async { 26 | var templatesStr = await rootBundle.loadString( 27 | 'assets/fileicons/fileicons.json', 28 | ); 29 | var iconsMap = json.decode(templatesStr); 30 | 31 | for (var item in (iconsMap as Map).keys) { 32 | var codeTemplate = FileIcon().fromMap(item, iconsMap[item]); 33 | fileIcons.add(codeTemplate); 34 | } 35 | } 36 | 37 | void loadExistingProjects() { 38 | Directory projectsDirectory = Directory(appPrivDocsDir.path + '/Projects'); 39 | if (!projectsDirectory.existsSync()) { 40 | return; 41 | } 42 | File projectsFile = File(projectsDirectory.path + '/Projects.json'); 43 | if (projectsFile.existsSync()) { 44 | var projectsList = json.decode(projectsFile.readAsStringSync()); 45 | for (Map project in projectsList) { 46 | var projectLoaded = loadProjectFromPath(project[project.keys.first]); 47 | addProjectToList(projectLoaded); 48 | } 49 | } 50 | } 51 | 52 | Project loadProjectFromPath(String path) { 53 | File propsFile = File(path + '/project.props'); 54 | Project project; 55 | if (propsFile.existsSync()) { 56 | project = Project().fromMap(json.decode(propsFile.readAsStringSync())); 57 | } 58 | return project; 59 | } 60 | 61 | void addProjectToList(Project project) { 62 | if (projects.where((p) => p.id == project.id).isEmpty) 63 | projects.add(project); 64 | } 65 | 66 | void addProject(Project project) { 67 | addProjectToList(project); 68 | saveToExistingProjects(project); 69 | } 70 | 71 | void removeProject(String id) { 72 | loadExistingProjects(); 73 | removeProjectFromList(id); 74 | } 75 | 76 | void removeProjectFromList(String id) { 77 | var projectsLoaded = projects.where((p) => p.id == id); 78 | if (projectsLoaded.isNotEmpty) { 79 | var project = projectsLoaded.first; 80 | removeProjectFromFileSystem(project.id); 81 | projects.remove(project); 82 | } 83 | } 84 | 85 | void removeProjectFromFileSystem(String id) { 86 | Directory projectsDirectory = Directory(appPrivDocsDir.path + '/Projects'); 87 | File projectsFile = File(projectsDirectory.path + '/Projects.json'); 88 | List list = []; 89 | for (var project in projects) { 90 | if (project.id != null && id != project.id) 91 | list.add({project.id: project.path}); 92 | if (id == project.id) { 93 | Directory projDir = Directory(project.path); 94 | projDir.deleteSync(recursive: true); 95 | } 96 | } 97 | projectsFile.writeAsStringSync(json.encode(list)); 98 | } 99 | 100 | void saveToExistingProjects(Project project) { 101 | Directory projectsDirectory = Directory(appPrivDocsDir.path + '/Projects'); 102 | File projectsFile = File(projectsDirectory.path + '/Projects.json'); 103 | List list = []; 104 | if (!projectsFile.existsSync()) { 105 | projectsFile.createSync(recursive: true); 106 | list.add({project.id: project.path}); 107 | } else { 108 | loadExistingProjects(); 109 | for (var project in projects) { 110 | if (project.id != null) list.add({project.id: project.path}); 111 | } 112 | } 113 | projectsFile.writeAsStringSync(json.encode(list)); 114 | } 115 | 116 | String getProjectName(String name) { 117 | if (name.contains(' ')) { 118 | List parts = name.split(' '); 119 | return parts.join('_'); 120 | } 121 | return name; 122 | } 123 | 124 | String getProjectPath(String name, Directory directory) { 125 | String path = ''; 126 | if (directory == null) 127 | path = appPrivDir.path + '/Projects/${getProjectName(name)}'; 128 | else 129 | return directory.path; 130 | return path; 131 | } 132 | 133 | Future createProject(String name, String path) async { 134 | var isCreated = false; 135 | if (path.isEmpty || path == 'Projects/$name') 136 | path = appPrivDir.path + '/Projects/$name'; 137 | var project = Project( 138 | id: randomAlphaNumeric(20), 139 | name: name, 140 | path: path, 141 | created: DateTime.now(), 142 | lastEdited: DateTime.now(), 143 | program: projectController.currentTemplate.value, 144 | ); 145 | createProjectPropertiesFile(project); 146 | addProject(project); 147 | isCreated = await addTemplateToProject(project); 148 | return isCreated; 149 | } 150 | 151 | Future addTemplateToProject(Project project) async { 152 | var content = await rootBundle 153 | .loadString('assets/templates/python/${project.program}'); 154 | File file = File(project.path + '/${project.program}'); 155 | file.createSync(recursive: true); 156 | file.writeAsStringSync(content); 157 | return true; 158 | } 159 | 160 | void createProjectPropertiesFile(Project project) { 161 | var string = json.encode(project.toMap()); 162 | Directory dir = Directory(project.path); 163 | if (dir.existsSync()) { 164 | throw 'Project Already Exists, Delete Existing Project to Proceed'; 165 | } else { 166 | dir.createSync(recursive: true); 167 | File propertiesFile = File(project.path + '/project.props'); 168 | propertiesFile.createSync(); 169 | propertiesFile.writeAsStringSync(string); 170 | } 171 | } 172 | } 173 | 174 | final projectController = Get.put(ProjectController()); 175 | -------------------------------------------------------------------------------- /lib/controllers/project_file_controller.dart: -------------------------------------------------------------------------------- 1 | import '../imports.dart'; 2 | import '../imports_conflicts.dart'; 3 | 4 | class ProjectFileController extends GetxController { 5 | var currentFileText = ''.obs; 6 | 7 | void saveCurrentFileContent(String text) { 8 | File currentFile = File(projectViewController.currentFile.value.path); 9 | currentFile.writeAsStringSync(text); 10 | } 11 | } 12 | 13 | final projectFileController = Get.put(ProjectFileController()); 14 | -------------------------------------------------------------------------------- /lib/controllers/project_view_controller.dart: -------------------------------------------------------------------------------- 1 | import '../imports.dart'; 2 | import '../imports_conflicts.dart'; 3 | 4 | class ProjectViewController extends GetxController { 5 | var openedFiles = [].obs; 6 | var currentFile = ProjectFile().obs; 7 | Project currentProject; 8 | 9 | void openFile(ProjectFile file) { 10 | if (!openedFiles.contains(file)) { 11 | openedFiles.add(file); 12 | changeCurrentFile(file); 13 | } 14 | } 15 | 16 | void closeAllFiles() { 17 | openedFiles.clear(); 18 | if (openedFiles.isEmpty) changeCurrentFile(ProjectFile()); 19 | } 20 | 21 | void closeFile(ProjectFile file) { 22 | openedFiles.remove(file); 23 | if (openedFiles.isEmpty) changeCurrentFile(ProjectFile()); 24 | } 25 | 26 | void changeCurrentFile(ProjectFile file) { 27 | currentFile.update((val) { 28 | val.name = file.name; 29 | val.path = file.path; 30 | }); 31 | } 32 | 33 | void changeCurrentFileEditMode(ProjectFile file) { 34 | currentFile.update((val) { 35 | val.name = file.name; 36 | val.path = file.path; 37 | val.isInEditMode = !val.isInEditMode; 38 | }); 39 | } 40 | 41 | void closeProject() { 42 | currentProject = null; 43 | processController.clearConsoleLog(); 44 | closeAllFiles(); 45 | uiController.updateCurrentAppRoute(AppRoute.Home); 46 | } 47 | } 48 | 49 | final projectViewController = Get.put(ProjectViewController()); 50 | -------------------------------------------------------------------------------- /lib/controllers/purchases_controller.dart: -------------------------------------------------------------------------------- 1 | import '../imports_conflicts.dart'; 2 | 3 | class PurchasesController extends GetxController { 4 | var oneTimePurchases = [].obs; 5 | var subscriptions = [].obs; 6 | 7 | void addOneTimePuchases(List details) { 8 | for (var item in details) { 9 | bool exists = oneTimePurchases 10 | .any((element) => element.identifier == item.identifier); 11 | if (!exists) { 12 | oneTimePurchases.add(item); 13 | } 14 | } 15 | } 16 | 17 | void addSubscriptions(List details) { 18 | for (var item in details) { 19 | bool exists = 20 | subscriptions.any((element) => element.identifier == item.identifier); 21 | if (!exists) { 22 | subscriptions.add(item); 23 | } 24 | } 25 | } 26 | 27 | var purchases = [].obs; 28 | 29 | void addPurchases(String purchaseIds) { 30 | if (!purchases.contains(purchaseIds)) { 31 | purchases.add(purchaseIds); 32 | } 33 | } 34 | 35 | var consumedPurchases = [].obs; 36 | 37 | void addConsumedPurchases(String purchaseIds) { 38 | if (!consumedPurchases.contains(purchaseIds)) { 39 | consumedPurchases.add(purchaseIds); 40 | } 41 | } 42 | } 43 | 44 | final purchasesController = Get.put(PurchasesController()); 45 | -------------------------------------------------------------------------------- /lib/controllers/ui_controller.dart: -------------------------------------------------------------------------------- 1 | import '../imports_conflicts.dart'; 2 | import '../imports.dart'; 3 | 4 | class UiController extends GetxController { 5 | PersistentBottomSheetController currentBottomSheetController; 6 | 7 | var appUpdate = AppUpdate.NOT_AVAILABLE.obs; 8 | 9 | void updateInAppUpdateAvailable(AppUpdate available) => 10 | appUpdate.value = available; 11 | 12 | var showSnackReply = false.obs; 13 | 14 | void updateShowSnackReply(bool show) { 15 | showSnackReply.value = show; 16 | } 17 | 18 | var reload = 0.obs; 19 | 20 | void updateReload(int i) { 21 | reload.value = i; 22 | } 23 | 24 | var currentAppRoute = AppRoute.Home.obs; 25 | 26 | void updateCurrentAppRoute(AppRoute appRoute) => 27 | currentAppRoute.value = appRoute; 28 | 29 | var currentTheme = AppTheme.Light.obs; 30 | 31 | void setTheme(AppTheme theme) { 32 | currentTheme.value = theme; 33 | } 34 | } 35 | 36 | final uiController = Get.put(UiController()); 37 | -------------------------------------------------------------------------------- /lib/controllers/var_controller.dart: -------------------------------------------------------------------------------- 1 | import '../imports.dart'; 2 | import '../imports_conflicts.dart'; 3 | 4 | class VarController extends GetxController { 5 | String zeroNetWrapperKey = ''; 6 | 7 | var event = ObservableEvent.installed.obs; 8 | 9 | void setObservableEvent(ObservableEvent eve) { 10 | event.value = eve; 11 | } 12 | 13 | var zeroNetLog = 'ZeroNet Mobile'.obs; 14 | 15 | void setZeroNetLog(String status) { 16 | zeroNetLog.value = status; 17 | } 18 | 19 | var zeroNetAppbarStatus = 'ZeroNet Mobile'.obs; 20 | 21 | void setZeroNetAppbarStatus(String status) { 22 | zeroNetAppbarStatus.value = status; 23 | } 24 | 25 | var zeroNetStatus = 'Not Running'.obs; 26 | 27 | void setZeroNetStatus(String status) { 28 | zeroNetStatus.value = status; 29 | } 30 | 31 | var zeroNetInstalled = false.obs; 32 | 33 | void isZeroNetInstalled(bool installed) { 34 | zeroNetInstalled.value = installed; 35 | } 36 | 37 | var zeroNetDownloaded = false.obs; 38 | 39 | void isZeroNetDownloaded(bool downloaded) { 40 | zeroNetDownloaded.value = downloaded; 41 | } 42 | 43 | var loadingStatus = 'Loading'.obs; 44 | 45 | void setLoadingStatus(String status) { 46 | loadingStatus.value = status; 47 | } 48 | 49 | var loadingPercent = 0.obs; 50 | 51 | void setLoadingPercent(int percent) { 52 | loadingPercent.value = percent; 53 | } 54 | } 55 | 56 | final varController = Get.put(VarController()); 57 | -------------------------------------------------------------------------------- /lib/imports.dart: -------------------------------------------------------------------------------- 1 | export 'dart:async'; 2 | export 'dart:convert'; 3 | export 'dart:io'; 4 | export 'dart:isolate'; 5 | export 'dart:typed_data'; 6 | 7 | export 'package:flutter/foundation.dart'; 8 | export 'package:flutter/gestures.dart'; 9 | export 'package:flutter/material.dart'; 10 | export 'package:flutter/services.dart'; 11 | 12 | export 'package:clipboard/clipboard.dart'; 13 | export 'package:crypto/crypto.dart'; 14 | export 'package:device_info/device_info.dart'; 15 | export 'package:equatable/equatable.dart'; 16 | export 'package:flutter_highlight/flutter_highlight.dart'; 17 | export 'package:flutter_highlight/themes/github.dart'; 18 | export 'package:hive/hive.dart'; 19 | export 'package:hive_flutter/hive_flutter.dart'; 20 | export 'package:file_picker/file_picker.dart'; 21 | export 'package:flutter_downloader/flutter_downloader.dart'; 22 | export 'package:flutter_json_widget/flutter_json_widget.dart'; 23 | export 'package:flutter_svg/flutter_svg.dart'; 24 | export 'package:google_fonts/google_fonts.dart'; 25 | export 'package:http/http.dart'; 26 | export 'package:in_app_purchase/in_app_purchase.dart'; 27 | export 'package:in_app_review/in_app_review.dart'; 28 | export 'package:in_app_update/in_app_update.dart'; 29 | export 'package:outline_material_icons/outline_material_icons.dart'; 30 | export 'package:package_info/package_info.dart'; 31 | export 'package:path_provider/path_provider.dart'; 32 | export 'package:random_string/random_string.dart'; 33 | export 'package:url_launcher/url_launcher.dart'; 34 | 35 | export 'controllers/project_file_controller.dart'; 36 | export 'controllers/project_view_controller.dart'; 37 | export 'controllers/project_controller.dart'; 38 | export 'controllers/process_controller.dart'; 39 | export 'controllers/ui_controller.dart'; 40 | export 'controllers/var_controller.dart'; 41 | export 'controllers/purchases_controller.dart'; 42 | export 'models/enums.dart'; 43 | export 'models/models.dart'; 44 | export 'others/common.dart'; 45 | export 'others/constants.dart'; 46 | export 'others/donation_const.dart'; 47 | export 'others/extensions.dart'; 48 | export 'others/native.dart'; 49 | export 'others/strings.dart'; 50 | export 'others/utils.dart'; 51 | export 'widgets/about_page.dart'; 52 | export 'widgets/common.dart'; 53 | export 'widgets/home_page.dart'; 54 | export 'widgets/loading_page.dart'; 55 | export 'widgets/log_page.dart'; 56 | export 'widgets/project_view_page.dart'; 57 | export 'widgets/settings_page.dart'; 58 | -------------------------------------------------------------------------------- /lib/imports_conflicts.dart: -------------------------------------------------------------------------------- 1 | export 'dart:ui'; 2 | 3 | export 'package:archive/archive.dart'; 4 | export 'package:get/get.dart'; 5 | export 'package:purchases_flutter/purchases_flutter.dart'; 6 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'imports.dart'; 2 | import 'imports_conflicts.dart'; 3 | 4 | //TODO:Remainder: Removed Half baked x86 bins, add them when we support x86 platform 5 | Future main() async { 6 | WidgetsFlutterBinding.ensureInitialized(); 7 | await Hive.initFlutter(); 8 | variableBox = await Hive.openBox('variableBox'); 9 | await init(); 10 | if (kEnableInAppPurchases) { 11 | InAppPurchaseConnection.enablePendingPurchases(); 12 | final Stream purchaseUpdates = 13 | InAppPurchaseConnection.instance.purchaseUpdatedStream; 14 | purchaseUpdates.listen((purchases) => listenToPurchaseUpdated(purchases)); 15 | } 16 | runApp(MyApp()); 17 | } 18 | 19 | class MyApp extends StatelessWidget { 20 | @override 21 | Widget build(BuildContext context) { 22 | SystemChrome.setSystemUIOverlayStyle( 23 | SystemUiOverlayStyle( 24 | statusBarColor: Colors.transparent, 25 | systemNavigationBarColor: Colors.white, 26 | statusBarIconBrightness: Brightness.dark, 27 | systemNavigationBarIconBrightness: Brightness.dark, 28 | ), 29 | ); 30 | return GetMaterialApp( 31 | title: appTitle, 32 | theme: ThemeData( 33 | primarySwatch: Colors.indigo, 34 | visualDensity: VisualDensity.adaptivePlatformDensity, 35 | ), 36 | debugShowCheckedModeBanner: false, 37 | home: Scaffold( 38 | resizeToAvoidBottomInset: true, 39 | body: Obx( 40 | () { 41 | if (varController.zeroNetInstalled.value) { 42 | if (firstTime) { 43 | SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values); 44 | // uiController.updateCurrentAppRoute(AppRoute.Settings); 45 | if (!isExecPermitted) 46 | makeExecHelper().then( 47 | (value) => isExecPermitted = value, 48 | ); 49 | variableBox.put('firstTime', false); 50 | } 51 | projectController.loadExistingProjects(); 52 | return Obx( 53 | () { 54 | switch (uiController.currentAppRoute.value) { 55 | case AppRoute.AboutPage: 56 | return WillPopScope( 57 | onWillPop: () { 58 | uiController.updateCurrentAppRoute(AppRoute.Home); 59 | return Future.value(false); 60 | }, 61 | child: AboutPage(), 62 | ); 63 | break; 64 | case AppRoute.ProjectViewPage: 65 | return WillPopScope( 66 | onWillPop: () { 67 | uiController.updateCurrentAppRoute(AppRoute.Home); 68 | projectViewController.closeProject(); 69 | return Future.value(false); 70 | }, 71 | child: ProjectViewPage(), 72 | ); 73 | break; 74 | case AppRoute.Home: 75 | getInAppPurchases(); 76 | return HomePage(); 77 | break; 78 | case AppRoute.Settings: 79 | return WillPopScope( 80 | onWillPop: () { 81 | uiController.updateCurrentAppRoute(AppRoute.Home); 82 | return Future.value(false); 83 | }, 84 | child: SettingsPage(), 85 | ); 86 | break; 87 | case AppRoute.LogPage: 88 | return WillPopScope( 89 | onWillPop: () { 90 | uiController.updateCurrentAppRoute(AppRoute.Home); 91 | return Future.value(false); 92 | }, 93 | child: ZeroNetLogPage(), 94 | ); 95 | break; 96 | default: 97 | return Container(); 98 | } 99 | }, 100 | ); 101 | } else 102 | return Loading(); 103 | }, 104 | ), 105 | ), 106 | ); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /lib/models/enums.dart: -------------------------------------------------------------------------------- 1 | import '../imports.dart'; 2 | 3 | enum AppUpdate { 4 | NOT_AVAILABLE, 5 | AVAILABLE, 6 | DOWNLOADING, 7 | DOWNLOADED, 8 | INSTALLING, 9 | } 10 | 11 | extension AppUpdateExt on AppUpdate { 12 | get text { 13 | switch (uiController.appUpdate.value) { 14 | case AppUpdate.AVAILABLE: 15 | return 'Update'; 16 | break; 17 | case AppUpdate.DOWNLOADING: 18 | return 'Downloading'; 19 | break; 20 | case AppUpdate.DOWNLOADED: 21 | return 'Downloaded'; 22 | break; 23 | case AppUpdate.INSTALLING: 24 | return 'Installing'; 25 | break; 26 | default: 27 | return 'Not Available'; 28 | } 29 | } 30 | 31 | void action() { 32 | switch (uiController.appUpdate.value) { 33 | case AppUpdate.AVAILABLE: 34 | { 35 | // InAppUpdate.performImmediateUpdate().then((value) => 36 | // uiStore.updateInAppUpdateAvailable(AppUpdate.NOT_AVAILABLE)); 37 | //TODO: Switch to startFlexibleUpdate() when below issue is Fixed. 38 | //https://github.com/feilfeilundfeil/flutter_in_app_update/issues/42 39 | InAppUpdate.startFlexibleUpdate().then((value) => 40 | uiController.updateInAppUpdateAvailable(AppUpdate.DOWNLOADED)); 41 | uiController.updateInAppUpdateAvailable(AppUpdate.DOWNLOADING); 42 | } 43 | break; 44 | case AppUpdate.DOWNLOADED: 45 | { 46 | InAppUpdate.completeFlexibleUpdate().then((value) => 47 | uiController.updateInAppUpdateAvailable(AppUpdate.NOT_AVAILABLE)); 48 | uiController.updateInAppUpdateAvailable(AppUpdate.INSTALLING); 49 | } 50 | break; 51 | default: 52 | } 53 | } 54 | } 55 | 56 | enum AppRoute { 57 | Home, 58 | ProjectViewPage, 59 | Settings, 60 | ZeroBrowser, 61 | LogPage, 62 | AboutPage, 63 | } 64 | 65 | extension AppRouteExt on AppRoute { 66 | get title { 67 | switch (this) { 68 | case AppRoute.AboutPage: 69 | return 'About'; 70 | break; 71 | case AppRoute.Home: 72 | return appTitle; 73 | break; 74 | case AppRoute.Settings: 75 | return 'Settings'; 76 | break; 77 | case AppRoute.ProjectViewPage: 78 | return projectViewController.currentProject?.name ?? 'Project Name'; 79 | break; 80 | case AppRoute.ZeroBrowser: 81 | return 'ZeroBrowser'; 82 | break; 83 | case AppRoute.LogPage: 84 | return 'ZeroNet Log'; 85 | break; 86 | default: 87 | } 88 | } 89 | 90 | IconData get icon { 91 | switch (this) { 92 | case AppRoute.AboutPage: 93 | case AppRoute.Settings: 94 | case AppRoute.ZeroBrowser: 95 | case AppRoute.LogPage: 96 | return OMIcons.home; 97 | break; 98 | case AppRoute.Home: 99 | return OMIcons.info; 100 | // return OMIcons.settings; 101 | break; 102 | case AppRoute.ProjectViewPage: 103 | return OMIcons.close; 104 | break; 105 | case AppRoute.ProjectViewPage: 106 | return OMIcons.close; 107 | break; 108 | default: 109 | return OMIcons.error; 110 | } 111 | } 112 | 113 | void onClick() { 114 | switch (uiController.currentAppRoute.value) { 115 | case AppRoute.Home: 116 | uiController.updateCurrentAppRoute(AppRoute.AboutPage); 117 | // uiStore.updateCurrentAppRoute(AppRoute.Settings); 118 | break; 119 | case AppRoute.AboutPage: 120 | case AppRoute.Settings: 121 | case AppRoute.LogPage: 122 | case AppRoute.ZeroBrowser: 123 | uiController.updateCurrentAppRoute(AppRoute.Home); 124 | break; 125 | case AppRoute.ProjectViewPage: 126 | projectViewController.closeProject(); 127 | break; 128 | default: 129 | } 130 | } 131 | } 132 | 133 | enum ObservableEvent { 134 | none, 135 | downloding, 136 | downloaded, 137 | installing, 138 | installed, 139 | } 140 | 141 | enum AppTheme { 142 | Light, 143 | Dark, 144 | Black, 145 | } 146 | 147 | enum ProcessStatus { 148 | Running, 149 | Completed, 150 | Idle, 151 | Paused, 152 | Stoped, 153 | Error, 154 | } 155 | 156 | extension ProcessStatusExt on ProcessStatus { 157 | String get text { 158 | switch (this) { 159 | case ProcessStatus.Idle: 160 | case ProcessStatus.Completed: 161 | return 'Run'; 162 | break; 163 | case ProcessStatus.Paused: 164 | return 'Resume'; 165 | break; 166 | case ProcessStatus.Stoped: 167 | return 'Stopped'; 168 | break; 169 | case ProcessStatus.Running: 170 | return 'Pause'; 171 | break; 172 | case ProcessStatus.Error: 173 | return 'Error'; 174 | break; 175 | default: 176 | return 'Implement This Text'; 177 | } 178 | } 179 | 180 | Icon get icon { 181 | switch (this) { 182 | case ProcessStatus.Idle: 183 | case ProcessStatus.Stoped: 184 | case ProcessStatus.Paused: 185 | case ProcessStatus.Completed: 186 | return Icon(Icons.play_arrow_outlined); 187 | break; 188 | case ProcessStatus.Running: 189 | return Icon(Icons.pause); 190 | break; 191 | default: 192 | return Icon(Icons.error); 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /lib/models/models.dart: -------------------------------------------------------------------------------- 1 | import '../imports.dart'; 2 | 3 | class UnzipParams { 4 | String item; 5 | Uint8List bytes; 6 | String dest; 7 | UnzipParams( 8 | this.item, 9 | this.bytes, { 10 | this.dest = '', 11 | }); 12 | } 13 | 14 | enum state { 15 | NOT_DOWNLOADED, 16 | DOWNLOADING, 17 | NOT_INSTALLED, 18 | INSTALLING, 19 | MAKING_AS_EXEC, 20 | READY, 21 | RUNNING, 22 | NONE, 23 | } 24 | 25 | class FileIcon { 26 | String name; 27 | String fileName; 28 | List supportedExt; 29 | 30 | FileIcon({ 31 | this.name, 32 | this.fileName, 33 | this.supportedExt, 34 | }); 35 | 36 | FileIcon fromMap(String name, Map map) { 37 | List exts = []; 38 | for (var item in map['supportedExt']) { 39 | exts.add(item); 40 | } 41 | return FileIcon( 42 | name: name, 43 | fileName: map['fileName'], 44 | supportedExt: exts, 45 | ); 46 | } 47 | } 48 | 49 | class AppDeveloper extends Equatable { 50 | final String name; 51 | final String profileIconLink; 52 | final String developerType; 53 | final String githubLink; 54 | final String facebookLink; 55 | final String twitterLink; 56 | const AppDeveloper({ 57 | this.name, 58 | this.profileIconLink, 59 | this.developerType, 60 | this.githubLink, 61 | this.facebookLink, 62 | this.twitterLink, 63 | }); 64 | 65 | @override 66 | List get props => [ 67 | name, 68 | profileIconLink, 69 | developerType, 70 | githubLink, 71 | facebookLink, 72 | twitterLink, 73 | ]; 74 | } 75 | 76 | class CodeTemplate extends Equatable { 77 | final String name; 78 | final String fileName; 79 | final String source; 80 | 81 | CodeTemplate({ 82 | this.name, 83 | this.fileName, 84 | this.source, 85 | }); 86 | 87 | CodeTemplate fromMap(String name, Map map) { 88 | return CodeTemplate( 89 | name: name, 90 | fileName: map['fileName'], 91 | source: map['source'], 92 | ); 93 | } 94 | 95 | @override 96 | List get props => [ 97 | name, 98 | fileName, 99 | source, 100 | ]; 101 | } 102 | 103 | class Project extends Equatable { 104 | final String id; 105 | final String name; 106 | final String path; 107 | final String program; 108 | final DateTime created; 109 | final DateTime lastEdited; 110 | 111 | Project({ 112 | this.id, 113 | this.name, 114 | this.path, 115 | this.program, 116 | this.created, 117 | this.lastEdited, 118 | }); 119 | 120 | Map toMap() { 121 | return { 122 | 'id': this.id, 123 | 'name': this.name, 124 | 'path': this.path, 125 | 'program': this.program, 126 | 'created': this.created.millisecondsSinceEpoch, 127 | 'lastEdited': this.lastEdited.millisecondsSinceEpoch, 128 | }; 129 | } 130 | 131 | Project fromMap(Map map) { 132 | return Project( 133 | id: map['id'], 134 | name: map['name'], 135 | path: map['path'], 136 | program: map['program'], 137 | created: DateTime.fromMillisecondsSinceEpoch(map['created']), 138 | lastEdited: DateTime.fromMillisecondsSinceEpoch(map['lastEdited']), 139 | ); 140 | } 141 | 142 | List get propsNames => [ 143 | 'id', 144 | 'name', 145 | 'path', 146 | 'program', 147 | 'created', 148 | 'lastEdited', 149 | ]; 150 | 151 | @override 152 | List get props => [ 153 | id, 154 | name, 155 | path, 156 | program, 157 | created, 158 | lastEdited, 159 | ]; 160 | } 161 | 162 | class ProjectFile with EquatableMixin { 163 | String name; 164 | String path; 165 | bool isInEditMode; 166 | 167 | ProjectFile({ 168 | this.name, 169 | this.path, 170 | this.isInEditMode = false, 171 | }); 172 | 173 | @override 174 | List get props => [name, path]; 175 | } 176 | 177 | extension ProjectFileExt on ProjectFile { 178 | bool get isPropsFile => this.path.endsWith('project.props'); 179 | } 180 | -------------------------------------------------------------------------------- /lib/others/common.dart: -------------------------------------------------------------------------------- 1 | import '../imports.dart'; 2 | import '../imports_conflicts.dart'; 3 | 4 | Directory appPrivDir; 5 | Directory appPrivDocsDir; 6 | Directory tempDir; 7 | Directory metaDir = Directory(dataDir + '/meta'); 8 | Directory trackersDir = Directory(dataDir + '/trackers'); 9 | AndroidDeviceInfo deviceInfo; 10 | bool isZeroNetInstalledm = false; 11 | bool isZeroNetDownloadedm = false; 12 | bool isDownloadExec = false; 13 | bool canLaunchUrl = false; 14 | bool firstTime = true; 15 | bool kIsPlayStoreInstall = false; 16 | bool kEnableInAppPurchases = !kDebugMode && kIsPlayStoreInstall; 17 | bool kEnableCryptoPurchases = !kDebugMode && kIsPlayStoreInstall; 18 | bool manuallyStoppedZeroNet = false; 19 | bool zeroNetStartedFromBoot = true; 20 | bool isExecPermitted = false; 21 | bool debugZeroNetCode = false; 22 | bool vibrateonZeroNetStart = false; 23 | bool enableZeroNetAddTrackers = false; 24 | int downloadStatus = 0; 25 | Map downloadsMap = {}; 26 | Map downloadStatusMap = {}; 27 | PackageInfo packageInfo; 28 | String appVersion = ''; 29 | String buildNumber; 30 | var zeroNetState = state.NONE; 31 | Client client = Client(); 32 | String arch; 33 | Box variableBox; 34 | String appNativeDir = ''; 35 | String zeroNetIPwithPort(String url) => 36 | url.replaceAll('http:', '').replaceAll('/', '').replaceAll('s', ''); 37 | String sesionKey = ''; 38 | String browserUrl = 'https://google.com'; 39 | String zeroBrowserTheme = 'light'; 40 | String snackMessage = ''; 41 | 42 | String downloadLink(String item) => 43 | releases + 'Android_Module_Binaries/$item.zip'; 44 | 45 | bool isUsrBinExists() => Directory(dataDir + '/usr').existsSync(); 46 | bool isZeroNetExists() => Directory(dataDir + '/ZeroNet-py3').existsSync(); 47 | String downloadingMetaDir(String tempDir, String name, String key) => 48 | Directory(tempDir + '/meta/$name.$key.downloading').path; 49 | String downloadedMetaDir(String tempDir, String name) => 50 | Directory(tempDir + '/meta/$name.downloaded').path; 51 | String installingMetaDir(String tempDir, String name, String key) => 52 | Directory(tempDir + '/meta/$name.$key.installing').path; 53 | String installedMetaDir(String dir, String name) => 54 | Directory(dir + '/$name.installed').path; 55 | Duration secs(int sec) => Duration(seconds: sec); 56 | List files(String arch) => [ 57 | 'python38_$arch', 58 | 'site_packages_common', 59 | ]; 60 | 61 | init() async { 62 | firstTime = variableBox.get('firstTime', defaultValue: true); 63 | getArch(); 64 | kIsPlayStoreInstall = await isPlayStoreInstall(); 65 | appNativeDir = await getNativeDir(); 66 | tempDir = await getTemporaryDirectory(); 67 | appPrivDir = await getExternalStorageDirectory(); 68 | appPrivDocsDir = await getApplicationDocumentsDirectory(); 69 | isZeroNetInstalledm = await isZeroNetInstalled(); 70 | if (isZeroNetInstalledm) { 71 | varController.isZeroNetInstalled(isZeroNetInstalledm); 72 | checkForAppUpdates(); 73 | projectController.loadCodeTemplates(); 74 | projectController.loadFileIcons(); 75 | } 76 | if (!tempDir.existsSync()) tempDir.createSync(recursive: true); 77 | Purchases.setup("ShCpAJsKdJrAAQawcMQSswqTyPWFMwXb"); 78 | } 79 | 80 | Future pickFile({List fileExts}) async { 81 | FilePickerResult result = await FilePicker.platform.pickFiles( 82 | type: FileType.any, 83 | allowedExtensions: fileExts, 84 | ); 85 | 86 | return result; 87 | } 88 | 89 | check() async { 90 | if (!isZeroNetInstalledm) { 91 | if (isZeroNetDownloadedm) { 92 | if (isZeroNetInstalledm) { 93 | varController.setLoadingStatus('ZeroNet Installed'); 94 | varController.isZeroNetInstalled(isZeroNetInstalledm); 95 | printOut('isZeroNetInstalledm'); 96 | } else { 97 | isZeroNetInstalled().then((onValue) async { 98 | isZeroNetInstalledm = onValue; 99 | varController.isZeroNetInstalled(onValue); 100 | if (!isZeroNetInstalledm) { 101 | if (!unZipIsolateBound) bindUnZipIsolate(); 102 | unZipinBg(); 103 | } 104 | }); 105 | } 106 | } else { 107 | isZeroNetInstalledm = await isZeroNetInstalled(); 108 | if (!isZeroNetInstalledm) { 109 | isZeroNetDownloadedm = await isZeroNetDownloaded(); 110 | if (isZeroNetDownloadedm) { 111 | varController.isZeroNetDownloaded(true); 112 | } else { 113 | varController.setLoadingStatus(downloading); 114 | if (!isDownloadExec) { 115 | if (await isModuleInstallSupported() && 116 | kEnableDynamicModules && 117 | await isPlayStoreInstall()) { 118 | await initSplitInstall(); 119 | printOut( 120 | 'PlayStore Module Install Supported', 121 | lineBreaks: 3, 122 | isNative: true, 123 | ); 124 | if (await isRequiredModulesInstalled()) { 125 | printOut( 126 | 'Required Modules are Installed', 127 | lineBreaks: 3, 128 | isNative: true, 129 | ); 130 | if (await copyAssetsToCache()) { 131 | printOut( 132 | 'Assets Copied to Cache', 133 | lineBreaks: 3, 134 | isNative: true, 135 | ); 136 | isZeroNetDownloadedm = true; 137 | varController.setLoadingStatus(installing); 138 | varController.setLoadingPercent(0); 139 | check(); 140 | } 141 | } else { 142 | printOut( 143 | 'Required Modules are not Installed, Installing', 144 | lineBreaks: 3, 145 | isNative: true, 146 | ); 147 | handleModuleDownloadStatus(); 148 | } 149 | } else { 150 | await initDownloadParams(); 151 | downloadBins(); 152 | } 153 | } 154 | } 155 | } else { 156 | varController.isZeroNetInstalled(true); 157 | } 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /lib/others/constants.dart: -------------------------------------------------------------------------------- 1 | import '../imports.dart'; 2 | 3 | const String pkgName = 'in.canews.pythonide${kDebugMode ? '.debug' : ''}'; 4 | const String dataDir = "/data/data/$pkgName/files"; 5 | const String bin = '$dataDir/usr/bin'; 6 | const String python = '$bin/python'; 7 | const String libDir = '$dataDir/usr/lib'; 8 | const String libDir64 = '$dataDir/usr/lib64'; 9 | const String downloading = 'Downloading Files'; 10 | const String installing = 'Installing ZeroNet Files'; 11 | const String facebookLink = 'https://facebook.com'; 12 | const String twitterLink = 'https://twitter.com'; 13 | const String githubLink = 'https://github.com'; 14 | const String rawGithubLink = 'https://raw.githubusercontent.com'; 15 | const String canewsInRepo = '/canewsin/ZeroNet'; 16 | const String releases = '$githubLink$canewsInRepo/releases/download/'; 17 | const String md5hashLink = '$rawGithubLink$canewsInRepo/py3-patches/md5.hashes'; 18 | const String zeroNetNotiId = 'zeroNetNetiId'; 19 | const String zeroNetChannelName = 'ZeroNet Mobile'; 20 | const String zeroNetChannelDes = 21 | 'Shows ZeroNet Notification to Persist from closing.'; 22 | const String notificationCategory = 'ZERONET_RUNNING'; 23 | const String isolateUnZipPort = 'unzip_send_port'; 24 | const String isolateDownloadPort = 'downloader_send_port'; 25 | const String zeronetStartUpError = 'Startup error: '; 26 | const String zeronetAlreadyRunningError = 27 | zeronetStartUpError + 'Can\'t open lock file'; 28 | const bool kEnableDynamicModules = !kDebugMode; 29 | 30 | const List binDirs = [ 31 | 'usr', 32 | 'site-packages', 33 | ]; 34 | const List soDirs = [ 35 | 'usr/bin', 36 | 'usr/lib', 37 | 'usr/lib/python3.8/lib-dynload', 38 | 'usr/lib/python3.8/site-packages', 39 | ]; 40 | const List appDevelopers = [ 41 | AppDeveloper( 42 | name: 'PramUkesh', 43 | developerType: 'developer', 44 | profileIconLink: 'assets/developers/pramukesh.jpg', 45 | githubLink: '$githubLink/PramUkesh/', 46 | facebookLink: '$facebookLink/n.bhargavvenky', 47 | twitterLink: '$twitterLink/PramukeshVenky', 48 | ), 49 | AppDeveloper( 50 | name: 'CANewsIn', 51 | developerType: 'organisation', 52 | profileIconLink: 'assets/developers/canewsin.jpg', 53 | githubLink: '$githubLink/canewsin/', 54 | facebookLink: '$facebookLink/canews.in', 55 | twitterLink: '$twitterLink/canewsin', 56 | ), 57 | ]; 58 | 59 | const String profileSwitcher = 'Profile Switcher'; 60 | const String profileSwitcherDes = 61 | 'Create and Use different Profiles on ZeroNet'; 62 | const String debugZeroNet = 'Debug ZeroNet Code'; 63 | const String debugZeroNetDes = 64 | 'Useful for Developers to find bugs and errors in the code.'; 65 | const String enableZeroNetConsole = 'Enable ZeroNet Console'; 66 | const String enableZeroNetConsoleDes = 67 | 'Useful for Developers to see the exec of ZeroNet Python code'; 68 | const String enableZeroNetFilters = 'Enable ZeroNet Filters'; 69 | const String enableZeroNetFiltersDes = 70 | 'Enabling ZeroNet Filters blocks known ametuer content sites and spam users.'; 71 | const String enableAdditionalTrackers = 'Additional BitTorrent Trackers'; 72 | const String enableAdditionalTrackersDes = 73 | 'Enabling External/Additional BitTorrent Trackers will give more ZeroNet Site Seeders or Clients.'; 74 | const String pluginManager = 'Plugin Manager'; 75 | const String pluginManagerDes = 'Enable/Disable ZeroNet Plugins'; 76 | const String vibrateOnZeroNetStart = 'Vibrate on ZeroNet Start'; 77 | const String vibrateOnZeroNetStartDes = 'Vibrates Phone When ZeroNet Starts'; 78 | const String enableFullScreenOnWebView = 'FullScreen for ZeroNet Zites'; 79 | const String enableFullScreenOnWebViewDes = 80 | 'This will Enable Full Screen for in app Webview of ZeroNet'; 81 | const String batteryOptimisation = 'Disable Battery Optimisation'; 82 | const String batteryOptimisationDes = 83 | 'This will Helps to Run App even App is in Background for long time.'; 84 | const String publicDataFolder = 'Public DataFolder'; 85 | const String publicDataFolderDes = 86 | 'This Will Make ZeroNet Data Folder Accessible via File Manager.'; 87 | const String autoStartZeroNet = 'AutoStart ZeroNet'; 88 | const String autoStartZeroNetDes = 89 | 'This Will Make ZeroNet Auto Start on App Start, So you don\'t have to click Start Button Every Time on App Start.'; 90 | const String autoStartZeroNetonBoot = 'AutoStart ZeroNet on Boot'; 91 | const String autoStartZeroNetonBootDes = 92 | 'This Will Make ZeroNet Auto Start on Device Boot.'; 93 | -------------------------------------------------------------------------------- /lib/others/donation_const.dart: -------------------------------------------------------------------------------- 1 | import '../imports.dart'; 2 | import '../imports_conflicts.dart'; 3 | 4 | const Map donationsAddressMap = { 5 | "BTC(Preferred)": "35NgjpB3pzkdHkAPrNh2EMERGxnXgwCb6G", 6 | "ETH": "0xa81a32dcce8e5bcb9792daa19ae7f964699ee536", 7 | "UPI(Indian Users)": "pramukesh@upi", 8 | "Liberapay": "https://liberapay.com/canews.in/donate", 9 | }; 10 | 11 | const Set kGooglePlayPurchaseOneTimeIds = { 12 | 'zeronet_one_1.00', 13 | 'zeronet_one_5.00', 14 | 'zeronet_one_15.00' 15 | }; 16 | 17 | const Set kGooglePlayPurchaseSubscriptionIds = { 18 | 'zeronet_sub_1.00', 19 | 'zeronet_sub_5.00', 20 | 'zeronet_sub_15.00' 21 | }; 22 | 23 | Future getInAppPurchases() async { 24 | Offerings offerings = await Purchases.getOfferings(); 25 | if (offerings.current != null) { 26 | var onetimePurchases = (offerings.current.availablePackages.where( 27 | (element) => element.identifier.contains('zeronet_one'), 28 | )).toList(); 29 | purchasesController.addOneTimePuchases(onetimePurchases); 30 | 31 | var subscriptions = (offerings.current.availablePackages.where( 32 | (element) => element.identifier.contains('zeronet_sub'), 33 | )).toList(); 34 | purchasesController.addSubscriptions(subscriptions); 35 | } 36 | } 37 | 38 | Future isProUser() async { 39 | try { 40 | final userName = ''; // getZeroIdUserName(); 41 | PurchaserInfo purchaserInfo; 42 | if (userName.isNotEmpty) purchaserInfo = await Purchases.identify(userName); 43 | purchaserInfo = await Purchases.getPurchaserInfo(); 44 | if (purchaserInfo.entitlements.active.length > 0) return true; 45 | } on PlatformException catch (e) { 46 | // Error fetching purchaser info 47 | } 48 | return false; 49 | } 50 | 51 | void purchasePackage(Package package) async { 52 | try { 53 | PurchaserInfo purchaserInfo; 54 | final userName = ''; //getZeroIdUserName(); 55 | if (userName.isNotEmpty) purchaserInfo = await Purchases.identify(userName); 56 | purchaserInfo = await Purchases.purchasePackage(package); 57 | 58 | var isPro = await isProUser(); 59 | if (isPro) { 60 | // Unlock that great "pro" content 61 | } 62 | print(purchaserInfo); 63 | } on PlatformException catch (e) { 64 | var errorCode = PurchasesErrorHelper.getErrorCode(e); 65 | if (errorCode != PurchasesErrorCode.purchaseCancelledError) { 66 | // showError(e); 67 | } 68 | } 69 | } 70 | 71 | void listenToPurchaseUpdated(List purchaseDetailsList) { 72 | purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async { 73 | if (purchaseDetails.status == PurchaseStatus.pending) { 74 | // showPendingUI(); 75 | } else { 76 | if (purchaseDetails.status == PurchaseStatus.error) { 77 | Get.showSnackbar( 78 | GetBar( 79 | message: 80 | //!TODO Improve Error Messages sp that user can understand easily. 81 | 'PurchaseStatus.error :: ${purchaseDetails.error.message}', 82 | ), 83 | ); 84 | } else if (purchaseDetails.status == PurchaseStatus.purchased) { 85 | bool valid = await _verifyPurchase(purchaseDetails); 86 | if (valid) { 87 | deliverProduct(purchaseDetails); 88 | } else { 89 | // _handleInvalidPurchase(purchaseDetails); 90 | return; 91 | } 92 | } 93 | 94 | if (purchaseDetails.productID != null && 95 | purchaseDetails.productID.contains('zeronet_one')) { 96 | await InAppPurchaseConnection.instance.consumePurchase(purchaseDetails); 97 | purchasesController.addConsumedPurchases(purchaseDetails.purchaseID); 98 | } 99 | if (purchaseDetails.pendingCompletePurchase) { 100 | await InAppPurchaseConnection.instance 101 | .completePurchase(purchaseDetails); 102 | } 103 | } 104 | }); 105 | } 106 | 107 | _verifyPurchase(PurchaseDetails purchaseDetails) { 108 | print(purchaseDetails.verificationData.localVerificationData); 109 | return Future.value(true); 110 | } 111 | 112 | void deliverProduct(PurchaseDetails purchaseDetails) async { 113 | // IMPORTANT!! Always verify a purchase purchase details before delivering the product. 114 | if (purchaseDetails.productID.contains('zeronet_one')) { 115 | purchasesController.addPurchases(purchaseDetails.purchaseID); 116 | } else {} 117 | } 118 | -------------------------------------------------------------------------------- /lib/others/extensions.dart: -------------------------------------------------------------------------------- 1 | import '../imports.dart'; 2 | 3 | extension FileSystemExtension on FileSystemEntity { 4 | String name() => this.path.replaceFirst(this.parent.path + '/', ''); 5 | String get fileExtension => this.name().split('.').last; 6 | } 7 | 8 | extension CapExtension on String { 9 | String get inCaps => '${this[0].toUpperCase()}${this.substring(1)}'; 10 | String get allInCaps => this.toUpperCase(); 11 | String get capitalizeFirstofEach => 12 | this.split(" ").map((str) => str.inCaps).join(" "); 13 | } 14 | 15 | extension DynamicExt on dynamic { 16 | int toInt() { 17 | if (this is num) { 18 | if (this is double) return this.toInt(); 19 | if (this is int) return this; 20 | } 21 | return -1; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/others/native.dart: -------------------------------------------------------------------------------- 1 | import '../imports.dart'; 2 | 3 | const MethodChannel _channel = const MethodChannel('in.canews.pythonide'); 4 | const EventChannel _events_channel = 5 | const EventChannel('in.canews.pythonide/installModules'); 6 | 7 | Future askBatteryOptimisation() async => 8 | await _channel.invokeMethod('batteryOptimisations'); 9 | 10 | Future isPlayStoreInstall() async => 11 | await _channel.invokeMethod('isPlayStoreInstall'); 12 | 13 | Future isBatteryOptimised() async => 14 | await _channel.invokeMethod('isBatteryOptimized'); 15 | 16 | Future moveTaskToBack() async => 17 | await _channel.invokeMethod('moveTaskToBack'); 18 | 19 | Future isModuleInstallSupported() async => 20 | await _channel.invokeMethod('isModuleInstallSupported'); 21 | 22 | Future isRequiredModulesInstalled() async => 23 | await _channel.invokeMethod('isRequiredModulesInstalled'); 24 | 25 | Future copyAssetsToCache() async => 26 | await _channel.invokeMethod('copyAssetsToCache'); 27 | 28 | Future getAppInstallTime() async => 29 | await _channel.invokeMethod('getAppInstallTime'); 30 | 31 | Future getAppLastUpdateTime() async => 32 | await _channel.invokeMethod('getAppLastUpdateTime'); 33 | 34 | Future initSplitInstall() async => 35 | await _channel.invokeMethod('initSplitInstall'); 36 | 37 | void uninstallModules() async => 38 | await _channel.invokeMethod('uninstallModules'); 39 | 40 | void nativePrint(String log) => _channel.invokeMethod('nativePrint', log); 41 | 42 | getNativeDir() async => await _channel.invokeMethod('nativeDir'); 43 | 44 | void handleModuleDownloadStatus() { 45 | _events_channel.receiveBroadcastStream().listen((onData) { 46 | Map data = json.decode(onData); 47 | final status = data['status']; 48 | if (status == 2) { 49 | final downloaded = data['downloaded']; 50 | final total = data['total']; 51 | double percentage = downloaded / total; 52 | varController.setLoadingPercent(percentage.toInt()); 53 | } 54 | printOut(onData, lineBreaks: 2, isNative: true); 55 | if (status == 5) check(); 56 | }); 57 | } 58 | 59 | getArch() async { 60 | if (deviceInfo == null) deviceInfo = await DeviceInfoPlugin().androidInfo; 61 | String archL = deviceInfo.supportedAbis[0]; 62 | if (archL.contains('arm64')) 63 | arch = 'arm64'; 64 | else if (archL.contains('armeabi')) 65 | arch = 'arm'; 66 | else if (archL.contains('x86_64')) 67 | arch = 'x86_64'; 68 | else if (archL.contains('x86')) arch = 'x86'; 69 | } 70 | -------------------------------------------------------------------------------- /lib/others/strings.dart: -------------------------------------------------------------------------------- 1 | //My App 2 | const appTitle = 'Python3 IDE'; 3 | 4 | //Loading 5 | const warning = """ 6 | Please Wait! This may take a while, happens 7 | only first time, Don't Press Back button. 8 | If You Accidentally Pressed Back, 9 | Clean App Storage in Settings or 10 | Uninstall and Reinstall The App. 11 | """; 12 | 13 | const logoPath = 'assets/logo.png'; 14 | 15 | //InAppUpdateWidget 16 | const appUpdateAvailable = 'App Update Available : '; 17 | 18 | //AboutButtonWidget 19 | const knowMore = 'Know More'; 20 | 21 | //RatingButtonWidget 22 | const feedbackStr = 'Give Your Rating/Feedback'; 23 | 24 | //AboutPage 25 | const appDescription1 = 26 | 'Python3 IDE Mobile is a full featured IDE for Android, ' 27 | 'to Run Python '; 28 | const appDescription2 = 29 | 'Code without requiring Additional PC Setup. Just Install the App and Run the Python Code with in Seconds or Just write some last minute code of your project in your mobile. '; 30 | const zeronetWebLink = 'https://github.com/canewsin/python_ide'; 31 | const contribute = "Contribute"; 32 | const contributeDescription = 33 | "If you want to support project's further development, " 34 | "you can contribute your time or money, " 35 | "If you want to contribute money you can send bitcoin or " 36 | "other supported crypto currencies to above addresses or " 37 | "buy in-app purchases, if want to contribute translations or " 38 | "code, visit official GitHub repo."; 39 | const clickAddToCopy = "* Click on Address to copy"; 40 | const developers = 'Developers'; 41 | const donationAddressTitle = 'Donation Addresses'; 42 | const donationDescription = 43 | "* Any Donation can activate all pro-features in app, " 44 | "these are just an encouragement to me to work more on the app. " 45 | "Pro-features will be made available to general public after certain time, " 46 | "thus you don't need to worry about exclusiveness of a feature. " 47 | "If you donate from any source other than Google Play Purchase, " 48 | "just send your transaction id to canews.in@gmail.com / ZeroMail: zeromepro, " 49 | "so than I can send activation code to activate pro-features."; 50 | -------------------------------------------------------------------------------- /lib/others/utils.dart: -------------------------------------------------------------------------------- 1 | import '../imports.dart'; 2 | import '../imports_conflicts.dart'; 3 | 4 | debugTime(Function func) { 5 | var start = DateTime.now(); 6 | func(); 7 | print(DateTime.now().difference(start).inMilliseconds); 8 | } 9 | 10 | printOut(Object object, {int lineBreaks = 0, bool isNative = false}) { 11 | if (kDebugMode || 12 | appVersion.contains('beta') || 13 | appVersion.contains('internal')) { 14 | var breaks = ''; 15 | for (var i = 0; i < lineBreaks; i++) breaks = breaks + '\n'; 16 | if (isNative) 17 | nativePrint(breaks + "$object" + breaks); 18 | else 19 | print(breaks + object + breaks); 20 | } 21 | } 22 | 23 | List md5List = []; 24 | bool isHashMatched(File file) { 25 | var bytes = file.readAsBytesSync(); 26 | var digest = md5.convert(bytes).toString(); 27 | for (var hash in md5List) { 28 | if (digest == hash) { 29 | return true; 30 | } 31 | } 32 | return false; 33 | } 34 | 35 | bool allFilesDownloaded() { 36 | bool isDownloaded = false; 37 | for (var item in files(arch)) { 38 | bool isLast = files(arch).length - 1 == files(arch).indexOf(item); 39 | File file = File(downloadedMetaDir(tempDir.path, item)); 40 | if (file.existsSync()) { 41 | if (isLast) isDownloaded = true; 42 | } else { 43 | File f = File(tempDir.path + '/$item.zip'); 44 | if (f.existsSync()) { 45 | if (!isHashMatched(f)) return false; 46 | file.createSync(recursive: true); 47 | if (isLast) isDownloaded = isHashMatched(f); 48 | } 49 | } 50 | } 51 | return isDownloaded; 52 | } 53 | 54 | initDownloadParams() async { 55 | await FlutterDownloader.initialize(); 56 | bindDownloadIsolate(); 57 | bindUnZipIsolate(); 58 | FlutterDownloader.registerCallback(handleDownloads); 59 | packageInfo = await PackageInfo.fromPlatform(); 60 | appVersion = packageInfo.version; 61 | buildNumber = packageInfo.buildNumber; 62 | } 63 | 64 | handleDownloads(String id, DownloadTaskStatus status, int progress) { 65 | final SendPort send = IsolateNameServer.lookupPortByName(isolateDownloadPort); 66 | send.send([id, status, progress]); 67 | } 68 | 69 | ReceivePort _downloadPort = ReceivePort(); 70 | 71 | bindDownloadIsolate() { 72 | bool isSuccess = IsolateNameServer.registerPortWithName( 73 | _downloadPort.sendPort, isolateDownloadPort); 74 | if (!isSuccess) { 75 | _unbindDownloadIsolate(); 76 | bindDownloadIsolate(); 77 | return; 78 | } 79 | _downloadPort.listen((data) { 80 | String id = data[0]; 81 | DownloadTaskStatus status = data[1]; 82 | int progress = data[2]; 83 | for (var item in downloadsMap.keys) { 84 | if (downloadsMap[item] == id) { 85 | downloadStatusMap[id] = progress; 86 | if (status == DownloadTaskStatus.complete) 87 | File(downloadedMetaDir(tempDir.path, item)) 88 | .createSync(recursive: true); 89 | } 90 | } 91 | var progressA = 0; 92 | for (var key in downloadStatusMap.keys) { 93 | progressA = (progressA + downloadStatusMap[key]); 94 | } 95 | var nooffiles = files(arch).length; 96 | varController.setLoadingPercent(progressA ~/ nooffiles); 97 | if ((progressA ~/ nooffiles) == 100) { 98 | isZeroNetDownloadedm = true; 99 | varController.setLoadingStatus(installing); 100 | varController.setLoadingPercent(0); 101 | check(); 102 | } 103 | }); 104 | } 105 | 106 | void _unbindDownloadIsolate() { 107 | IsolateNameServer.removePortNameMapping(isolateDownloadPort); 108 | } 109 | 110 | ReceivePort _unZipPort = ReceivePort(); 111 | bool unZipIsolateBound = false; 112 | 113 | bindUnZipIsolate() { 114 | bool isSuccess = IsolateNameServer.registerPortWithName( 115 | _unZipPort.sendPort, isolateUnZipPort); 116 | if (!isSuccess) { 117 | _unbindUnZipIsolate(); 118 | bindUnZipIsolate(); 119 | return; 120 | } 121 | unZipIsolateBound = true; 122 | _unZipPort.listen((data) { 123 | String name = data[0]; 124 | int currentFile = data[1]; 125 | int totalFiles = data[2]; 126 | var percent = (currentFile / totalFiles) * 100; 127 | if (percent.toInt() % 5 == 0) { 128 | varController.setLoadingStatus('Installing $name'); 129 | varController.setLoadingPercent(percent.toInt()); 130 | } 131 | if (percent == 100) { 132 | percentUnZip = percentUnZip + 100; 133 | } 134 | var nooffiles = files(arch).length; 135 | if (percentUnZip == nooffiles * 100) { 136 | printOut('Installation Completed', isNative: true); 137 | makeExecHelper().then((value) => isExecPermitted = value); 138 | uninstallModules(); 139 | check(); 140 | } 141 | }); 142 | } 143 | 144 | void _unbindUnZipIsolate() { 145 | IsolateNameServer.removePortNameMapping(isolateUnZipPort); 146 | } 147 | 148 | downloadBins() async { 149 | isDownloadExec = true; 150 | 151 | if (tempDir != null && arch != null) { 152 | int t = 0; 153 | for (var item in files(arch)) { 154 | t = t + 2; 155 | var file = File(tempDir.path + '/$item.zip'); 156 | if (!file.existsSync()) { 157 | sesionKey = randomAlpha(10); 158 | var tempFilePath = downloadingMetaDir( 159 | tempDir.path, 160 | item, 161 | sesionKey, 162 | ); 163 | if (!File(tempFilePath).existsSync()) { 164 | Timer(Duration(seconds: t), () { 165 | FlutterDownloader.enqueue( 166 | url: downloadLink(item), 167 | savedDir: tempDir.path, 168 | showNotification: false, 169 | openFileFromNotification: false, 170 | ).then((taskId) { 171 | File(tempFilePath).createSync(recursive: true); 172 | downloadsMap[item] = taskId; 173 | }); 174 | }); 175 | } 176 | } else { 177 | var bytes = file.readAsBytesSync(); 178 | var digest = md5.convert(bytes).toString(); 179 | var res = await client.get(Uri.parse(md5hashLink)); 180 | var list = json.decode(res.body) as List; 181 | list.forEach((f) => md5List.add(f as String)); 182 | for (var hash in md5List) { 183 | if (digest == hash) { 184 | File f = File(downloadedMetaDir(tempDir.path, item)); 185 | if (!f.existsSync()) f.createSync(recursive: true); 186 | printOut(item + ' downloaded'); 187 | if (files(arch).length - 1 == files(arch).indexOf(item)) { 188 | isZeroNetDownloadedm = true; 189 | } 190 | } 191 | } 192 | } 193 | } 194 | check(); 195 | } 196 | } 197 | 198 | unzip() async { 199 | downloadStatus = 0; 200 | for (var item in files(arch)) { 201 | downloadStatus = downloadStatus + 100; 202 | File f = File(tempDir.path + '/$item.zip'); 203 | File f2 = File(tempDir.path + '/$item.installing'); 204 | File f3 = File(tempDir.path + '/$item.installed'); 205 | zeroNetState = state.INSTALLING; 206 | if (!(f2.existsSync() && f3.existsSync())) { 207 | if (f.path.contains('usr')) 208 | _unzipBytes(item, f.readAsBytesSync(), dest: 'usr/'); 209 | else if (f.path.contains('site-packages')) 210 | _unzipBytes(item, f.readAsBytesSync(), dest: 'usr/lib/python3.8/'); 211 | else 212 | _unzipBytes(item, f.readAsBytesSync()); 213 | f2.createSync(recursive: true); 214 | } 215 | } 216 | isZeroNetInstalledm = await isZeroNetInstalled(); 217 | } 218 | 219 | unZipinBg() async { 220 | if (arch != null) 221 | for (var item in files(arch)) { 222 | File f = File(tempDir.path + '/$item.zip'); 223 | File f2 = File(installingMetaDir(tempDir.path, item, sesionKey)); 224 | zeroNetState = state.INSTALLING; 225 | if (!(f2.existsSync())) { 226 | f2.createSync(recursive: true); 227 | if (f.path.contains('usr')) { 228 | await compute( 229 | _unzipBytesAsync, 230 | UnzipParams( 231 | item, 232 | f.readAsBytesSync(), 233 | dest: 'usr/', 234 | ), 235 | ); 236 | } else if (f.path.contains('site_packages') || 237 | f.path.contains('lib-dynload')) { 238 | await compute( 239 | _unzipBytesAsync, 240 | UnzipParams( 241 | item, 242 | f.readAsBytesSync(), 243 | dest: 'usr/lib/python3.8/', 244 | ), 245 | ); 246 | } else if (f.path.contains('python38')) { 247 | await compute( 248 | _unzipBytesAsync, 249 | UnzipParams( 250 | item, 251 | f.readAsBytesSync(), 252 | dest: 'usr/lib/', 253 | ), 254 | ); 255 | } else { 256 | await compute( 257 | _unzipBytesAsync, 258 | UnzipParams( 259 | item, 260 | f.readAsBytesSync(), 261 | ), 262 | ); 263 | } 264 | } 265 | } 266 | // printOut('Dont Call this Function Twice'); 267 | // check(); 268 | } 269 | 270 | void _unzipBytesAsync(UnzipParams params) async { 271 | printOut("UnZippingFiles......."); 272 | var out = dataDir + '/' + params.dest; 273 | Archive archive = ZipDecoder().decodeBytes(params.bytes); 274 | int totalfiles = archive.length; 275 | int i = 0; 276 | for (ArchiveFile file in archive) { 277 | String filename = file.name; 278 | printOut('$out$filename'); 279 | i++; 280 | final SendPort send = IsolateNameServer.lookupPortByName(isolateUnZipPort); 281 | send.send([params.item, i, totalfiles]); 282 | String outName = '$out' + filename; 283 | if (file.isFile) { 284 | List data = file.content; 285 | // if (!File(outName).existsSync()) { 286 | File f = File(outName); 287 | if (!f.existsSync()) 288 | f 289 | ..createSync(recursive: true) 290 | ..writeAsBytesSync(data); 291 | // } 292 | } else { 293 | Directory(outName).exists().then((exists) { 294 | if (!exists) Directory(outName)..create(recursive: true); 295 | }); 296 | } 297 | } 298 | File(installedMetaDir(metaDir.path, params.item)).createSync(recursive: true); 299 | } 300 | 301 | int percentUnZip = 0; 302 | 303 | _unzipBytes(String name, List bytes, {String dest = ''}) async { 304 | printOut("UnZippingFiles......."); 305 | var out = dataDir + '/' + dest; 306 | Archive archive = ZipDecoder().decodeBytes(bytes); 307 | for (ArchiveFile file in archive) { 308 | String filename = file.name; 309 | printOut('$out$filename'); 310 | String outName = '$out' + filename; 311 | if (file.isFile) { 312 | List data = file.content; 313 | // if (!File(outName).existsSync()) { 314 | File(outName).exists().then((onValue) { 315 | if (!onValue) { 316 | File(outName) 317 | ..createSync(recursive: true) 318 | ..writeAsBytes(data).then((f) { 319 | bool isBin = f.path.contains('bin'); 320 | if (isBin) 321 | makeExec(f.path); 322 | else if (f.path.contains('.so')) makeExec(f.path); 323 | }); 324 | } 325 | }); 326 | // } 327 | } else { 328 | Directory(outName).exists().then((exists) { 329 | if (!exists) Directory(outName)..create(recursive: true); 330 | }); 331 | } 332 | } 333 | File(installedMetaDir(tempDir.path, name)).createSync(recursive: true); 334 | } 335 | 336 | Future makeExecHelper() async { 337 | for (var item in soDirs) { 338 | var dir = Directory(dataDir + '/$item'); 339 | if (dir.existsSync()) { 340 | var list = dir.listSync().where((element) { 341 | if (element is File) { 342 | if (element.path.contains('so')) return true; 343 | return false; 344 | } 345 | return false; 346 | }); 347 | for (var item in list) { 348 | if (item is File) { 349 | printOut(item.path); 350 | makeExec(item.path); 351 | } 352 | } 353 | } 354 | } 355 | return true; 356 | } 357 | 358 | makeExec(String path) => Process.runSync('chmod', ['744', path]); 359 | 360 | load() async { 361 | if (arch == null) await init(); 362 | isZeroNetInstalled().then((onValue) { 363 | isZeroNetInstalledm = onValue; 364 | if (!isZeroNetInstalledm) 365 | isZeroNetDownloaded().then((onValue) { 366 | isZeroNetDownloadedm = onValue; 367 | }); 368 | }); 369 | } 370 | 371 | Future isZeroNetInstalled() async { 372 | bool isExists = false; 373 | for (var item in files(arch)) { 374 | File f = File(installedMetaDir(metaDir.path, item)); 375 | var exists = f.existsSync(); 376 | if (!exists) return Future.value(exists); 377 | isExists = exists; 378 | } 379 | return isExists; 380 | } 381 | 382 | Future isZeroNetDownloaded() async { 383 | bool isExists = false; 384 | if (await isModuleInstallSupported()) { 385 | if (await isRequiredModulesInstalled()) { 386 | for (var item in files(arch)) { 387 | var i = files(arch).indexOf(item); 388 | bool f2 = File(tempDir.path + '/$item.zip').existsSync(); 389 | if (i == files(arch).length - 1) { 390 | isExists = f2; 391 | } else if (!f2) { 392 | return false; 393 | } 394 | } 395 | } 396 | } else { 397 | for (var item in files(arch)) { 398 | var i = binDirs.indexOf(item); 399 | bool f = File(downloadedMetaDir(tempDir.path, item)).existsSync(); 400 | // bool f1 = File(tempDir.path + '/$item.downloading').existsSync(); 401 | bool f2 = File(tempDir.path + '/$item.zip').existsSync(); 402 | bool isAllDownloaded = allFilesDownloaded(); 403 | var exists = ((f && f2) || isAllDownloaded); 404 | if (!exists) return Future.value(exists); 405 | if (i == binDirs.length - 1) { 406 | isExists = exists; 407 | } 408 | } 409 | varController.isZeroNetDownloaded(isExists); 410 | } 411 | return isExists; 412 | } 413 | 414 | checkForAppUpdates() async { 415 | DateTime time = DateTime.now(); 416 | var updateTimeEpoch = int.parse(await getAppLastUpdateTime()); 417 | var updateTime = DateTime.fromMillisecondsSinceEpoch(updateTimeEpoch); 418 | int updateDays; 419 | if (appVersion.contains('internal')) { 420 | updateDays = time.difference(updateTime).inSeconds; 421 | } else { 422 | updateDays = time.difference(updateTime).inDays; 423 | } 424 | if (updateDays > 3 && !kDebugMode) { 425 | if (kIsPlayStoreInstall) { 426 | AppUpdateInfo info = await InAppUpdate.checkForUpdate(); 427 | if (info.updateAvailability == UpdateAvailability.updateAvailable && 428 | info.flexibleUpdateAllowed) 429 | uiController.updateInAppUpdateAvailable(AppUpdate.AVAILABLE); 430 | } 431 | } 432 | } 433 | -------------------------------------------------------------------------------- /lib/widgets/about_page.dart: -------------------------------------------------------------------------------- 1 | import '../imports.dart'; 2 | import '../imports_conflicts.dart'; 3 | 4 | class AboutPage extends StatelessWidget { 5 | @override 6 | Widget build(BuildContext context) { 7 | return WillPopScope( 8 | onWillPop: () { 9 | uiController.updateCurrentAppRoute(AppRoute.Home); 10 | return Future.value(false); 11 | }, 12 | child: Container( 13 | child: SingleChildScrollView( 14 | child: Column( 15 | children: [ 16 | Padding(padding: EdgeInsets.all(24)), 17 | Padding( 18 | padding: const EdgeInsets.only(left: 18.0, right: 18.0), 19 | child: Column( 20 | crossAxisAlignment: CrossAxisAlignment.start, 21 | children: [ 22 | ZeroNetAppBar(), 23 | Padding( 24 | padding: const EdgeInsets.all(8.0), 25 | ), 26 | Row( 27 | children: [ 28 | Image.asset( 29 | logoPath, 30 | width: 100.0, 31 | height: 100.0, 32 | ), 33 | Padding(padding: const EdgeInsets.all(8.0)), 34 | Flexible( 35 | child: Container( 36 | child: Text( 37 | appDescription1, 38 | style: GoogleFonts.roboto( 39 | fontSize: 20.0, 40 | fontWeight: FontWeight.w500, 41 | height: 1.5, 42 | ), 43 | ), 44 | ), 45 | ), 46 | ], 47 | ), 48 | Container( 49 | child: Text.rich( 50 | TextSpan( 51 | text: appDescription2, 52 | style: GoogleFonts.roboto( 53 | fontSize: 20.0, 54 | fontWeight: FontWeight.w500, 55 | height: 1.5, 56 | ), 57 | children: [ 58 | TextSpan( 59 | text: zeronetWebLink, 60 | style: GoogleFonts.roboto( 61 | fontSize: 20.0, 62 | fontWeight: FontWeight.w500, 63 | height: 1.5, 64 | color: Color(0xFF8663FF), 65 | decoration: TextDecoration.underline, 66 | ), 67 | recognizer: TapGestureRecognizer() 68 | ..onTap = () { 69 | launch(zeronetWebLink); 70 | }, 71 | ), 72 | ], 73 | ), 74 | ), 75 | ), 76 | Padding( 77 | padding: const EdgeInsets.all(12.0), 78 | ), 79 | DeveloperWidget(), 80 | Padding( 81 | padding: const EdgeInsets.all(8.0), 82 | ), 83 | DonationWidget(), 84 | Padding( 85 | padding: const EdgeInsets.all(8.0), 86 | ), 87 | Text( 88 | contribute, 89 | style: GoogleFonts.roboto( 90 | fontSize: 20.0, 91 | fontWeight: FontWeight.bold, 92 | ), 93 | ), 94 | Padding( 95 | padding: const EdgeInsets.all(4.0), 96 | ), 97 | Text( 98 | contributeDescription, 99 | style: GoogleFonts.roboto( 100 | fontSize: 18.0, 101 | ), 102 | ), 103 | Padding( 104 | padding: const EdgeInsets.all(8.0), 105 | ), 106 | ], 107 | ), 108 | ) 109 | ], 110 | ), 111 | ), 112 | ), 113 | ); 114 | } 115 | } 116 | 117 | class DonationWidget extends StatelessWidget { 118 | @override 119 | Widget build(BuildContext context) { 120 | return Column( 121 | mainAxisSize: MainAxisSize.min, 122 | crossAxisAlignment: CrossAxisAlignment.start, 123 | children: [ 124 | Text( 125 | donationAddressTitle, 126 | style: GoogleFonts.roboto( 127 | fontSize: 18.0, 128 | fontWeight: FontWeight.bold, 129 | ), 130 | ), 131 | Padding( 132 | padding: const EdgeInsets.all(4.0), 133 | ), 134 | if (kEnableCryptoPurchases) 135 | LayoutBuilder( 136 | builder: (ctx, cons) { 137 | List children = []; 138 | for (var crypto in donationsAddressMap.keys) { 139 | children.add( 140 | Column( 141 | crossAxisAlignment: CrossAxisAlignment.start, 142 | children: [ 143 | Text( 144 | crypto, 145 | style: GoogleFonts.roboto( 146 | fontSize: 16.0, 147 | ), 148 | ), 149 | ClickableTextWidget( 150 | text: donationsAddressMap[crypto], 151 | textStyle: GoogleFonts.roboto( 152 | fontSize: 15.0, 153 | fontWeight: FontWeight.w500, 154 | height: 1.5, 155 | color: Color(0xFF8663FF), 156 | decoration: TextDecoration.underline, 157 | ), 158 | onClick: () { 159 | FlutterClipboard.copy(donationsAddressMap[crypto]); 160 | Scaffold.of(context).showSnackBar( 161 | SnackBar( 162 | content: Text( 163 | '$crypto Donation Address Copied to Clipboard', 164 | ), 165 | ), 166 | ); 167 | }, 168 | ), 169 | Padding( 170 | padding: const EdgeInsets.all(6.0), 171 | ) 172 | ], 173 | ), 174 | ); 175 | } 176 | return Column( 177 | crossAxisAlignment: CrossAxisAlignment.start, 178 | children: children, 179 | ); 180 | }, 181 | ), 182 | if (kEnableCryptoPurchases) 183 | Flexible( 184 | child: Text( 185 | clickAddToCopy, 186 | style: GoogleFonts.roboto( 187 | fontSize: 16.0, 188 | ), 189 | ), 190 | ), 191 | if (kEnableCryptoPurchases) 192 | Padding( 193 | padding: const EdgeInsets.all(8.0), 194 | ), 195 | if (kEnableInAppPurchases) GooglePlayInAppPurchases(), 196 | Padding( 197 | padding: const EdgeInsets.all(8.0), 198 | ), 199 | Flexible( 200 | child: Text( 201 | donationDescription, 202 | style: GoogleFonts.roboto( 203 | fontSize: 16.0, 204 | ), 205 | ), 206 | ) 207 | ], 208 | ); 209 | } 210 | } 211 | 212 | class DeveloperWidget extends StatelessWidget { 213 | @override 214 | Widget build(BuildContext context) { 215 | return Column( 216 | crossAxisAlignment: CrossAxisAlignment.start, 217 | children: [ 218 | Text( 219 | developers, 220 | style: GoogleFonts.roboto( 221 | fontSize: 20.0, 222 | fontWeight: FontWeight.bold, 223 | ), 224 | ), 225 | Padding(padding: const EdgeInsets.all(4.0)), 226 | LayoutBuilder( 227 | builder: (ctx, cons) { 228 | List children = []; 229 | for (var developer in appDevelopers) { 230 | children.add( 231 | Card( 232 | shape: RoundedRectangleBorder( 233 | borderRadius: BorderRadius.circular(12.0), 234 | ), 235 | elevation: 4.0, 236 | child: Container( 237 | padding: const EdgeInsets.only(left: 8.0, right: 8.0), 238 | decoration: BoxDecoration( 239 | borderRadius: BorderRadius.circular(12.0), 240 | ), 241 | child: Row( 242 | children: [ 243 | Padding( 244 | padding: const EdgeInsets.all(8.0), 245 | child: CircleAvatar( 246 | radius: 30.0, 247 | backgroundImage: 248 | ExactAssetImage(developer.profileIconLink), 249 | ), 250 | ), 251 | Padding( 252 | padding: const EdgeInsets.all(8.0), 253 | child: Column( 254 | children: [ 255 | Text( 256 | developer.name + '(${developer.developerType})', 257 | style: GoogleFonts.roboto( 258 | fontSize: 20.0, 259 | fontWeight: FontWeight.w500, 260 | ), 261 | ), 262 | LayoutBuilder(builder: (context, cons) { 263 | List children = []; 264 | final iconsPath = 'assets/icons'; 265 | List assets = [ 266 | '$iconsPath/github_dark.png', 267 | '$iconsPath/twitter_dark.png', 268 | '$iconsPath/facebook_dark.png', 269 | ]; 270 | List links = [ 271 | developer.githubLink, 272 | developer.twitterLink, 273 | developer.facebookLink, 274 | ]; 275 | for (var item in assets) { 276 | children.add(InkWell( 277 | onTap: () { 278 | var i = assets.indexOf(item); 279 | launch(links[i]); 280 | }, 281 | child: Padding( 282 | padding: const EdgeInsets.all(6.0), 283 | child: Image.asset( 284 | item, 285 | height: 30.0, 286 | width: 30.0, 287 | ), 288 | ), 289 | )); 290 | } 291 | return Row( 292 | children: children, 293 | ); 294 | }) 295 | ], 296 | ), 297 | ) 298 | ], 299 | ), 300 | ), 301 | ), 302 | ); 303 | } 304 | return Column( 305 | children: children, 306 | ); 307 | }, 308 | ), 309 | ], 310 | ); 311 | } 312 | } 313 | 314 | class GooglePlayInAppPurchases extends StatelessWidget { 315 | @override 316 | Widget build(BuildContext context) { 317 | return Container( 318 | child: Column( 319 | crossAxisAlignment: CrossAxisAlignment.start, 320 | children: [ 321 | Text.rich( 322 | TextSpan( 323 | text: 'Google Play Purchases ', 324 | children: [ 325 | TextSpan( 326 | text: '(30% taken by Google) :', 327 | style: GoogleFonts.roboto( 328 | fontSize: 14.0, 329 | ), 330 | ), 331 | ], 332 | style: GoogleFonts.roboto( 333 | fontSize: 18.0, 334 | fontWeight: FontWeight.bold, 335 | ), 336 | ), 337 | ), 338 | Obx(() { 339 | List mChildren = []; 340 | Map> googlePurchasesTypes = { 341 | 'One Time': purchasesController.oneTimePurchases, 342 | 'Monthly Subscriptions': purchasesController.subscriptions, 343 | }; 344 | for (var item in googlePurchasesTypes.keys) { 345 | List purchases = googlePurchasesTypes[item]; 346 | purchases 347 | ..sort((item1, item2) { 348 | int item1I1 = item1.identifier.lastIndexOf('_') + 1; 349 | int item1I2 = item1.identifier.lastIndexOf('.'); 350 | String item1PriceStr = 351 | item1.identifier.substring(item1I1, item1I2); 352 | int item1Price = int.parse(item1PriceStr); 353 | int item2I1 = item2.identifier.lastIndexOf('_') + 1; 354 | int item2I2 = item2.identifier.lastIndexOf('.'); 355 | String item2PriceStr = 356 | item2.identifier.substring(item2I1, item2I2); 357 | int item2Price = int.parse(item2PriceStr); 358 | return item1Price < item2Price ? -1 : 1; 359 | }); 360 | if (purchases.length > 0) { 361 | List children = []; 362 | for (var package in purchases) { 363 | var i = purchases.indexOf(package); 364 | Color c = Color(0xFF); 365 | String label = ''; 366 | switch (i) { 367 | case 0: 368 | label = 'Tip'; 369 | c = Color(0xFF06CAB6); 370 | break; 371 | case 1: 372 | label = 'Coffee'; 373 | c = Color(0xFF0696CA); 374 | break; 375 | case 2: 376 | label = 'Lunch'; 377 | c = Color(0xFFCA067B); 378 | break; 379 | default: 380 | } 381 | children.add( 382 | RaisedButton( 383 | shape: RoundedRectangleBorder( 384 | borderRadius: BorderRadius.circular(8.0), 385 | ), 386 | child: Padding( 387 | padding: const EdgeInsets.only( 388 | left: 16.0, 389 | right: 16.0, 390 | top: 8.0, 391 | bottom: 8.0, 392 | ), 393 | child: Text( 394 | "$label(${package.product.priceString})", 395 | style: GoogleFonts.roboto( 396 | fontSize: 16.0, 397 | fontWeight: FontWeight.w500, 398 | color: Colors.white, 399 | ), 400 | ), 401 | ), 402 | color: c, 403 | onPressed: () => purchasePackage(package), 404 | ), 405 | ); 406 | } 407 | mChildren.add( 408 | Column( 409 | children: [ 410 | Padding(padding: const EdgeInsets.all(8.0)), 411 | Text( 412 | item, 413 | style: GoogleFonts.roboto( 414 | fontSize: 16.0, 415 | fontWeight: FontWeight.w500, 416 | ), 417 | ), 418 | Padding(padding: const EdgeInsets.all(8.0)), 419 | Center( 420 | child: Wrap( 421 | spacing: 25.0, 422 | runSpacing: 10.0, 423 | alignment: WrapAlignment.spaceEvenly, 424 | children: children, 425 | ), 426 | ) 427 | ], 428 | ), 429 | ); 430 | } 431 | } 432 | return Column( 433 | children: mChildren, 434 | ); 435 | }) 436 | ], 437 | ), 438 | ); 439 | } 440 | } 441 | -------------------------------------------------------------------------------- /lib/widgets/common.dart: -------------------------------------------------------------------------------- 1 | import '../imports.dart'; 2 | import '../imports_conflicts.dart'; 3 | 4 | class ZeroNetAppBar extends StatelessWidget { 5 | const ZeroNetAppBar({ 6 | Key key, 7 | }) : super(key: key); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Obx(() { 12 | return Row( 13 | crossAxisAlignment: CrossAxisAlignment.center, 14 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 15 | children: [ 16 | Text( 17 | uiController.currentAppRoute.value.title, 18 | style: GoogleFonts.roboto( 19 | fontSize: 32.0, 20 | fontWeight: FontWeight.bold, 21 | ), 22 | ), 23 | Row( 24 | children: [ 25 | if (uiController.currentAppRoute.value == AppRoute.Settings) 26 | InkWell( 27 | child: Icon( 28 | OMIcons.info, 29 | size: 32.0, 30 | color: Colors.black, 31 | ), 32 | onTap: () => 33 | uiController.updateCurrentAppRoute(AppRoute.AboutPage), 34 | ), 35 | if (uiController.currentAppRoute.value == AppRoute.Settings) 36 | Padding(padding: const EdgeInsets.only(right: 20.0)), 37 | InkWell( 38 | child: Icon( 39 | uiController.currentAppRoute.value.icon, 40 | size: 32.0, 41 | color: Colors.black, 42 | ), 43 | onTap: uiController.currentAppRoute.value.onClick, 44 | ) 45 | ], 46 | ) 47 | ], 48 | ); 49 | }); 50 | } 51 | } 52 | 53 | class ClickableTextWidget extends StatelessWidget { 54 | ClickableTextWidget({ 55 | this.text, 56 | this.textStyle, 57 | this.onClick, 58 | }); 59 | 60 | final String text; 61 | final TextStyle textStyle; 62 | final VoidCallback onClick; 63 | 64 | @override 65 | Widget build(BuildContext context) { 66 | return Text.rich( 67 | TextSpan( 68 | text: text, 69 | style: textStyle, 70 | recognizer: TapGestureRecognizer()..onTap = onClick, 71 | ), 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/widgets/home_page.dart: -------------------------------------------------------------------------------- 1 | import '../imports.dart'; 2 | import '../imports_conflicts.dart'; 3 | 4 | class HomePage extends StatelessWidget { 5 | @override 6 | Widget build(BuildContext context) { 7 | return Container( 8 | height: MediaQuery.of(context).size.height, 9 | child: SingleChildScrollView( 10 | child: Column( 11 | crossAxisAlignment: CrossAxisAlignment.start, 12 | mainAxisSize: MainAxisSize.max, 13 | children: [ 14 | Padding(padding: EdgeInsets.all(24)), 15 | Padding( 16 | padding: const EdgeInsets.only(left: 18.0, right: 18.0), 17 | child: Column( 18 | children: [ 19 | ZeroNetAppBar(), 20 | Padding( 21 | padding: EdgeInsets.only(bottom: 30), 22 | ), 23 | ProjectsView(), 24 | Padding( 25 | padding: EdgeInsets.only(bottom: 15), 26 | ), 27 | // PopularZeroNetSites(), 28 | // Padding( 29 | // padding: EdgeInsets.only(bottom: 5), 30 | // ), 31 | InAppUpdateWidget(), 32 | if (kIsPlayStoreInstall) 33 | Padding( 34 | padding: EdgeInsets.only(bottom: 15), 35 | ), 36 | if (kIsPlayStoreInstall) RatingButtonWidget(), 37 | Padding( 38 | padding: EdgeInsets.only(bottom: 15), 39 | ), 40 | AboutButtonWidget(), 41 | Padding( 42 | padding: EdgeInsets.only(bottom: 15), 43 | ), 44 | ], 45 | ), 46 | ), 47 | ], 48 | ), 49 | ), 50 | ); 51 | } 52 | } 53 | 54 | class InAppUpdateWidget extends StatelessWidget { 55 | @override 56 | Widget build(BuildContext context) { 57 | return Obx(() { 58 | if (uiController.appUpdate.value != AppUpdate.NOT_AVAILABLE) 59 | return Row( 60 | mainAxisSize: MainAxisSize.max, 61 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 62 | children: [ 63 | Text( 64 | appUpdateAvailable, 65 | style: GoogleFonts.roboto( 66 | fontSize: 20.0, 67 | fontWeight: FontWeight.w500, 68 | ), 69 | ), 70 | RaisedButton( 71 | onPressed: uiController.appUpdate.value.action, 72 | color: Color(0xFF008297), 73 | padding: EdgeInsets.only(left: 10, right: 10), 74 | shape: RoundedRectangleBorder( 75 | borderRadius: BorderRadius.circular(30.0), 76 | ), 77 | child: Obx(() { 78 | return Text( 79 | uiController.appUpdate.value.text, 80 | style: GoogleFonts.roboto( 81 | fontSize: 20.0, 82 | fontWeight: FontWeight.w500, 83 | color: Colors.white, 84 | ), 85 | ); 86 | }), 87 | ), 88 | ], 89 | ); 90 | return Container(); 91 | }); 92 | } 93 | } 94 | 95 | class AboutButtonWidget extends StatelessWidget { 96 | @override 97 | Widget build(BuildContext context) { 98 | return RaisedButton( 99 | onPressed: () => uiController.updateCurrentAppRoute(AppRoute.AboutPage), 100 | color: Color(0xFFAA5297), 101 | padding: EdgeInsets.only(top: 10, bottom: 10, left: 30, right: 30), 102 | shape: RoundedRectangleBorder( 103 | borderRadius: BorderRadius.circular(30.0), 104 | ), 105 | child: Text( 106 | knowMore, 107 | style: GoogleFonts.roboto( 108 | fontSize: 16.0, 109 | fontWeight: FontWeight.normal, 110 | color: Colors.white, 111 | ), 112 | ), 113 | ); 114 | } 115 | } 116 | 117 | class RatingButtonWidget extends StatelessWidget { 118 | @override 119 | Widget build(BuildContext context) { 120 | return RaisedButton( 121 | onPressed: () async { 122 | final InAppReview inAppReview = InAppReview.instance; 123 | //TODO: remove this once we support non playstore reviews. 124 | if (await inAppReview.isAvailable() && kIsPlayStoreInstall) { 125 | inAppReview.requestReview(); 126 | } else { 127 | //TODO: Handle this case. eg: Non-PlayStore Install, Already Reviewed Users etc. 128 | } 129 | }, 130 | color: Color(0xFF008297), 131 | padding: EdgeInsets.only(top: 10, bottom: 10, left: 30, right: 30), 132 | shape: RoundedRectangleBorder( 133 | borderRadius: BorderRadius.circular(30.0), 134 | ), 135 | child: Text( 136 | feedbackStr, 137 | style: GoogleFonts.roboto( 138 | fontSize: 16.0, 139 | fontWeight: FontWeight.normal, 140 | color: Colors.white, 141 | ), 142 | ), 143 | ); 144 | } 145 | } 146 | 147 | final TextEditingController _pathController = TextEditingController( 148 | text: 'Projects/', 149 | ); 150 | 151 | class ProjectsView extends StatelessWidget { 152 | const ProjectsView({Key key}) : super(key: key); 153 | 154 | @override 155 | Widget build(BuildContext context) { 156 | return Column( 157 | crossAxisAlignment: CrossAxisAlignment.start, 158 | children: [ 159 | Text( 160 | 'Projects', 161 | style: TextStyle( 162 | fontSize: 24.0, 163 | fontWeight: FontWeight.bold, 164 | ), 165 | ), 166 | SizedBox( 167 | child: Obx( 168 | () { 169 | var projects = projectController.projects; 170 | var length = projects.length; 171 | return GridView.builder( 172 | itemCount: length, 173 | shrinkWrap: true, 174 | physics: NeverScrollableScrollPhysics(), 175 | itemBuilder: (c, i) { 176 | var project = projects[i]; 177 | return InkWell( 178 | onTap: project.id == null 179 | ? () { 180 | Get.bottomSheet( 181 | SizedBox( 182 | child: Container( 183 | padding: EdgeInsets.only( 184 | bottom: MediaQuery.of(context) 185 | .viewInsets 186 | .bottom), 187 | color: Colors.white, 188 | child: Container( 189 | margin: EdgeInsets.only( 190 | left: 18.0, 191 | right: 18.0, 192 | ), 193 | child: Column( 194 | children: [ 195 | SizedBox( 196 | height: 60.0, 197 | ), 198 | Text( 199 | 'Create New Project', 200 | style: TextStyle( 201 | fontSize: 24.0, 202 | fontWeight: FontWeight.bold, 203 | ), 204 | ), 205 | SizedBox( 206 | height: 25.0, 207 | ), 208 | Container( 209 | width: Get.width, 210 | child: Row( 211 | children: [ 212 | Text( 213 | 'Name : ', 214 | style: TextStyle( 215 | fontSize: 18.0, 216 | fontWeight: FontWeight.bold, 217 | ), 218 | ), 219 | Spacer(), 220 | Container( 221 | width: Get.width * 0.72, 222 | child: TextFormField( 223 | style: TextStyle( 224 | fontSize: 18.0, 225 | ), 226 | onChanged: (text) { 227 | projectController 228 | .createdNewProjectName 229 | .value = text; 230 | projectController 231 | .createdNewProjectPath 232 | .value = 233 | 'Projects/' + text; 234 | _pathController.text = 235 | projectController 236 | .createdNewProjectPath 237 | .value; 238 | }, 239 | ), 240 | ), 241 | ], 242 | ), 243 | ), 244 | SizedBox( 245 | height: 25.0, 246 | ), 247 | Container( 248 | width: Get.width, 249 | child: Row( 250 | children: [ 251 | Text( 252 | 'Path : ', 253 | style: TextStyle( 254 | fontSize: 18.0, 255 | fontWeight: FontWeight.bold, 256 | ), 257 | ), 258 | Spacer(), 259 | Container( 260 | width: Get.width * 0.72, 261 | child: TextFormField( 262 | controller: _pathController, 263 | onChanged: (text) { 264 | projectController 265 | .createdNewProjectPath 266 | .value = text; 267 | }, 268 | decoration: InputDecoration( 269 | hintText: 270 | '(Optional, You can leave this as empty)', 271 | ), 272 | ), 273 | ), 274 | ], 275 | ), 276 | ), 277 | SizedBox( 278 | height: 25.0, 279 | ), 280 | Container( 281 | width: Get.width, 282 | child: Row( 283 | children: [ 284 | Text( 285 | 'Template : ', 286 | style: TextStyle( 287 | fontSize: 18.0, 288 | fontWeight: FontWeight.bold, 289 | ), 290 | ), 291 | Spacer(), 292 | Obx( 293 | () => DropdownButton( 294 | value: projectController 295 | .currentTemplate.value, 296 | items: projectController 297 | .templates 298 | .map( 299 | (e) => DropdownMenuItem( 300 | child: Text(e.name), 301 | value: e.fileName, 302 | ), 303 | ) 304 | .toList(), 305 | onChanged: (v) { 306 | projectController 307 | .currentTemplate 308 | .value = v; 309 | }, 310 | ), 311 | ), 312 | ], 313 | ), 314 | ), 315 | Spacer(), 316 | Container( 317 | width: Get.width, 318 | margin: const EdgeInsets.only( 319 | bottom: 18.0, 320 | ), 321 | child: Row( 322 | children: [ 323 | ElevatedButton( 324 | onPressed: () { 325 | _pathController.clear(); 326 | Get.back(); 327 | }, 328 | child: Text('Cancel'), 329 | ), 330 | Spacer(), 331 | Obx( 332 | () { 333 | var projName = 334 | projectController 335 | .createdNewProjectName 336 | .value; 337 | return ElevatedButton( 338 | onPressed: projName.isEmpty 339 | ? null 340 | : () { 341 | projectController 342 | .createProject( 343 | projectController 344 | .createdNewProjectName 345 | .value, 346 | projectController 347 | .createdNewProjectPath 348 | .value, 349 | ); 350 | _pathController 351 | .clear(); 352 | Get.back(); 353 | }, 354 | child: Text('Create'), 355 | ); 356 | }, 357 | ) 358 | ], 359 | ), 360 | ), 361 | Padding( 362 | padding: const EdgeInsets.only( 363 | bottom: 18.0, 364 | ), 365 | child: Text( 366 | 'Enter Your Project Details to Continue', 367 | ), 368 | ) 369 | ], 370 | ), 371 | ), 372 | ), 373 | ), 374 | isScrollControlled: true, 375 | ); 376 | } 377 | : () { 378 | projectViewController.currentProject = project; 379 | uiController.updateCurrentAppRoute( 380 | AppRoute.ProjectViewPage, 381 | ); 382 | }, 383 | child: Container( 384 | decoration: BoxDecoration( 385 | border: Border.all(), 386 | borderRadius: BorderRadius.circular(6.0), 387 | ), 388 | child: project.id == null 389 | ? Column( 390 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 391 | children: [ 392 | Spacer(flex: 2), 393 | Icon(Icons.add), 394 | Spacer(flex: 1), 395 | Text( 396 | 'Create New Project', 397 | style: TextStyle( 398 | fontWeight: FontWeight.bold, 399 | ), 400 | ), 401 | Spacer(), 402 | ], 403 | ) 404 | : Stack( 405 | children: [ 406 | Align( 407 | alignment: Alignment.topRight, 408 | child: IconButton( 409 | onPressed: () { 410 | Get.bottomSheet( 411 | Container( 412 | color: Colors.white, 413 | child: Container( 414 | margin: EdgeInsets.only( 415 | left: 18.0, 416 | right: 18.0, 417 | ), 418 | child: Builder( 419 | builder: (context) { 420 | List children = [ 421 | SizedBox( 422 | height: 25.0, 423 | ), 424 | Center( 425 | child: Text( 426 | 'Project Details', 427 | style: TextStyle( 428 | fontSize: 18.0, 429 | fontWeight: 430 | FontWeight.bold, 431 | ), 432 | ), 433 | ), 434 | ]; 435 | for (var item 436 | in project.propsNames) { 437 | var value = project.props[ 438 | project.propsNames 439 | .indexOf( 440 | item, 441 | )] 442 | .toString(); 443 | if (item == 'path') { 444 | if (value.startsWith( 445 | appPrivDir.path)) { 446 | value = value.substring( 447 | appPrivDir 448 | .path.length + 449 | 1); 450 | } 451 | } 452 | children.add(SizedBox( 453 | height: 25.0, 454 | )); 455 | children.add( 456 | Row( 457 | children: [ 458 | Text( 459 | '$item : ' 460 | .capitalizeFirst, 461 | style: TextStyle( 462 | fontSize: 18.0, 463 | fontWeight: 464 | FontWeight.bold, 465 | ), 466 | ), 467 | Spacer(), 468 | Container( 469 | constraints: 470 | BoxConstraints( 471 | maxWidth: 472 | Get.width * 473 | 0.60, 474 | ), 475 | child: Text( 476 | value, 477 | overflow: 478 | TextOverflow 479 | .clip, 480 | style: TextStyle( 481 | fontSize: 16.0, 482 | ), 483 | ), 484 | ), 485 | ], 486 | ), 487 | ); 488 | } 489 | children.addAll([ 490 | Spacer(), 491 | Container( 492 | width: Get.width, 493 | margin: 494 | const EdgeInsets.only( 495 | bottom: 18.0, 496 | ), 497 | child: Row( 498 | children: [ 499 | ElevatedButton( 500 | onPressed: () => 501 | Get.back(), 502 | child: Text('Cancel'), 503 | ), 504 | Spacer(), 505 | ElevatedButton( 506 | onPressed: () { 507 | projectController 508 | .removeProject( 509 | project.id, 510 | ); 511 | Get.back(); 512 | }, 513 | child: Text('Delete'), 514 | ), 515 | Spacer(), 516 | ElevatedButton( 517 | onPressed: () { 518 | projectViewController 519 | .currentProject = 520 | project; 521 | uiController 522 | .updateCurrentAppRoute( 523 | AppRoute 524 | .ProjectViewPage, 525 | ); 526 | Get.back(); 527 | }, 528 | child: Text('Open'), 529 | ), 530 | ], 531 | ), 532 | ), 533 | ]); 534 | return Column( 535 | crossAxisAlignment: 536 | CrossAxisAlignment.start, 537 | children: children, 538 | ); 539 | }, 540 | ), 541 | ), 542 | ), 543 | ); 544 | }, 545 | icon: Icon( 546 | Icons.info_outline_rounded, 547 | ), 548 | ), 549 | ), 550 | Center( 551 | child: Container( 552 | child: Column( 553 | mainAxisAlignment: 554 | MainAxisAlignment.center, 555 | children: [ 556 | Text( 557 | project.name.capitalizeFirstofEach, 558 | textAlign: TextAlign.center, 559 | style: TextStyle( 560 | fontWeight: FontWeight.w500, 561 | ), 562 | ), 563 | ], 564 | ), 565 | ), 566 | ), 567 | ], 568 | ), 569 | ), 570 | ); 571 | }, 572 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 573 | mainAxisSpacing: 6.0, 574 | crossAxisSpacing: 6.0, 575 | crossAxisCount: 2, 576 | ), 577 | ); 578 | }, 579 | ), 580 | ), 581 | ], 582 | ); 583 | } 584 | } 585 | -------------------------------------------------------------------------------- /lib/widgets/loading_page.dart: -------------------------------------------------------------------------------- 1 | import '../imports_conflicts.dart'; 2 | import '../imports.dart'; 3 | 4 | class Loading extends StatelessWidget { 5 | @override 6 | Widget build(BuildContext context) { 7 | SystemChrome.setEnabledSystemUIOverlays([]); 8 | check(); 9 | return Scaffold( 10 | body: Center( 11 | child: Column( 12 | mainAxisAlignment: MainAxisAlignment.center, 13 | children: [ 14 | Padding( 15 | padding: EdgeInsets.only( 16 | left: Get.width / 12, 17 | right: Get.width / 12, 18 | ), 19 | child: Image.asset(logoPath), 20 | ), 21 | Padding( 22 | padding: EdgeInsets.all(24.0), 23 | ), 24 | Obx( 25 | () { 26 | var status = varController.loadingStatus.value; 27 | return Text( 28 | status, 29 | style: TextStyle( 30 | fontSize: 24.0, 31 | fontStyle: FontStyle.italic, 32 | ), 33 | ); 34 | }, 35 | ), 36 | Obx(() { 37 | var percent = varController.loadingPercent.value; 38 | return (percent < 1) 39 | ? CircularProgressIndicator() 40 | : Text( 41 | '($percent%)', 42 | style: TextStyle( 43 | fontSize: 18.0, 44 | fontStyle: FontStyle.italic, 45 | ), 46 | ); 47 | }), 48 | Text( 49 | warning, 50 | style: TextStyle( 51 | fontSize: 12.0, 52 | fontStyle: FontStyle.italic, 53 | ), 54 | ), 55 | ], 56 | ), 57 | ), 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/widgets/log_page.dart: -------------------------------------------------------------------------------- 1 | import '../imports.dart'; 2 | import '../imports_conflicts.dart'; 3 | 4 | class ZeroNetLogPage extends StatelessWidget { 5 | @override 6 | Widget build(BuildContext context) { 7 | return Column( 8 | children: [ 9 | Padding(padding: EdgeInsets.all(24)), 10 | Padding( 11 | padding: const EdgeInsets.only(left: 18.0, right: 18.0), 12 | child: Column( 13 | children: [ 14 | ZeroNetAppBar(), 15 | Padding( 16 | padding: EdgeInsets.only(bottom: 30), 17 | ), 18 | Container( 19 | height: MediaQuery.of(context).size.height * 0.83, 20 | child: SingleChildScrollView( 21 | child: Obx( 22 | () => Text(varController.zeroNetLog.value), 23 | ), 24 | ), 25 | ), 26 | ], 27 | ), 28 | ), 29 | ], 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/widgets/project_view_page.dart: -------------------------------------------------------------------------------- 1 | import '../imports.dart'; 2 | import '../imports_conflicts.dart'; 3 | 4 | class ProjectViewPage extends StatelessWidget { 5 | @override 6 | Widget build(BuildContext context) { 7 | return Stack( 8 | children: [ 9 | Container( 10 | height: MediaQuery.of(context).size.height, 11 | child: SingleChildScrollView( 12 | child: Column( 13 | crossAxisAlignment: CrossAxisAlignment.start, 14 | mainAxisSize: MainAxisSize.max, 15 | children: [ 16 | Padding(padding: EdgeInsets.all(24)), 17 | Padding( 18 | padding: const EdgeInsets.only(left: 18.0, right: 18.0), 19 | child: Column( 20 | children: [ 21 | ZeroNetAppBar(), 22 | Padding( 23 | padding: EdgeInsets.only(bottom: 10), 24 | ), 25 | Obx(() { 26 | var openedFiles = projectViewController.openedFiles; 27 | var length = openedFiles.length; 28 | return Container( 29 | width: Get.width, 30 | height: 40.0, 31 | child: ListView.builder( 32 | physics: BouncingScrollPhysics(), 33 | shrinkWrap: true, 34 | itemCount: openedFiles.isEmpty ? 1 : length, 35 | scrollDirection: Axis.horizontal, 36 | itemBuilder: (ctx, i) { 37 | if (openedFiles.isEmpty) 38 | return ProjectFileTabView(); 39 | return ProjectFileTabView( 40 | projectFile: openedFiles[i], 41 | ); 42 | }, 43 | ), 44 | ); 45 | }), 46 | Obx(() { 47 | var currentFile = 48 | projectViewController.currentFile.value; 49 | return Column( 50 | children: [ 51 | if (currentFile.name == null) 52 | ProjectFileExplorerView() 53 | else 54 | ProjectFileView(), 55 | ], 56 | ); 57 | }) 58 | ], 59 | ), 60 | ), 61 | ], 62 | ), 63 | ), 64 | ), 65 | Align( 66 | alignment: Alignment.bottomRight, 67 | child: Container( 68 | margin: EdgeInsets.only( 69 | bottom: 20.0, 70 | right: 20.0, 71 | ), 72 | child: FloatingActionButton.extended( 73 | onPressed: () { 74 | switch (processController.status.value) { 75 | case ProcessStatus.Idle: 76 | Timer(Duration(milliseconds: 500), () { 77 | processController.clearConsoleLog(); 78 | processController 79 | .changeProcessStatus(ProcessStatus.Running); 80 | var pPath = ''; 81 | if (projectViewController.currentFile.value.name == 82 | null || 83 | projectViewController.currentFile.value.isPropsFile) { 84 | pPath = projectViewController.currentProject.path + 85 | '/' + 86 | projectViewController.currentProject.program; 87 | } else { 88 | pPath = projectViewController.currentFile.value.path; 89 | } 90 | processController.runProcess(pPath); 91 | }); 92 | Get.bottomSheet( 93 | ConsoleLogWidget(), 94 | ).whenComplete(() => processController.clearConsoleLog()); 95 | break; 96 | case ProcessStatus.Running: 97 | processController.changeProcessStatus(ProcessStatus.Paused); 98 | break; 99 | case ProcessStatus.Paused: 100 | processController.changeProcessStatus(ProcessStatus.Stoped); 101 | break; 102 | case ProcessStatus.Stoped: 103 | processController 104 | .changeProcessStatus(ProcessStatus.Completed); 105 | break; 106 | case ProcessStatus.Completed: 107 | processController.changeProcessStatus(ProcessStatus.Idle); 108 | break; 109 | default: 110 | } 111 | }, 112 | label: Row( 113 | children: [ 114 | Obx(() { 115 | return processController.status.value.icon; 116 | }), 117 | SizedBox( 118 | width: 10.0, 119 | ), 120 | Obx(() => Text(processController.status.value.text)), 121 | ], 122 | ), 123 | ), 124 | ), 125 | ), 126 | ], 127 | ); 128 | } 129 | } 130 | 131 | class ConsoleLogWidget extends StatelessWidget { 132 | const ConsoleLogWidget({ 133 | Key key, 134 | }) : super(key: key); 135 | 136 | @override 137 | Widget build(BuildContext context) { 138 | return Container( 139 | color: Colors.white, 140 | padding: const EdgeInsets.all(16.0), 141 | constraints: BoxConstraints( 142 | minHeight: Get.height * .25, 143 | ), 144 | child: Column( 145 | mainAxisAlignment: MainAxisAlignment.start, 146 | crossAxisAlignment: CrossAxisAlignment.start, 147 | children: [ 148 | Center( 149 | child: Column( 150 | mainAxisSize: MainAxisSize.min, 151 | children: [ 152 | Container( 153 | height: 8.0, 154 | width: Get.width * 0.25, 155 | margin: const EdgeInsets.only(bottom: 8.0), 156 | decoration: BoxDecoration( 157 | color: Colors.black38, 158 | borderRadius: BorderRadius.circular(5.0), 159 | ), 160 | ), 161 | Text( 162 | 'Console Log', 163 | style: TextStyle( 164 | fontSize: 24.0, 165 | fontWeight: FontWeight.bold, 166 | ), 167 | ), 168 | ], 169 | ), 170 | ), 171 | Obx( 172 | () => Text( 173 | processController.consoleLog.value.isEmpty 174 | ? 'Running..., Please Wait!' 175 | : processController.consoleLog.value, 176 | ), 177 | ), 178 | ], 179 | ), 180 | ); 181 | } 182 | } 183 | 184 | class ProjectFileTabView extends StatelessWidget { 185 | final ProjectFile projectFile; 186 | const ProjectFileTabView({ 187 | Key key, 188 | this.projectFile, 189 | }) : super(key: key); 190 | 191 | @override 192 | Widget build(BuildContext context) { 193 | return Container( 194 | margin: const EdgeInsets.only(right: 1.0), 195 | decoration: BoxDecoration( 196 | color: Colors.grey.shade300, 197 | borderRadius: BorderRadius.only( 198 | topLeft: Radius.circular(8.0), 199 | topRight: Radius.circular(16.0), 200 | ), 201 | ), 202 | child: Row( 203 | children: [ 204 | Padding( 205 | padding: const EdgeInsets.only( 206 | left: 16.0, 207 | right: 0.0, 208 | ), 209 | child: Text( 210 | projectFile == null ? 'Project Files' : projectFile.name, 211 | textAlign: TextAlign.center, 212 | ), 213 | ), 214 | IconButton( 215 | onPressed: () { 216 | projectViewController.closeFile(projectFile); 217 | }, 218 | icon: Icon(Icons.close), 219 | ) 220 | ], 221 | ), 222 | ); 223 | } 224 | } 225 | 226 | class ProjectFileExplorerView extends StatelessWidget { 227 | const ProjectFileExplorerView({Key key}) : super(key: key); 228 | 229 | @override 230 | Widget build(BuildContext context) { 231 | return Container( 232 | height: Get.height - 140.0, 233 | width: Get.width, 234 | color: Colors.grey.shade300, 235 | child: Column( 236 | children: [ 237 | Builder( 238 | builder: (ctx) { 239 | List files = []; 240 | Directory projectDir = 241 | Directory(projectViewController.currentProject.path); 242 | var filesList = projectDir.listSync(); 243 | for (var item in filesList) { 244 | files.add( 245 | ProjectFile( 246 | name: item.name(), 247 | path: item.path, 248 | ), 249 | ); 250 | } 251 | return ListView.builder( 252 | shrinkWrap: true, 253 | itemCount: files.length, 254 | physics: NeverScrollableScrollPhysics(), 255 | itemBuilder: (ctx, i) { 256 | return Material( 257 | type: MaterialType.transparency, 258 | child: InkWell( 259 | onTap: () { 260 | projectViewController.openFile(files[i]); 261 | }, 262 | child: Container( 263 | margin: const EdgeInsets.only( 264 | top: 8.0, 265 | bottom: 8.0, 266 | left: 16.0, 267 | right: 16.0, 268 | ), 269 | height: 25, 270 | child: Builder( 271 | builder: (ctx) { 272 | bool isFileIconSupported = false; 273 | FileIcon fileIcon; 274 | var fileIconList = 275 | projectController.fileIcons.where( 276 | (element) => element.supportedExt.contains( 277 | File(files[i].path).fileExtension, 278 | ), 279 | ); 280 | if (fileIconList.isNotEmpty) { 281 | fileIcon = fileIconList.first; 282 | isFileIconSupported = true; 283 | } 284 | return Row( 285 | children: [ 286 | if (!isFileIconSupported) 287 | Icon(Icons.file_present_rounded), 288 | if (isFileIconSupported) 289 | SvgPicture.asset( 290 | 'assets/fileicons/${fileIcon.fileName}', 291 | semanticsLabel: fileIcon.name, 292 | ), 293 | SizedBox(width: 10.0), 294 | Text(files[i].name), 295 | ], 296 | ); 297 | }, 298 | ), 299 | ), 300 | ), 301 | ); 302 | }, 303 | ); 304 | }, 305 | ), 306 | ], 307 | ), 308 | ); 309 | } 310 | } 311 | 312 | class ProjectFileView extends StatelessWidget { 313 | const ProjectFileView({Key key}) : super(key: key); 314 | 315 | @override 316 | Widget build(BuildContext context) { 317 | return Obx(() { 318 | var currentFile = projectViewController.currentFile; 319 | File f = File(currentFile.value.path); 320 | var content = f.readAsStringSync(); 321 | return Stack( 322 | children: [ 323 | Container( 324 | height: Get.height - 140.0, 325 | width: Get.width, 326 | color: Colors.grey.shade300, 327 | padding: const EdgeInsets.all(4.0), 328 | child: currentFile.value.path.endsWith('project.props') 329 | ? JsonViewerWidget(json.decode(content)) 330 | : Padding( 331 | padding: const EdgeInsets.only(top: 40.0), 332 | child: Obx( 333 | () => projectViewController.currentFile.value.isInEditMode 334 | ? Container( 335 | color: Colors.white, 336 | padding: const EdgeInsets.only( 337 | left: 12.0, 338 | top: 4.0, 339 | ), 340 | child: TextFormField( 341 | autofocus: true, 342 | decoration: InputDecoration( 343 | fillColor: Colors.white, 344 | ), 345 | initialValue: content, 346 | minLines: 150, 347 | maxLines: 150, 348 | onChanged: (txt) { 349 | projectFileController.currentFileText.value = 350 | txt; 351 | }, 352 | style: TextStyle( 353 | fontFamily: 'LucidaSansDemiBold', 354 | fontSize: 16, 355 | height: 1.0, 356 | ), 357 | ), 358 | ) 359 | : HighlightView( 360 | content, 361 | language: 'python', 362 | theme: githubTheme, 363 | padding: EdgeInsets.all(12), 364 | textStyle: TextStyle( 365 | fontFamily: 'LucidaSansDemiBold', 366 | fontSize: 16, 367 | ), 368 | ), 369 | ), 370 | ), 371 | ), 372 | if (projectViewController.currentFile.value?.path?.endsWith('.py')) 373 | Align( 374 | alignment: Alignment.topRight, 375 | child: Obx( 376 | () => Container( 377 | margin: EdgeInsets.only(left: Get.width * 0.66), 378 | child: Row( 379 | mainAxisSize: MainAxisSize.max, 380 | children: [ 381 | IconButton( 382 | icon: Icon( 383 | projectViewController.currentFile.value.isInEditMode 384 | ? Icons.edit_off_outlined 385 | : Icons.edit_outlined, 386 | ), 387 | onPressed: () { 388 | projectFileController.currentFileText.value = content; 389 | projectViewController.changeCurrentFileEditMode( 390 | projectViewController.currentFile.value, 391 | ); 392 | }, 393 | ), 394 | IconButton( 395 | icon: Icon(Icons.save_outlined), 396 | onPressed: !(projectViewController 397 | .currentFile.value.isInEditMode) 398 | ? null 399 | : () { 400 | projectFileController.saveCurrentFileContent( 401 | projectFileController 402 | .currentFileText.value); 403 | projectViewController.changeCurrentFileEditMode( 404 | projectViewController.currentFile.value, 405 | ); 406 | }, 407 | ), 408 | ], 409 | ), 410 | ), 411 | ), 412 | ) 413 | ], 414 | ); 415 | }); 416 | } 417 | } 418 | -------------------------------------------------------------------------------- /lib/widgets/settings_page.dart: -------------------------------------------------------------------------------- 1 | import '../imports.dart'; 2 | 3 | class SettingsPage extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) { 6 | return Container( 7 | height: MediaQuery.of(context).size.height, 8 | child: SingleChildScrollView( 9 | child: Column( 10 | crossAxisAlignment: CrossAxisAlignment.start, 11 | mainAxisSize: MainAxisSize.max, 12 | children: [ 13 | Padding(padding: EdgeInsets.all(24)), 14 | Padding( 15 | padding: const EdgeInsets.only(left: 18.0, right: 18.0), 16 | child: Column( 17 | children: [ 18 | ZeroNetAppBar(), 19 | // Padding( 20 | // padding: EdgeInsets.only(bottom: 30), 21 | // ), 22 | // ListView.builder( 23 | // physics: BouncingScrollPhysics(), 24 | // shrinkWrap: true, 25 | // itemCount: Utils.defSettings.keys.length, 26 | // itemBuilder: (ctx, i) { 27 | // // Setting current = 28 | // // Utils.defSettings[Utils.defSettings.keys.toList()[i]]; 29 | // if (current.name == enableZeroNetFilters && firstTime) { 30 | // return Container(); 31 | // } 32 | // return SettingsCard( 33 | // setting: current, 34 | // ); 35 | // }, 36 | // ), 37 | ], 38 | ), 39 | ), 40 | ], 41 | ), 42 | ), 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: pythonide 2 | description: Python IDE Mobile - IDE for Python 3. 3 | version: 1.0.0+1 4 | 5 | environment: 6 | sdk: ">=2.11.0 <3.0.0" 7 | 8 | dependencies: 9 | flutter: 10 | sdk: flutter 11 | archive: 3.1.2 12 | crypto: ^3.0.0 13 | clipboard: ^0.1.3 14 | device_info: ^2.0.2 15 | hive: ^2.0.4 16 | hive_flutter: ^1.0.0 17 | file_picker: ^3.0.2+2 18 | flutter_downloader: ^1.6.1 19 | flutter_highlight: ^0.7.0 20 | flutter_json_widget: ^1.0.2 21 | flutter_quill: ^1.3.3 22 | flutter_svg: 0.22.0 23 | http: ^0.13.0 24 | get: ^4.1.4 25 | google_fonts: ^2.1.0 26 | in_app_purchase: ^0.5.2 27 | in_app_review: ^2.0.2 28 | in_app_update: ^2.0.0 29 | outline_material_icons: 0.1.1 30 | equatable: ^2.0.2 31 | package_info: ^2.0.2 32 | path_provider: ^2.0.2 33 | purchases_flutter: ^3.2.2 34 | random_string: 2.1.0 35 | url_launcher: ^6.0.6 36 | 37 | dev_dependencies: 38 | flutter_test: 39 | sdk: flutter 40 | 41 | flutter: 42 | uses-material-design: true 43 | assets: 44 | - assets/ 45 | - assets/developers/ 46 | - assets/icons/ 47 | - assets/fileicons/ 48 | - assets/templates/python/ 49 | fonts: 50 | - family: LucidaSansDemiBold 51 | fonts: 52 | - asset: assets/fonts/lucidasansdemibold.ttf 53 | weight: 700 --------------------------------------------------------------------------------