commit
669f6301f8
|
|
@ -9,7 +9,6 @@
|
||||||
"copyright": "Copyright (c) 2023 Your Company",
|
"copyright": "Copyright (c) 2023 Your Company",
|
||||||
"sep": "/",
|
"sep": "/",
|
||||||
"kotlin_dir": "{{ cookiecutter.org_name.replace('.', cookiecutter.sep) }}{{ cookiecutter.sep }}{{ cookiecutter.project_name }}{{ cookiecutter.sep }}",
|
"kotlin_dir": "{{ cookiecutter.org_name.replace('.', cookiecutter.sep) }}{{ cookiecutter.sep }}{{ cookiecutter.project_name }}{{ cookiecutter.sep }}",
|
||||||
"windows_tcp_port": 63777,
|
|
||||||
"hide_loading_animation": true,
|
"hide_loading_animation": true,
|
||||||
"team_id": "",
|
"team_id": "",
|
||||||
"base_url": "/",
|
"base_url": "/",
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ buildscript {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:7.3.0'
|
classpath 'com.android.tools.build:gradle:7.4.2'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,78 @@
|
||||||
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flet/flet.dart';
|
import 'package:flet/flet.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
import 'package:serious_python/serious_python.dart';
|
import 'package:serious_python/serious_python.dart';
|
||||||
import 'package:url_strategy/url_strategy.dart';
|
import 'package:url_strategy/url_strategy.dart';
|
||||||
|
|
||||||
const bool isProduction = bool.fromEnvironment('dart.vm.product');
|
const bool isProduction = bool.fromEnvironment('dart.vm.product');
|
||||||
|
|
||||||
|
const assetPath = "app/app.zip";
|
||||||
|
const pythonModuleName = "{{ cookiecutter.python_module_name }}";
|
||||||
|
final hideLoadingPage =
|
||||||
|
bool.tryParse("{{ cookiecutter.hide_loading_animation }}".toLowerCase()) ??
|
||||||
|
true;
|
||||||
|
const outLogFilename = "out.log";
|
||||||
|
const errorExitCode = 100;
|
||||||
|
|
||||||
|
const pythonScript = """
|
||||||
|
import certifi, os, runpy, socket, sys, traceback
|
||||||
|
|
||||||
|
os.environ["REQUESTS_CA_BUNDLE"] = certifi.where()
|
||||||
|
os.environ["SSL_CERT_FILE"] = certifi.where()
|
||||||
|
|
||||||
|
if os.getenv("FLET_PLATFORM") == "android":
|
||||||
|
import ssl
|
||||||
|
|
||||||
|
def create_default_context(
|
||||||
|
purpose=ssl.Purpose.SERVER_AUTH, *, cafile=None, capath=None, cadata=None
|
||||||
|
):
|
||||||
|
return ssl.create_default_context(
|
||||||
|
purpose=purpose, cafile=certifi.where(), capath=capath, cadata=cadata
|
||||||
|
)
|
||||||
|
|
||||||
|
ssl._create_default_https_context = create_default_context
|
||||||
|
|
||||||
|
out_file = open("$outLogFilename", "w+", buffering=1)
|
||||||
|
|
||||||
|
callback_socket_addr = os.environ.get("FLET_PYTHON_CALLBACK_SOCKET_ADDR")
|
||||||
|
if ":" in callback_socket_addr:
|
||||||
|
addr, port = callback_socket_addr.split(":")
|
||||||
|
callback_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
callback_socket.connect((addr, int(port)))
|
||||||
|
else:
|
||||||
|
callback_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
|
callback_socket.connect(callback_socket_addr)
|
||||||
|
|
||||||
|
sys.stdout = sys.stderr = out_file
|
||||||
|
|
||||||
|
def flet_exit(code=0):
|
||||||
|
callback_socket.sendall(str(code).encode())
|
||||||
|
out_file.close()
|
||||||
|
callback_socket.close()
|
||||||
|
|
||||||
|
sys.exit = flet_exit
|
||||||
|
|
||||||
|
ex = None
|
||||||
|
try:
|
||||||
|
runpy.run_module("{module_name}", run_name="__main__")
|
||||||
|
except Exception as e:
|
||||||
|
ex = e
|
||||||
|
traceback.print_exception(e)
|
||||||
|
|
||||||
|
sys.exit(0 if ex is None else $errorExitCode)
|
||||||
|
""";
|
||||||
|
|
||||||
|
// global vars
|
||||||
|
String pageUrl = "";
|
||||||
|
String assetsDir = "";
|
||||||
|
String appDir = "";
|
||||||
|
Map<String, String> environmentVariables = {};
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
if (isProduction) {
|
if (isProduction) {
|
||||||
// ignore: avoid_returning_null_for_void
|
// ignore: avoid_returning_null_for_void
|
||||||
|
|
@ -17,29 +81,49 @@ void main() async {
|
||||||
|
|
||||||
runApp(FutureBuilder(
|
runApp(FutureBuilder(
|
||||||
future: prepareApp(),
|
future: prepareApp(),
|
||||||
builder: (BuildContext context, AsyncSnapshot<List<String>> snapshot) {
|
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||||
if (snapshot.hasData) {
|
if (snapshot.hasData) {
|
||||||
return FletApp(
|
// OK - start Python program
|
||||||
pageUrl: snapshot.data![0],
|
return kIsWeb
|
||||||
assetsDir: snapshot.data![1],
|
? FletApp(
|
||||||
hideLoadingPage: bool.tryParse(
|
pageUrl: pageUrl,
|
||||||
"{{ cookiecutter.hide_loading_animation }}".toLowerCase()),
|
assetsDir: assetsDir,
|
||||||
);
|
hideLoadingPage: hideLoadingPage,
|
||||||
|
)
|
||||||
|
: FutureBuilder(
|
||||||
|
future: runPythonApp(),
|
||||||
|
builder:
|
||||||
|
(BuildContext context, AsyncSnapshot<String?> snapshot) {
|
||||||
|
if (snapshot.hasData || snapshot.hasError) {
|
||||||
|
// error or premature finish
|
||||||
|
return MaterialApp(
|
||||||
|
home: ErrorScreen(
|
||||||
|
title: "Error running app",
|
||||||
|
text: snapshot.data ?? snapshot.error.toString()),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// no result of error
|
||||||
|
return FletApp(
|
||||||
|
pageUrl: pageUrl,
|
||||||
|
assetsDir: assetsDir,
|
||||||
|
hideLoadingPage: hideLoadingPage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
} else if (snapshot.hasError) {
|
} else if (snapshot.hasError) {
|
||||||
return Text('Error loading Flet app: ${snapshot.error}');
|
// error
|
||||||
|
return MaterialApp(
|
||||||
|
home: ErrorScreen(
|
||||||
|
title: "Error starting app",
|
||||||
|
text: snapshot.error.toString()));
|
||||||
} else {
|
} else {
|
||||||
// loading
|
// loading
|
||||||
return const SizedBox.shrink();
|
return const MaterialApp(home: BlankScreen());
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<String>> prepareApp() async {
|
Future prepareApp() async {
|
||||||
await setupDesktop();
|
|
||||||
|
|
||||||
String pageUrl = "";
|
|
||||||
String assetsDir = "";
|
|
||||||
|
|
||||||
if (kIsWeb) {
|
if (kIsWeb) {
|
||||||
// web mode - connect via HTTP
|
// web mode - connect via HTTP
|
||||||
pageUrl = Uri.base.toString();
|
pageUrl = Uri.base.toString();
|
||||||
|
|
@ -48,33 +132,163 @@ Future<List<String>> prepareApp() async {
|
||||||
setPathUrlStrategy();
|
setPathUrlStrategy();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
await setupDesktop();
|
||||||
|
|
||||||
// extract app from asset
|
// extract app from asset
|
||||||
var appDir = await extractAssetZip("app/app.zip");
|
appDir = await extractAssetZip(assetPath, checkHash: true);
|
||||||
|
|
||||||
// set current directory to app path
|
// set current directory to app path
|
||||||
Directory.current = appDir;
|
Directory.current = appDir;
|
||||||
|
|
||||||
assetsDir = path.join(appDir, "assets");
|
assetsDir = path.join(appDir, "assets");
|
||||||
|
|
||||||
var environmentVariables = {
|
environmentVariables["FLET_PLATFORM"] =
|
||||||
"FLET_PLATFORM": defaultTargetPlatform.name.toLowerCase()
|
defaultTargetPlatform.name.toLowerCase();
|
||||||
};
|
|
||||||
|
|
||||||
if (defaultTargetPlatform == TargetPlatform.windows) {
|
if (defaultTargetPlatform == TargetPlatform.windows) {
|
||||||
// use TCP on Windows
|
// use TCP on Windows
|
||||||
var port = int.parse("{{ cookiecutter.windows_tcp_port }}");
|
var tcpPort = await getUnusedPort();
|
||||||
pageUrl = "tcp://localhost:$port";
|
pageUrl = "tcp://localhost:$tcpPort";
|
||||||
environmentVariables["FLET_SERVER_PORT"] = port.toString();
|
environmentVariables["FLET_SERVER_PORT"] = tcpPort.toString();
|
||||||
} else {
|
} else {
|
||||||
// use UDS on other platforms
|
// use UDS on other platforms
|
||||||
pageUrl = "flet.sock";
|
pageUrl = "flet.sock";
|
||||||
environmentVariables["FLET_SERVER_UDS_PATH"] = pageUrl;
|
environmentVariables["FLET_SERVER_UDS_PATH"] = pageUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
SeriousPython.runProgram(
|
|
||||||
path.join(appDir, "{{ cookiecutter.python_module_name }}.pyc"),
|
|
||||||
environmentVariables: environmentVariables);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [pageUrl, assetsDir];
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> runPythonApp() async {
|
||||||
|
var script = pythonScript.replaceAll('{module_name}', pythonModuleName);
|
||||||
|
|
||||||
|
var completer = Completer<String>();
|
||||||
|
|
||||||
|
ServerSocket outSocketServer;
|
||||||
|
String socketAddr = "";
|
||||||
|
StringBuffer pythonOut = StringBuffer();
|
||||||
|
|
||||||
|
if (defaultTargetPlatform == TargetPlatform.windows) {
|
||||||
|
var tcpAddr = "127.0.0.1";
|
||||||
|
outSocketServer = await ServerSocket.bind(tcpAddr, 0);
|
||||||
|
debugPrint(
|
||||||
|
'Python output TCP Server is listening on port ${outSocketServer.port}');
|
||||||
|
socketAddr = "$tcpAddr:${outSocketServer.port}";
|
||||||
|
} else {
|
||||||
|
socketAddr = "stdout.sock";
|
||||||
|
outSocketServer = await ServerSocket.bind(
|
||||||
|
InternetAddress(socketAddr, type: InternetAddressType.unix), 0);
|
||||||
|
debugPrint('Python output Socket Server is listening on $socketAddr');
|
||||||
|
}
|
||||||
|
|
||||||
|
environmentVariables["FLET_PYTHON_CALLBACK_SOCKET_ADDR"] = socketAddr;
|
||||||
|
|
||||||
|
void closeOutServer() async {
|
||||||
|
outSocketServer.close();
|
||||||
|
|
||||||
|
int exitCode = int.tryParse(pythonOut.toString().trim()) ?? 0;
|
||||||
|
|
||||||
|
if (exitCode == errorExitCode) {
|
||||||
|
var out = "";
|
||||||
|
if (await File(outLogFilename).exists()) {
|
||||||
|
out = await File(outLogFilename).readAsString();
|
||||||
|
}
|
||||||
|
completer.complete(out);
|
||||||
|
} else {
|
||||||
|
exit(exitCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
outSocketServer.listen((client) {
|
||||||
|
debugPrint(
|
||||||
|
'Connection from: ${client.remoteAddress.address}:${client.remotePort}');
|
||||||
|
client.listen((data) {
|
||||||
|
var s = String.fromCharCodes(data);
|
||||||
|
pythonOut.write(s);
|
||||||
|
}, onError: (error) {
|
||||||
|
client.close();
|
||||||
|
closeOutServer();
|
||||||
|
}, onDone: () {
|
||||||
|
client.close();
|
||||||
|
closeOutServer();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// run python async
|
||||||
|
SeriousPython.runProgram(path.join(appDir, "$pythonModuleName.pyc"),
|
||||||
|
script: script, environmentVariables: environmentVariables);
|
||||||
|
|
||||||
|
// wait for client connection to close
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ErrorScreen extends StatelessWidget {
|
||||||
|
final String title;
|
||||||
|
final String text;
|
||||||
|
|
||||||
|
const ErrorScreen({super.key, required this.title, required this.text});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: SafeArea(
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
Clipboard.setData(ClipboardData(text: text));
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Copied to clipboard')),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.copy,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
label: const Text("Copy"),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: SelectableText(text,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall),
|
||||||
|
))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BlankScreen extends StatelessWidget {
|
||||||
|
const BlankScreen({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const Scaffold(
|
||||||
|
body: SizedBox.shrink(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> getUnusedPort() {
|
||||||
|
return ServerSocket.bind("127.0.0.1", 0).then((socket) {
|
||||||
|
var port = socket.port;
|
||||||
|
socket.close();
|
||||||
|
return port;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
platform :osx, '10.15'
|
platform :osx, '11.0'
|
||||||
|
|
||||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
|
|
|
||||||
|
|
@ -457,7 +457,7 @@
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
|
@ -536,7 +536,7 @@
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
|
|
@ -583,7 +583,7 @@
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
serious_python: ^0.6.1
|
serious_python: ^0.7.0
|
||||||
# serious_python:
|
# serious_python:
|
||||||
# git:
|
# git:
|
||||||
# url: https://github.com/flet-dev/serious-python
|
# url: https://github.com/flet-dev/serious-python
|
||||||
|
|
@ -43,6 +43,7 @@ flutter:
|
||||||
|
|
||||||
assets:
|
assets:
|
||||||
- app/app.zip
|
- app/app.zip
|
||||||
|
- app/app.zip.hash
|
||||||
|
|
||||||
# dart run flutter_launcher_icons
|
# dart run flutter_launcher_icons
|
||||||
flutter_launcher_icons:
|
flutter_launcher_icons:
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,16 @@ self.initPyodide = async function () {
|
||||||
self.pyodide.registerJsModule("flet_js", flet_js);
|
self.pyodide.registerJsModule("flet_js", flet_js);
|
||||||
flet_js.documentUrl = documentUrl;
|
flet_js.documentUrl = documentUrl;
|
||||||
await self.pyodide.runPythonAsync(`
|
await self.pyodide.runPythonAsync(`
|
||||||
import sys
|
import sys, runpy, traceback
|
||||||
from pyodide.http import pyfetch
|
from pyodide.http import pyfetch
|
||||||
response = await pyfetch("assets/app/app.zip")
|
response = await pyfetch("assets/app/app.zip")
|
||||||
await response.unpack_archive()
|
await response.unpack_archive()
|
||||||
sys.path.append("__pypackages__")
|
sys.path.append("__pypackages__")
|
||||||
|
try:
|
||||||
|
runpy.run_module("${self.pythonModuleName}", run_name="__main__")
|
||||||
|
except Exception as e:
|
||||||
|
traceback.print_exception(e)
|
||||||
`);
|
`);
|
||||||
pyodide.pyimport(self.pythonModuleName);
|
|
||||||
await self.flet_js.start_connection(self.receiveCallback);
|
await self.flet_js.start_connection(self.receiveCallback);
|
||||||
self.postMessage("initialized");
|
self.postMessage("initialized");
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue