├── scripts ├── .gitignore ├── requirements.txt ├── lib │ ├── app.desktop │ ├── snapcraft.yaml │ ├── build_nsis.py │ ├── build_dmg.py │ ├── __init__.py │ ├── build_mac.py │ ├── build_snap.py │ └── installer.nsi ├── run.py ├── init.py └── build.py ├── rust ├── .gitignore ├── assets │ ├── icon.icns │ ├── icon.ico │ ├── icudtl.dat │ ├── icon_128x128.png │ └── icon_16x16x32.png ├── build.rs ├── Cargo.toml └── src │ └── main.rs ├── .tmplfiles ├── .gitignore ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── pubspec.yaml └── lib └── main.dart /scripts/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ -------------------------------------------------------------------------------- /rust/.gitignore: -------------------------------------------------------------------------------- 1 | /.cargo/ 2 | /target/ -------------------------------------------------------------------------------- /.tmplfiles: -------------------------------------------------------------------------------- 1 | pubspec.yaml 2 | rust/Cargo.toml -------------------------------------------------------------------------------- /scripts/requirements.txt: -------------------------------------------------------------------------------- 1 | toml==0.10.0 2 | dmgbuild==1.3.2 3 | -------------------------------------------------------------------------------- /rust/assets/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gliheng/flutter-app-template/HEAD/rust/assets/icon.icns -------------------------------------------------------------------------------- /rust/assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gliheng/flutter-app-template/HEAD/rust/assets/icon.ico -------------------------------------------------------------------------------- /rust/assets/icudtl.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gliheng/flutter-app-template/HEAD/rust/assets/icudtl.dat -------------------------------------------------------------------------------- /rust/assets/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gliheng/flutter-app-template/HEAD/rust/assets/icon_128x128.png -------------------------------------------------------------------------------- /rust/assets/icon_16x16x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gliheng/flutter-app-template/HEAD/rust/assets/icon_16x16x32.png -------------------------------------------------------------------------------- /rust/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | #[cfg(target_os="windows")] { 3 | let mut res = winres::WindowsResource::new(); 4 | res.set_icon_with_id("./assets/icon.ico", "GLFW_ICON"); 5 | res.compile().unwrap(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /scripts/lib/app.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=$name 3 | Comment=Disassemble a pile of tiles by removing matching pairs 4 | ;Keywords=game;strategy;puzzle;board; 5 | Exec=$name 6 | Icon=icon.ico 7 | Terminal=false 8 | Type=Application 9 | ;Categories=GTK;Game;BoardGame; 10 | StartupNotify=true 11 | 12 | -------------------------------------------------------------------------------- /scripts/lib/snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: $name 2 | version: $version 3 | base: core18 4 | summary: $summary 5 | description: $description 6 | 7 | grade: devel 8 | confinement: classic 9 | 10 | parts: 11 | bin: 12 | source: ./files 13 | plugin: dump 14 | 15 | apps: 16 | $name: 17 | desktop: usr/share/applications/$name.desktop 18 | command: $name 19 | 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /ios 2 | /android 3 | 4 | # Miscellaneous 5 | *.class 6 | *.lock 7 | *.log 8 | *.pyc 9 | *.swp 10 | .DS_Store 11 | .atom/ 12 | .buildlog/ 13 | .history 14 | .svn/ 15 | 16 | # IntelliJ related 17 | *.iml 18 | *.ipr 19 | *.iws 20 | .idea/ 21 | 22 | # Flutter/Dart/Pub related 23 | **/doc/api/ 24 | .dart_tool/ 25 | .flutter-plugins 26 | .packages 27 | .pub-cache/ 28 | .pub/ 29 | build/ 30 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Flutter Attach", 9 | "request": "attach", 10 | "deviceId": "flutter-tester", 11 | "observatoryUri": "http://127.0.0.1:50300/", 12 | "type": "dart", 13 | "cwd": "${workspaceFolder}" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "$name" 3 | description = "Amazing desktop flutter app" 4 | version = "0.3.0" 5 | authors = ["juju ", "Sophie Tauchert "] 6 | publish = false 7 | edition = "2018" 8 | 9 | [package.metadata.flutter] 10 | identifier = "com.example.$name" # This is only used on mac 11 | 12 | [dependencies] 13 | log = "0.4.6" 14 | flutter-engine = "0.3.4" 15 | fern = { version = "^0.5", features = ["colored"] } 16 | chrono = "^0.4" 17 | 18 | [target.'cfg(target_os="macos")'.dependencies] 19 | core-foundation = "0.6.3" 20 | 21 | [target.'cfg(target_os="windows")'.build-dependencies] 22 | winres = "0.1.9" 23 | -------------------------------------------------------------------------------- /scripts/lib/build_nsis.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import tempfile 4 | from shutil import copyfile, copytree, rmtree 5 | 6 | def prepare(envs): 7 | name = envs['NAME'] 8 | output_dir = envs['OUTPUT_DIR'] 9 | assets_dir = envs['RUST_ASSETS_DIR'] 10 | return dict( 11 | NAME = name, 12 | VERSION = envs['VERSION'], 13 | FILE1 = os.path.join(output_dir, name + '.exe'), 14 | FILE2 = os.path.join(output_dir, 'flutter_engine.dll'), 15 | FILE3 = os.path.join(assets_dir, 'icudtl.dat'), 16 | ICON = os.path.join(assets_dir, 'icon.ico'), 17 | FLUTTER_ASSETS = envs['FLUTTER_ASSETS'], 18 | OUTPUT_FILE = os.path.join(output_dir, 'Installer.exe'), 19 | ) 20 | 21 | def build(envs): 22 | subprocess.run([ 23 | 'makensis', 24 | os.path.join(os.path.dirname(__file__), 'installer.nsi') 25 | ], env = envs) 26 | 27 | return envs['OUTPUT_FILE'] 28 | 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Amadeus Guo 4 | Copyright (c) 2019 Denis Subbotin 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /scripts/lib/build_dmg.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import tempfile 4 | 5 | def prepare(envs): 6 | return dict(envs) 7 | 8 | def build(envs): 9 | f = tempfile.NamedTemporaryFile(mode='w+', delete=False) 10 | s = tmpl.format( 11 | **envs, 12 | ) 13 | f.write(s) 14 | f.close() 15 | 16 | output = os.path.join(envs['OUTPUT_DIR'], envs['NAME'] + '.dmg') 17 | 18 | subprocess.run([ 19 | 'dmgbuild', '-s', f.name, 20 | envs['NAME'], 21 | output, 22 | ], check = True) 23 | 24 | os.unlink(f.name) 25 | return output 26 | 27 | tmpl = ''' 28 | import os 29 | 30 | files = ['{APP_PATH}'] 31 | symlinks = {{ 'Applications': '/Applications' }} 32 | 33 | # badge_icon = os.path.join('{RUST_ASSETS_DIR}', 'icon.icns') 34 | icon = os.path.join('{RUST_ASSETS_DIR}', 'icon.icns') 35 | 36 | # Where to put the icons 37 | icon_locations = {{ 38 | '{APP_NAME}': (140, 120), 39 | 'Applications': (500, 120) 40 | }} 41 | 42 | background = 'builtin-arrow' 43 | 44 | show_status_bar = False 45 | show_tab_view = False 46 | show_toolbar = False 47 | show_pathbar = False 48 | show_sidebar = False 49 | 50 | show_icon_preview = False 51 | 52 | default_view = 'icon-view' 53 | ''' 54 | -------------------------------------------------------------------------------- /scripts/lib/__init__.py: -------------------------------------------------------------------------------- 1 | import os, sys, subprocess 2 | from string import Template 3 | 4 | 5 | def look_for_proj_dir(d, fn = 'Cargo.toml'): 6 | while not os.path.isfile(os.path.join(d, fn)): 7 | p = os.path.dirname(d) 8 | if not p or p == d: 9 | return None 10 | d = p 11 | return d 12 | 13 | def read_sdk_version(root): 14 | fp = os.path.join(root, 'bin', 'internal', 'engine.version') 15 | with open(fp) as f: 16 | return f.read().strip() 17 | 18 | def guess_sdk_path(): 19 | try: 20 | output = subprocess.check_output([ 21 | 'where.exe' if sys.platform == 'win32' else 'which', 22 | 'flutter' 23 | ], encoding = 'utf8') 24 | lines = output.strip().split() 25 | path = lines[0] 26 | path = os.path.dirname(path) 27 | path = os.path.dirname(path) 28 | return path 29 | except (FileNotFoundError, subprocess.CalledProcessError): 30 | pass 31 | 32 | # Read engine version from FLUTTER_ROOT first 33 | # otherwise use FLUTTER_ENGINE_VERSION env variable 34 | def get_flutter_version(): 35 | if 'FLUTTER_ENGINE_VERSION' in os.environ: 36 | return os.environ.get('FLUTTER_ENGINE_VERSION') 37 | 38 | root = os.environ.get('FLUTTER_ROOT') 39 | if root: 40 | return read_sdk_version(root) 41 | else: 42 | sdk = guess_sdk_path() 43 | print("sdk is proballby at", sdk) 44 | if sdk: 45 | return read_sdk_version(sdk) 46 | else: 47 | raise Exception('Cannot find flutter engine version. flutter cli not in PATH. You may need to set either FLUTTER_ROOT or FLUTTER_ENGINE_VERSION') 48 | 49 | def get_workspace_dir(proj_dir): 50 | return look_for_proj_dir(os.path.dirname(proj_dir)) 51 | 52 | 53 | def tmpl_file(fp, config): 54 | with open(fp, 'r+') as f: 55 | s = Template(f.read()).substitute(**config) 56 | f.seek(0) 57 | f.truncate() 58 | f.write(s) 59 | -------------------------------------------------------------------------------- /scripts/run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os, sys 3 | import re 4 | import subprocess 5 | import signal 6 | import sys 7 | import threading 8 | from lib import look_for_proj_dir 9 | 10 | def signal_handler(signal, frame): 11 | sys.exit(0) 12 | 13 | signal.signal(signal.SIGINT, signal_handler) 14 | 15 | FLUTTER = 'flutter.bat' if sys.platform == 'win32' else 'flutter' 16 | PROJ_DIR = look_for_proj_dir(os.path.abspath(__file__), 'pubspec.yaml') 17 | RUST_PROJ_DIR = os.path.join(PROJ_DIR, 'rust') 18 | 19 | class CargoThread(threading.Thread): 20 | def __init__(self): 21 | super().__init__() 22 | self.observatory_uri = '' 23 | self.observatory_open = threading.Event() 24 | 25 | def run(self): 26 | self.proc = subprocess.Popen(['cargo', 'run'], 27 | stdout = subprocess.PIPE, 28 | cwd = RUST_PROJ_DIR 29 | ) 30 | 31 | while True: 32 | line = self.proc.stdout.readline() 33 | if self.proc.poll() is not None: 34 | # proc ended 35 | return 36 | print(line.decode(), end = '') 37 | match = re.search(r'Observatory listening on (?Phttps?://)(\S*)', line.decode()) 38 | 39 | if match: 40 | self.observatory_uri = match.group('schema') + match.group(2) 41 | self.observatory_open.set() 42 | 43 | 44 | def cargo_run(): 45 | cargo = CargoThread() 46 | cargo.start() 47 | cargo.observatory_open.wait() 48 | return cargo.observatory_uri 49 | 50 | if __name__ == '__main__': 51 | print('🍀 Building flutter bundle') 52 | subprocess.run( 53 | [FLUTTER, 'build', 'bundle'], 54 | cwd = PROJ_DIR, check = True) 55 | 56 | print('🦀 Building rust project') 57 | uri = cargo_run() 58 | if not uri: 59 | raise Exception('Launch cargo error') 60 | 61 | print('🍹 Attaching dart debugger') 62 | subprocess.run( 63 | [FLUTTER, 'attach', '--device-id=flutter-tester', '--debug-uri=' + uri], 64 | cwd = PROJ_DIR, check = True) 65 | -------------------------------------------------------------------------------- /scripts/init.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import glob 4 | import subprocess 5 | from lib import look_for_proj_dir, tmpl_file 6 | 7 | def get_config(): 8 | try: 9 | proj_dir = look_for_proj_dir(os.path.abspath(__file__), 'pubspec.yaml') 10 | name = input('What\'s the name of the project?\n') 11 | lib_name = name.replace('-', '_') 12 | 13 | tmplfile_path = os.path.join(proj_dir, '.tmplfiles') 14 | try: 15 | with open(tmplfile_path) as f: 16 | tmplfiles = [] 17 | for line in f.readlines(): 18 | line = line.strip() 19 | 20 | if os.path.isdir(os.path.join(proj_dir, line)): 21 | line = os.path.join(line, '*') 22 | 23 | tmplfiles.append(line) 24 | except: 25 | tmplfiles = [] 26 | 27 | return { 28 | "name": name, 29 | "lib_name": lib_name, # underlined version of name 30 | "proj_dir": proj_dir, 31 | "tmplfiles": tmplfiles, 32 | "tmplfile_path": tmplfile_path 33 | } 34 | except KeyboardInterrupt: 35 | return None 36 | 37 | 38 | def install_py_deps(config): 39 | subprocess.run( 40 | ['pip3', 'install', '-r', './scripts/requirements.txt'], 41 | cwd = config['proj_dir'], check = True) 42 | 43 | def tmpl_proj(config): 44 | proj_dir = config['proj_dir'] 45 | for pattern in config['tmplfiles']: 46 | for fp in glob.iglob(os.path.join(proj_dir, pattern)): 47 | fp = os.path.join(proj_dir, fp) 48 | if os.path.isfile(fp): 49 | tmpl_file(fp, config) 50 | 51 | def run(): 52 | config = get_config() 53 | if not config: 54 | return 55 | 56 | # if a name is not specified, skip templating process 57 | if config['name']: 58 | print('🔮 Creating files') 59 | tmpl_proj(config) 60 | 61 | print('🔦 Installing build dependencies') 62 | install_py_deps(config) 63 | 64 | print('🍭 Done! Happy coding.') 65 | 66 | # remove tmplfile, useless now 67 | os.remove(config['tmplfile_path']) 68 | 69 | if __name__ == '__main__': 70 | run() 71 | -------------------------------------------------------------------------------- /scripts/lib/build_mac.py: -------------------------------------------------------------------------------- 1 | import os 2 | from shutil import copy, copyfile, copytree, rmtree 3 | import subprocess 4 | 5 | def prepare(envs): 6 | envs = dict(envs) 7 | APP_NAME = envs['NAME'] + '.app' 8 | APP_PATH = os.path.join(envs['OUTPUT_DIR'], APP_NAME) 9 | envs.update( 10 | APP_NAME = APP_NAME, 11 | APP_PATH = APP_PATH, 12 | ) 13 | return envs 14 | 15 | def build(envs): 16 | APP_PATH = envs.get('APP_PATH') 17 | 18 | # clear last output 19 | rmtree(APP_PATH, ignore_errors = True) 20 | 21 | bin_dir = os.path.join(APP_PATH, 'Contents', 'MacOS') 22 | frm_dir = os.path.join(APP_PATH, 'Contents', 'Frameworks') 23 | res_dir = os.path.join(APP_PATH, 'Contents', 'Resources') 24 | 25 | os.makedirs(bin_dir, exist_ok = True) 26 | os.makedirs(res_dir, exist_ok = True) 27 | copy(os.path.join(envs['TARGET_DIR'], 'debug' if envs['DEBUG'] else 'release' , envs['NAME']), bin_dir) 28 | subprocess.run(['chmod', '+x', os.path.join(bin_dir, envs['NAME'])], check = True) 29 | copytree(os.path.join(envs['WORKSPACE_TARGET_DIR'] or envs['TARGET_DIR'], 'flutter-engine', envs['FLUTTER_LIB_VER'], 'FlutterEmbedder.framework'), os.path.join(frm_dir, 'FlutterEmbedder.framework'), symlinks = True) 30 | 31 | # copy resources 32 | copy(os.path.join(envs['RUST_ASSETS_DIR'], 'icon.icns'), res_dir) 33 | copy(os.path.join(envs['RUST_ASSETS_DIR'], 'icudtl.dat'), res_dir) 34 | copytree(envs['FLUTTER_ASSETS'], os.path.join(res_dir, 'flutter_assets')) 35 | 36 | plist = plist_tmpl.format( 37 | identifier = envs['IDENTIFIER'], 38 | name = envs['NAME'] 39 | ) 40 | plist_file = open(os.path.join(APP_PATH, 'Contents', 'Info.plist'), 'w+') 41 | plist_file.write(plist) 42 | 43 | return APP_PATH 44 | 45 | 46 | plist_tmpl = ''' 47 | 48 | 49 | 50 | CFBundleExecutable 51 | {name} 52 | CFBundleIconFile 53 | icon.icns 54 | CFBundleIdentifier 55 | {identifier} 56 | NSHighResolutionCapable 57 | 58 | LSUIElement 59 | 60 | 61 | 62 | ''' 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flutter-app-template [![Join Gitter Chat Channel](https://badges.gitter.im/flutter-rs/community.svg)](https://gitter.im/flutter-rs/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 2 | 3 | 4 | Example app built using flutter-rs. 5 | 6 | 7 | ![screenshot](https://raw.githubusercontent.com/gliheng/flutter-rs/master/www/images/screenshot_mac.png) 8 | 9 | 10 | - Support Hot reload 11 | - MethodChannel, EventChannel 12 | - Async runtime using tokio 13 | - System dialogs 14 | - Clipboard support 15 | - Cross platform support, Runs on mac, windows, linux 16 | - Support distribution format: (windows NSIS, mac app, mac dmg, linux snap) 17 | 18 | # Get Started 19 | 20 | ## Install requirements 21 | 22 | - [Rust](https://www.rust-lang.org/tools/install) 23 | 24 | - libglfw: 25 | - Install on Mac with: `brew install glfw` 26 | - Install on linux with `apt install libglfw3` 27 | - cmake: 28 | - Install on Mac with: `brew install cmake` 29 | - Install on linux with `apt install cmake` 30 | 31 | - [flutter sdk](https://flutter.io) 32 | 33 | ## Config flutter engine version 34 | flutter-rs need to know your flutter engine version. 35 | You can set this using any of the following methods. 36 | - If you have flutter cli in your PATH, you're set. 37 | - Set FLUTTER_ROOT environment variable to your flutter sdk path 38 | - Set FLUTTER_ENGINE_VERSION environment variable to your engine version 39 | 40 | ## Clone this repo 41 | 42 | git clone git@github.com:gliheng/flutter-app-template.git flutter-app 43 | cd flutter-app 44 | python ./scripts/init.py 45 | 46 | ## Develop 47 | - To develop with cli hot-reloading, simple run: 48 | 49 | `python ./scripts/run.py` 50 | 51 | - To debug using VS Code dart tools: 52 | 53 | Start process using `cargo run` 54 | 55 | Then attach to debugger using 56 | `flutter attach --debug-uri=DEBUG_URI` 57 | 58 | ## Distribute 59 | - To build distribution, use: 60 | `python ./scripts/build.py mac|dmg|nsis|snap` 61 | 62 | **Note:** 63 | Build scripts are written in python3. Install python depenendencies using `pip3 install -r scripts/requirements.txt` 64 | Build on Windows require [NSIS3](https://sourceforge.net/projects/nsis/files/NSIS%203/) 65 | -------------------------------------------------------------------------------- /scripts/lib/build_snap.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | from shutil import copy, copyfile, copytree, rmtree 4 | from . import tmpl_file 5 | 6 | def prepare(envs): 7 | envs = dict(envs) 8 | SNAP_DIR = os.path.join(envs['OUTPUT_DIR'], 'snap') 9 | envs.update( 10 | SNAP_DIR = SNAP_DIR, 11 | ) 12 | return envs 13 | 14 | def tmpl_config(envs): 15 | return { 16 | 'name': envs['NAME'], 17 | 'version': envs['VERSION'], 18 | 'description': envs['DESCRIPTION'], 19 | 'summary': 'Flutter application built with flutter-rs', 20 | } 21 | 22 | def collect(envs): 23 | snap_dir = envs['SNAP_DIR'] 24 | files_dir = os.path.join(snap_dir, 'files') 25 | os.makedirs(files_dir, exist_ok = True) 26 | 27 | bin = os.path.join(envs['TARGET_DIR'], 'debug' if envs['DEBUG'] else 'release' , envs['NAME']) 28 | 29 | # flutter app files 30 | copy(bin, files_dir) 31 | # subprocess.run(['chmod', '+x', os.path.join(files_dir, envs['NAME'])], check = True) 32 | copy(os.path.join(envs['WORKSPACE_TARGET_DIR'] or envs['TARGET_DIR'], 'flutter-engine', envs['FLUTTER_LIB_VER'], 'libflutter_engine.so'), files_dir) 33 | copy(os.path.join(envs['RUST_ASSETS_DIR'], 'icon.ico'), files_dir) 34 | copy(os.path.join(envs['RUST_ASSETS_DIR'], 'icudtl.dat'), files_dir) 35 | 36 | dest = os.path.join(files_dir, 'flutter_assets') 37 | if os.path.exists(dest): 38 | rmtree(dest) 39 | copytree(envs['FLUTTER_ASSETS'], dest) 40 | 41 | config = tmpl_config(envs) 42 | 43 | # snap build files 44 | file = os.path.join(os.path.dirname(__file__), 'snapcraft.yaml') 45 | dest = os.path.join(snap_dir, 'snapcraft.yaml') 46 | copyfile(file, dest) 47 | tmpl_file(dest, config) 48 | 49 | file = os.path.join(os.path.dirname(__file__), 'app.desktop') 50 | desktop_file_name = envs['NAME'] + '.desktop' 51 | desktop_dir = os.path.join(files_dir, 'usr/share/applications') 52 | os.makedirs(desktop_dir, exist_ok = True) 53 | dest = os.path.join(desktop_dir, desktop_file_name) 54 | copyfile(file, dest) 55 | tmpl_file(dest, config) 56 | 57 | def build(envs): 58 | collect(envs) 59 | fn = '{}_{}.snap'.format(envs['NAME'], envs['VERSION']) 60 | output = os.path.join( 61 | envs['SNAP_DIR'], 62 | fn, 63 | ) 64 | subprocess.run(['snapcraft', 'snap', '-o', fn], cwd = envs['SNAP_DIR'], check = True) 65 | return output 66 | 67 | 68 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: $lib_name 2 | description: A new Flutter project. 3 | 4 | # The following defines the version and build number for your application. 5 | # A version number is three numbers separated by dots, like 1.2.43 6 | # followed by an optional build number separated by a +. 7 | # Both the version and the builder number may be overridden in flutter 8 | # build by specifying --build-name and --build-number, respectively. 9 | # Read more about versioning at semver.org. 10 | version: 1.0.0+1 11 | 12 | environment: 13 | sdk: ">=2.0.0-dev.68.0 <3.0.0" 14 | 15 | dependencies: 16 | flutter: 17 | sdk: flutter 18 | system_info: ^0.1.1 19 | 20 | # The following adds the Cupertino Icons font to your application. 21 | # Use with the CupertinoIcons class for iOS style icons. 22 | cupertino_icons: ^0.1.2 23 | 24 | dev_dependencies: 25 | flutter_test: 26 | sdk: flutter 27 | 28 | 29 | # For information on the generic Dart part of this file, see the 30 | # following page: https://www.dartlang.org/tools/pub/pubspec 31 | 32 | # The following section is specific to Flutter. 33 | flutter: 34 | 35 | # The following line ensures that the Material Icons font is 36 | # included with your application, so that you can use the icons in 37 | # the material Icons class. 38 | uses-material-design: true 39 | 40 | # To add assets to your application, add an assets section, like this: 41 | # assets: 42 | # - images/a_dot_burr.jpeg 43 | # - images/a_dot_ham.jpeg 44 | 45 | # An image asset can refer to one or more resolution-specific "variants", see 46 | # https://flutter.io/assets-and-images/#resolution-aware. 47 | 48 | # For details regarding adding assets from package dependencies, see 49 | # https://flutter.io/assets-and-images/#from-packages 50 | 51 | # To add custom fonts to your application, add a fonts section here, 52 | # in this "flutter" section. Each entry in this list should have a 53 | # "family" key with the font family name, and a "fonts" key with a 54 | # list giving the asset and other descriptors for the font. For 55 | # example: 56 | # fonts: 57 | # - family: Schyler 58 | # fonts: 59 | # - asset: fonts/Schyler-Regular.ttf 60 | # - asset: fonts/Schyler-Italic.ttf 61 | # style: italic 62 | # - family: Trajan Pro 63 | # fonts: 64 | # - asset: fonts/TrajanPro.ttf 65 | # - asset: fonts/TrajanPro_Bold.ttf 66 | # weight: 700 67 | # 68 | # For details regarding fonts from package dependencies, 69 | # see https://flutter.io/custom-fonts/#from-packages 70 | -------------------------------------------------------------------------------- /rust/src/main.rs: -------------------------------------------------------------------------------- 1 | // This build a windows app without console on windows in release mode 2 | #![cfg_attr(all(windows, not(debug_assertions)), windows_subsystem = "windows")] 3 | 4 | use std::{env, path::PathBuf}; 5 | 6 | use fern::colors::{Color, ColoredLevelConfig}; 7 | use log::info; 8 | 9 | #[cfg(target_os = "macos")] 10 | use core_foundation::bundle; 11 | 12 | #[cfg(target_os = "macos")] 13 | fn get_res_dir() -> PathBuf { 14 | let bd = bundle::CFBundle::main_bundle(); 15 | let exe = bd 16 | .executable_url() 17 | .expect("Cannot get executable dir") 18 | .to_path() 19 | .expect("to_path error"); 20 | exe.parent().unwrap().parent().unwrap().join("Resources") 21 | } 22 | 23 | #[cfg(not(target_os = "macos"))] 24 | fn get_res_dir() -> PathBuf { 25 | env::current_exe() 26 | .expect("Cannot get application dir") 27 | .parent() 28 | .expect("Cannot get application dir") 29 | .to_path_buf() 30 | } 31 | 32 | fn main() { 33 | let colors = ColoredLevelConfig::new() 34 | .error(Color::Red) 35 | .warn(Color::Yellow) 36 | .info(Color::Green) 37 | .debug(Color::White) 38 | .trace(Color::BrightBlack); 39 | 40 | fern::Dispatch::new() 41 | .level(log::LevelFilter::Debug) 42 | .format(move |out, message, record| { 43 | out.finish(format_args!( 44 | "{}[{}][{}][{}] {}\x1B[0m", 45 | format_args!("\x1B[{}m", colors.get_color(&record.level()).to_fg_str()), 46 | chrono::Local::now().format("%H:%M:%S"), 47 | record.level(), 48 | record.target(), 49 | message 50 | )) 51 | }) 52 | .chain(std::io::stdout()) 53 | .apply() 54 | .unwrap(); 55 | 56 | let (assets_path, icu_data_path) = match env::var("CARGO_MANIFEST_DIR") { 57 | Ok(proj_dir) => { 58 | info!("Running inside cargo project"); 59 | let proj_dir = PathBuf::from(&proj_dir); 60 | ( 61 | proj_dir 62 | .parent() 63 | .unwrap() 64 | .join("build") 65 | .join("flutter_assets"), 66 | proj_dir.join("assets/icudtl.dat"), 67 | ) 68 | } 69 | Err(_) => { 70 | let res = get_res_dir(); 71 | (res.join("flutter_assets"), res.join("icudtl.dat")) 72 | } 73 | }; 74 | 75 | let mut engine = flutter_engine::init().unwrap(); 76 | engine 77 | .create_window( 78 | &flutter_engine::WindowArgs { 79 | height: 768, 80 | width: 1024, 81 | title: "Flutter Demo", 82 | mode: flutter_engine::WindowMode::Windowed, 83 | bg_color: (255, 255, 255), 84 | }, 85 | assets_path.to_string_lossy().to_string(), 86 | icu_data_path.to_string_lossy().to_string(), 87 | vec![], 88 | ) 89 | .unwrap(); 90 | engine.run_window_loop(None, None); 91 | } 92 | -------------------------------------------------------------------------------- /scripts/build.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os, sys 3 | import toml 4 | import argparse 5 | from lib import look_for_proj_dir, get_flutter_version, get_workspace_dir 6 | import subprocess 7 | 8 | FLUTTER = 'flutter.bat' if sys.platform == 'win32' else 'flutter' 9 | 10 | def collect_env(args): 11 | PROJ_DIR = look_for_proj_dir(os.path.abspath(__file__), 'pubspec.yaml') 12 | RUST_PROJ_DIR = os.path.join(PROJ_DIR, 'rust') 13 | RUST_ASSETS_DIR = os.path.join(RUST_PROJ_DIR, 'assets') 14 | TOML_FILE = os.path.join(RUST_PROJ_DIR, 'Cargo.toml') 15 | META = toml.loads(open(TOML_FILE).read()) 16 | NAME = META['package']['name'] 17 | VERSION = META['package']['version'] 18 | DESCRIPTION = META['package']['description'] 19 | 20 | DEBUG = not args.release 21 | RELEASE = args.release 22 | 23 | TARGET_DIR = os.path.join(RUST_PROJ_DIR, 'target') 24 | WORKSPACE = get_workspace_dir(RUST_PROJ_DIR) 25 | WORKSPACE_TARGET_DIR = os.path.join(WORKSPACE, 'target') if WORKSPACE else None 26 | OUTPUT_DIR = os.path.join(TARGET_DIR, 'debug' if DEBUG else 'release') 27 | FLUTTER_CONFIG = META['package']['metadata']['flutter'] 28 | IDENTIFIER = FLUTTER_CONFIG['identifier'] if 'identifier' in FLUTTER_CONFIG else 'one.juju.flutter-app' 29 | FLUTTER_LIB_VER = get_flutter_version() 30 | FLUTTER_ASSETS = os.path.join(os.path.dirname(RUST_PROJ_DIR), 'build', 'flutter_assets') 31 | return locals() 32 | 33 | def cargo_build(cwd, release = False): 34 | args = ['cargo', 'build'] 35 | if release: 36 | args.append('--release') 37 | subprocess.run(args, cwd = cwd) 38 | 39 | def build_flutter(envs): 40 | subprocess.run([FLUTTER, 'build', 'bundle'], cwd = envs['PROJ_DIR']) 41 | 42 | if __name__ == '__main__': 43 | parser = argparse.ArgumentParser(prog='build', description='rust app distribution builder') 44 | parser.add_argument('dist', choices=['mac', 'dmg', 'nsis', 'snap'], help='distribution type') 45 | parser.add_argument('--release', action='store_true', help='build release package') 46 | 47 | args = parser.parse_args() 48 | envs = collect_env(args) 49 | 50 | print('🍀 Building flutter bundle') 51 | build_flutter(envs) 52 | 53 | print('🦀 Building rust project') 54 | cargo_build(envs['RUST_PROJ_DIR'], envs['RELEASE']) 55 | 56 | print('🐶 Creating distribution') 57 | # prepare has a chance to modify envs 58 | if args.dist == 'mac': 59 | from lib.build_mac import prepare, build 60 | envs = prepare(envs) 61 | output = build(envs) 62 | elif args.dist == 'dmg': 63 | from lib.build_mac import prepare, build 64 | envs = prepare(envs) 65 | build(envs) 66 | 67 | from lib.build_dmg import prepare, build 68 | envs = prepare(envs) 69 | output = build(envs) 70 | elif args.dist == 'snap': 71 | from lib.build_snap import prepare, build 72 | envs = prepare(envs) 73 | output = build(envs) 74 | elif args.dist == 'nsis': 75 | from lib.build_nsis import prepare, build 76 | envs = prepare(envs) 77 | output = build(envs) 78 | print('🍭 {} distribution generated at {}'.format(args.dist, output)) 79 | -------------------------------------------------------------------------------- /scripts/lib/installer.nsi: -------------------------------------------------------------------------------- 1 | !include "MUI2.nsh" 2 | !include "FileFunc.nsh" 3 | 4 | ;-------------------------------- 5 | ;General 6 | 7 | ;Name and file 8 | !define APPNAME "$%NAME%" 9 | !define VERSION "$%VERSION%" 10 | !define LOCALE_APPNAME "Flutter App Demo" 11 | !define PUBLISHER "flutter-rs Developers" 12 | Name "${LOCALE_APPNAME}" 13 | OutFile "$%OUTPUT_FILE%" 14 | BrandingText "${LOCALE_APPNAME} ${VERSION}" 15 | 16 | ;!define MUI_ICON "$%ICON%" 17 | ;!define MUI_UNICON "$%ICON%" 18 | !define MUI_ICON "${NSISDIR}\Contrib\Graphics\Icons\arrow-install.ico" 19 | !define MUI_UNICON "${NSISDIR}\Contrib\Graphics\Icons\arrow-uninstall.ico" 20 | !define MUI_FINISHPAGE_RUN "$INSTDIR\${APPNAME}.exe" 21 | 22 | !define MUI_WELCOMEFINISHPAGE_BITMAP "${NSISDIR}\Contrib\Graphics\Wizard\nsis3-metro.bmp" 23 | !define MUI_UNWELCOMEFINISHPAGE_BITMAP "${NSISDIR}\Contrib\Graphics\Wizard\nsis3-metro.bmp" 24 | 25 | ;Default installation folder 26 | InstallDir "$LOCALAPPDATA\${APPNAME}" 27 | 28 | ;Get installation folder from registry if available 29 | InstallDirRegKey HKCU "Software\${APPNAME}" "" 30 | 31 | ;Request application privileges for Windows Vista 32 | RequestExecutionLevel user 33 | 34 | ;-------------------------------- 35 | ;Interface Settings 36 | 37 | !define MUI_ABORTWARNING 38 | 39 | ;-------------------------------- 40 | ;Pages 41 | 42 | !insertmacro MUI_PAGE_WELCOME 43 | ; !insertmacro MUI_PAGE_LICENSE "License.txt" 44 | ; !insertmacro MUI_PAGE_COMPONENTS 45 | !insertmacro MUI_PAGE_DIRECTORY 46 | !insertmacro MUI_PAGE_INSTFILES 47 | !insertmacro MUI_PAGE_FINISH 48 | 49 | !insertmacro MUI_UNPAGE_WELCOME 50 | !insertmacro MUI_UNPAGE_CONFIRM 51 | !insertmacro MUI_UNPAGE_INSTFILES 52 | !insertmacro MUI_UNPAGE_FINISH 53 | 54 | ;-------------------------------- 55 | ;Languages 56 | 57 | !insertmacro MUI_LANGUAGE "English" 58 | 59 | ;-------------------------------- 60 | ;Installer Sections 61 | 62 | Section "Default" 63 | 64 | SetOutPath "$INSTDIR" 65 | 66 | ;ADD YOUR OWN FILES HERE... 67 | File "$%FILE1%" 68 | File "$%FILE2%" 69 | File "$%FILE3%" 70 | File /r "$%FLUTTER_ASSETS%" 71 | 72 | ;Create Uninstaller 73 | WriteUninstaller "$INSTDIR\Uninstall.exe" 74 | 75 | call installShortcuts 76 | Call installRegEntry 77 | 78 | SectionEnd 79 | 80 | ;-------------------------------- 81 | ;Uninstaller Section 82 | 83 | Section "Uninstall" 84 | 85 | ;ADD YOUR OWN FILES HERE... 86 | RMDir "$INSTDIR" 87 | 88 | ;Rmove Uninstaller 89 | Delete "$INSTDIR\Uninstall.exe" 90 | 91 | call un.removeShortcuts 92 | Call un.removeRegEntry 93 | 94 | SectionEnd 95 | 96 | Function installShortcuts 97 | ;create shortcuts in the start menu and on the desktop 98 | CreateDirectory "$SMPROGRAMS\${APPNAME}" 99 | CreateShortCut "$SMPROGRAMS\${APPNAME}\${LOCALE_APPNAME}.lnk" "$INSTDIR\${APPNAME}.exe" 100 | CreateShortCut "$SMPROGRAMS\${APPNAME}\Uninstall ${LOCALE_APPNAME}.lnk" "$INSTDIR\Uninstall.exe" 101 | CreateShortCut "$DESKTOP\${LOCALE_APPNAME}.lnk" "$INSTDIR\${APPNAME}.exe" 102 | FunctionEnd 103 | 104 | Function un.removeShortcuts 105 | ; delete the shortcuts 106 | delete "$SMPROGRAMS\${APPNAME}\${LOCALE_APPNAME}.lnk" 107 | delete "$SMPROGRAMS\${APPNAME}\Uninstall ${LOCALE_APPNAME}.lnk" 108 | rmDir "$SMPROGRAMS\${APPNAME}" 109 | delete "$DESKTOP\${LOCALE_APPNAME}.lnk" 110 | FunctionEnd 111 | 112 | Function installRegEntry 113 | ;Store installation folder 114 | WriteRegStr HKCU "Software\${APPNAME}" "" $INSTDIR 115 | 116 | WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \ 117 | "DisplayName" "${LOCALE_APPNAME}" 118 | WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \ 119 | "UninstallString" "$INSTDIR\Uninstall.exe" 120 | WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \ 121 | "DisplayIcon" "$INSTDIR\${APPNAME}.exe,0" 122 | WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \ 123 | "DisplayVersion" "${VERSION}" 124 | WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \ 125 | "Publisher" "${PUBLISHER}" 126 | 127 | ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 128 | IntFmt $0 "0x%08X" $0 129 | WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \ 130 | "EstimatedSize" "$0" 131 | FunctionEnd 132 | 133 | Function un.removeRegEntry 134 | DeleteRegKey HKCU "Software\${APPNAME}" 135 | DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" 136 | FunctionEnd 137 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:system_info/system_info.dart'; 3 | import 'package:flutter/foundation.dart' show debugDefaultTargetPlatformOverride; 4 | 5 | void main() { 6 | // Override is necessary to prevent Unknown platform' flutter startup error. 7 | debugDefaultTargetPlatformOverride = TargetPlatform.android; 8 | runApp(MyApp()); 9 | } 10 | 11 | class MyApp extends StatelessWidget { 12 | // This widget is the root of your application. 13 | @override 14 | Widget build(BuildContext context) { 15 | String fontFamily; 16 | if (SysInfo.operatingSystemName == "Ubuntu") { 17 | fontFamily = "Ubuntu"; 18 | } 19 | return MaterialApp( 20 | title: 'Flutter Demo', 21 | theme: ThemeData( 22 | // This is the theme of your application. 23 | // 24 | // Try running your application with "flutter run". You'll see the 25 | // application has a blue toolbar. Then, without quitting the app, try 26 | // changing the primarySwatch below to Colors.green and then invoke 27 | // "hot reload" (press "r" in the console where you ran "flutter run", 28 | // or simply save your changes to "hot reload" in a Flutter IDE). 29 | // Notice that the counter didn't reset back to zero; the application 30 | // is not restarted. 31 | fontFamily: fontFamily, 32 | primarySwatch: Colors.blue, 33 | ), 34 | home: MyHomePage(title: 'Flutter Demo Home Page'), 35 | ); 36 | } 37 | } 38 | 39 | class MyHomePage extends StatefulWidget { 40 | MyHomePage({Key key, this.title}) : super(key: key); 41 | 42 | // This widget is the home page of your application. It is stateful, meaning 43 | // that it has a State object (defined below) that contains fields that affect 44 | // how it looks. 45 | 46 | // This class is the configuration for the state. It holds the values (in this 47 | // case the title) provided by the parent (in this case the App widget) and 48 | // used by the build method of the State. Fields in a Widget subclass are 49 | // always marked "final". 50 | 51 | final String title; 52 | 53 | @override 54 | _MyHomePageState createState() => _MyHomePageState(); 55 | } 56 | 57 | class _MyHomePageState extends State { 58 | int _counter = 0; 59 | 60 | void _incrementCounter() { 61 | setState(() { 62 | // This call to setState tells the Flutter framework that something has 63 | // changed in this State, which causes it to rerun the build method below 64 | // so that the display can reflect the updated values. If we changed 65 | // _counter without calling setState(), then the build method would not be 66 | // called again, and so nothing would appear to happen. 67 | _counter++; 68 | }); 69 | } 70 | 71 | @override 72 | Widget build(BuildContext context) { 73 | // This method is rerun every time setState is called, for instance as done 74 | // by the _incrementCounter method above. 75 | // 76 | // The Flutter framework has been optimized to make rerunning build methods 77 | // fast, so that you can just rebuild anything that needs updating rather 78 | // than having to individually change instances of widgets. 79 | return Scaffold( 80 | appBar: AppBar( 81 | // Here we take the value from the MyHomePage object that was created by 82 | // the App.build method, and use it to set our appbar title. 83 | title: Text(widget.title), 84 | ), 85 | body: Center( 86 | // Center is a layout widget. It takes a single child and positions it 87 | // in the middle of the parent. 88 | child: Column( 89 | // Column is also layout widget. It takes a list of children and 90 | // arranges them vertically. By default, it sizes itself to fit its 91 | // children horizontally, and tries to be as tall as its parent. 92 | // 93 | // Invoke "debug painting" (press "p" in the console, choose the 94 | // "Toggle Debug Paint" action from the Flutter Inspector in Android 95 | // Studio, or the "Toggle Debug Paint" command in Visual Studio Code) 96 | // to see the wireframe for each widget. 97 | // 98 | // Column has various properties to control how it sizes itself and 99 | // how it positions its children. Here we use mainAxisAlignment to 100 | // center the children vertically; the main axis here is the vertical 101 | // axis because Columns are vertical (the cross axis would be 102 | // horizontal). 103 | mainAxisAlignment: MainAxisAlignment.center, 104 | children: [ 105 | Text( 106 | 'You have pushed the button this many times:', 107 | ), 108 | Text( 109 | '$_counter', 110 | style: Theme.of(context).textTheme.display1, 111 | ), 112 | ], 113 | ), 114 | ), 115 | floatingActionButton: FloatingActionButton( 116 | onPressed: _incrementCounter, 117 | tooltip: 'Increment', 118 | child: Icon(Icons.add), 119 | ), // This trailing comma makes auto-formatting nicer for build methods. 120 | ); 121 | } 122 | } 123 | --------------------------------------------------------------------------------