From 37ebb3c2174ee7adc8af51aca0ed8ed1fda8399a Mon Sep 17 00:00:00 2001 From: Chikith Rishi Maddi Date: Wed, 26 Aug 2026 21:20:19 +1000 Subject: [PATCH 1/6] File opening Method updated - resolving android issue --- ui/app.py | 221 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 196 insertions(+), 25 deletions(-) diff --git a/ui/app.py b/ui/app.py index 53a9da1..75d5a2b 100644 --- a/ui/app.py +++ b/ui/app.py @@ -242,7 +242,7 @@ def _create_controls(self) -> None: )) self.output_directory_field = self._style_input_field(ft.TextField( label="Output directory (optional)", - hint_text="Where generated files should be saved", + hint_text="Default: Documents/Hyperkey (Windows), Downloads/Hyperkey (Android)", on_change=self._refresh_command_preview, )) @@ -297,14 +297,19 @@ def _create_controls(self) -> None: min_lines=4, max_lines=8, )) + self.copy_command_button = ft.IconButton( + icon=ft.Icons.CONTENT_COPY, + tooltip="Copy command", + on_click=self._copy_command, + ) # Advanced CLI fallback. Keep this deliberately large because commands # can be long, especially when Android returns longer document paths. self.cli_field = self._style_input_field(ft.TextField( label="Hyperkey arguments or full command", hint_text=( - 'metadata.csv -r raw_data -o result OR ' - 'python hyperkey.py metadata.csv -r raw_data' + 'metadata.csv -r raw_data -n result OR ' + 'metadata.csv -r raw_data -o output_folder -n result' ), multiline=True, min_lines=8, @@ -333,6 +338,7 @@ def _create_controls(self) -> None: self.outputs_content = ft.Column(spacing=12) self.output_status = ft.Text() self.url_launcher = ft.UrlLauncher() + self.share_service = ft.Share() self.logs_field = self._style_input_field(ft.TextField( label="Run log", @@ -483,6 +489,56 @@ async def _portable_file_path(self, picked: ft.FilePickerFile) -> str: destination.write_bytes(picked.bytes) return str(destination) + def _is_android(self) -> bool: + """Return True when Hyperkey is running as an Android app.""" + return self.page.platform == ft.PagePlatform.ANDROID + + async def _get_android_default_output_directory(self) -> Path: + """ + Return Hyperkey's public Android output directory. + + Android uses the device Downloads directory so generated CSV, HTML, + PDF, PNG, JSON, and log files remain user-visible and can be handed + to compatible external applications. + """ + downloads = await ft.StoragePaths().get_downloads_directory() + + if not downloads: + raise RuntimeError( + "Android Downloads directory is unavailable on this device." + ) + + output_dir = Path(downloads) / "Hyperkey" + output_dir.mkdir(parents=True, exist_ok=True) + return output_dir + + async def _ensure_form_default_output_directory(self) -> None: + """ + Apply the Android default only when the user has not selected a custom + output directory. + + Windows is intentionally left blank here. pipeline.py resolves its + normal Windows default to the current user's Documents/Hyperkey folder. + """ + if (self.output_directory_field.value or "").strip(): + return + + if not self._is_android(): + return + + output_dir = await self._get_android_default_output_directory() + self.output_directory_field.value = str(output_dir) + self._refresh_command_preview(None) + + @staticmethod + def _arguments_have_output_directory(arguments: list[str]) -> bool: + """Return True when CLI arguments already contain -o/--output.""" + for argument in arguments: + value = str(argument).strip() + if value in {"-o", "--output"} or value.startswith("--output="): + return True + return False + # ------------------------------------------------------------------ # Pickers # ------------------------------------------------------------------ @@ -584,7 +640,16 @@ def _run_screen(self) -> ft.Control: "Equivalent CLI command", subtitle="Live preview of the command that will be executed.", icon=ft.Icons.TERMINAL, - controls=[self.command_preview], + controls=[ + ft.Row( + spacing=8, + vertical_alignment=ft.CrossAxisAlignment.START, + controls=[ + self.command_preview, + self.copy_command_button, + ], + ) + ], ) actions = ft.ResponsiveRow( @@ -630,7 +695,8 @@ def _cli_screen(self) -> ft.Control: controls=[ self.cli_field, ft.Text( - "Example: metadata.csv -r raw_data -o sydneyAPPN " + "Example: metadata.csv -r raw_data " + "-o output_folder -n sydneyAPPN " "-l species_locations.csv --outlier-analysis", theme_style=ft.TextThemeStyle.BODY_SMALL, selectable=True, @@ -796,22 +862,47 @@ def _markdown_report_path(self) -> Path | None: return None async def _open_output_path(self, path: Path) -> None: - """ - Open an output using the operating system while keeping Hyperkey open. - - Desktop opens the file in its associated application. On Android the - platform launcher chooses the associated viewer when available. - """ + """Open a generated file with a compatible external application.""" try: resolved = path.resolve() + + if not resolved.exists() or not resolved.is_file(): + raise FileNotFoundError(f"Generated file not found: {resolved}") + await self.url_launcher.launch_url( resolved.as_uri(), - mode=ft.LaunchMode.EXTERNAL_APPLICATION, + mode=( + ft.LaunchMode.EXTERNAL_NON_BROWSER_APPLICATION + if self._is_android() + else ft.LaunchMode.EXTERNAL_APPLICATION + ), + ) + self.output_status.value = f"Opening: {resolved.name}" + + except Exception as exc: + self.output_status.value = ( + f"Unable to open '{path.name}': {exc}" + ) + + self.page.update() + + async def _share_output_path(self, path: Path) -> None: + """Share a generated file using the platform share sheet.""" + try: + resolved = path.resolve() + + if not resolved.exists() or not resolved.is_file(): + raise FileNotFoundError(f"Generated file not found: {resolved}") + + await self.share_service.share_files( + [ft.ShareFile.from_path(str(resolved))], + title=f"Share {resolved.name}", ) - self.output_status.value = f"Opened: {resolved.name}" + self.output_status.value = f"Sharing: {resolved.name}" + except Exception as exc: self.output_status.value = ( - f"Unable to open '{path.name}' automatically: {exc}" + f"Unable to share '{path.name}': {exc}" ) self.page.update() @@ -820,6 +911,9 @@ def _output_file_card(self, label: str, path: Path) -> ft.Card: async def open_file(_e) -> None: await self._open_output_path(path) + async def share_file(_e) -> None: + await self._share_output_path(path) + size_text = "" try: size = path.stat().st_size @@ -837,12 +931,52 @@ async def open_file(_e) -> None: subtitle += f"\n{size_text}" return ft.Card( - content=ft.ListTile( - leading=ft.Icon(ft.Icons.INSERT_DRIVE_FILE_OUTLINED), - title=ft.Text(label, weight=ft.FontWeight.W_600), - subtitle=ft.Text(subtitle, max_lines=3), - trailing=ft.Icon(ft.Icons.OPEN_IN_NEW), - on_click=open_file, + content=ft.Container( + padding=12, + content=ft.Column( + spacing=10, + controls=[ + ft.Row( + vertical_alignment=ft.CrossAxisAlignment.START, + controls=[ + ft.Icon(ft.Icons.INSERT_DRIVE_FILE_OUTLINED), + ft.Column( + expand=True, + spacing=2, + controls=[ + ft.Text( + label, + weight=ft.FontWeight.W_600, + ), + ft.Text( + subtitle, + max_lines=3, + size=12, + selectable=True, + ), + ], + ), + ], + ), + ft.Row( + alignment=ft.MainAxisAlignment.END, + spacing=8, + wrap=True, + controls=[ + ft.Button( + content="Open", + icon=ft.Icons.OPEN_IN_NEW, + on_click=open_file, + ), + ft.OutlinedButton( + content="Share", + icon=ft.Icons.SHARE_OUTLINED, + on_click=share_file, + ), + ], + ), + ], + ), ) ) @@ -1121,7 +1255,7 @@ def _outputs_screen(self) -> ft.Control: controls=[ self._screen_title( "Outputs", - "Generated files from the latest run. Tap a file to open it without closing Hyperkey.", + "Generated files from the latest run. Open them with a compatible app or share them without closing Hyperkey.", ), self.output_status, self.outputs_content, @@ -1234,7 +1368,7 @@ async def open_report(_e) -> None: self.outputs_content.controls.append( section_card( "Generated files", - subtitle="Tap any file to open it in the default application.", + subtitle="Open a file with a compatible application or share it.", controls=[ self._output_file_card(label, path) for label, path in generated_files @@ -1342,6 +1476,25 @@ def _change_screen(self, e) -> None: self.current_screen = e.control.selected_index self._render_screen() + async def _copy_command(self, _e) -> None: + """Copy the generated CLI command to the system clipboard.""" + command = (self.command_preview.value or "").strip() + + if not command: + self.form_status.value = "No command available to copy." + self.page.update() + return + + try: + await ft.Clipboard().set(command) + self.form_status.value = "CLI command copied to clipboard." + self.form_status.color = ft.Colors.GREEN + except Exception as exc: + self.form_status.value = f"Unable to copy command: {exc}" + self.form_status.color = ft.Colors.RED + + self.page.update() + def _refresh_command_preview(self, _e) -> None: config = self._config_from_form() args = self.service.build_arguments(config) @@ -1360,6 +1513,12 @@ async def _run_form(self, _e) -> None: self.page.update() try: + # Default output policy: + # Windows -> Documents/Hyperkey (resolved by pipeline.py) + # Android -> Downloads/Hyperkey (resolved here through Flet) + # A user-selected output directory still overrides either default. + await self._ensure_form_default_output_directory() + config = self._config_from_form() # Hyperkey's processing stack is synchronous and report generation @@ -1391,6 +1550,18 @@ async def _run_cli(self, _e) -> None: try: arguments = self.service.parse_cli_text(self.cli_field.value or "") + # Keep Advanced CLI mode consistent with the normal form: + # Android defaults to Downloads/Hyperkey only when the command + # does not already contain -o/--output. + if ( + self._is_android() + and not self._arguments_have_output_directory(arguments) + ): + android_output = ( + await self._get_android_default_output_directory() + ) + arguments.extend(["-o", str(android_output)]) + # Keep synchronous backend libraries, including Playwright Sync API, # outside Flet's asyncio event loop. result = await asyncio.to_thread( @@ -1480,11 +1651,11 @@ def _show_help(self, _e) -> None: ), help_item( "Output name", - "Optional base name. Existing Hyperkey dated naming is preserved by the backend.", + "Optional output-name prefix. It is passed separately as -n/--name. Existing Hyperkey dated naming is preserved by the backend.", ), help_item( "Output directory", - "Optional destination directory for generated outputs. It is combined with Output name before being sent as -o.", + "Optional destination directory for generated outputs. If empty, Windows uses Documents/Hyperkey and Android uses Downloads/Hyperkey. A selected folder is passed separately as -o/--output and overrides the default.", ), help_item( "Dark mode", @@ -1504,7 +1675,7 @@ def _show_help(self, _e) -> None: ), help_item( "Outputs", - "Shows the files generated by the latest successful run. Tap a file to open it in the default application while Hyperkey remains open. The Markdown report is also previewed directly inside the app.", + "Shows the files generated by the latest successful run. Use Open to launch a compatible application or Share to send the file through Android/Windows sharing. The Markdown report is also previewed directly inside the app.", ), help_item( "Results and Logs", From f015bfc6a94ba5dbb2a6b410a833d5b06a9d15a8 Mon Sep 17 00:00:00 2001 From: Chikith Rishi Maddi Date: Thu, 27 Aug 2026 01:08:44 +1000 Subject: [PATCH 2/6] File opener - Broad Permission --- ui/app.py | 213 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 202 insertions(+), 11 deletions(-) diff --git a/ui/app.py b/ui/app.py index 75d5a2b..0832840 100644 --- a/ui/app.py +++ b/ui/app.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +import mimetypes import os import re import tempfile @@ -8,6 +9,12 @@ from uuid import uuid4 import flet as ft +import flet_permission_handler as fph + +try: + from hyperkey_file_opener import HyperkeyFileOpener +except ImportError: + HyperkeyFileOpener = None from .components import browse_field, help_item, section_card, stat_card from .models import HyperkeyRunConfig, RunResult @@ -339,6 +346,10 @@ def _create_controls(self) -> None: self.output_status = ft.Text() self.url_launcher = ft.UrlLauncher() self.share_service = ft.Share() + self.android_file_opener = ( + HyperkeyFileOpener() if HyperkeyFileOpener is not None else None + ) + self.permission_handler = fph.PermissionHandler() self.logs_field = self._style_input_field(ft.TextField( label="Run log", @@ -539,6 +550,168 @@ def _arguments_have_output_directory(arguments: list[str]) -> bool: return True return False + # ------------------------------------------------------------------ + # Android storage permission + # ------------------------------------------------------------------ + async def _android_all_files_access_granted(self) -> bool: + """Return True when Android has granted Hyperkey All files access.""" + if not self._is_android(): + return True + + try: + status = await self.permission_handler.get_status( + fph.Permission.MANAGE_EXTERNAL_STORAGE + ) + return status == fph.PermissionStatus.GRANTED + except Exception: + return False + + async def _request_android_all_files_access(self, _e=None) -> None: + """ + Send the user to Android's special All files access permission screen. + + MANAGE_EXTERNAL_STORAGE is a special Android permission. The Flet + permission handler opens the appropriate system settings screen rather + than displaying a normal runtime-permission popup. + """ + if not self._is_android(): + return + + try: + self.page.pop_dialog() + except Exception: + pass + + try: + status = await self.permission_handler.request( + fph.Permission.MANAGE_EXTERNAL_STORAGE + ) + + # On Android this request can leave Hyperkey while the user toggles + # "Allow access to manage all files" in system settings. Re-check + # when control returns to the app instead of trusting the first + # status value alone. + granted = await self._android_all_files_access_granted() + + if granted: + self.form_status.value = ( + "File access granted. Hyperkey can now use direct custom " + "paths in shared storage." + ) + self.form_status.color = ft.Colors.GREEN + else: + status_name = getattr(status, "name", "not granted") + self.form_status.value = ( + "All files access is not enabled. File/folder pickers will " + "still work, but manually entered Android paths may be " + f"inaccessible. Status: {status_name}." + ) + self.form_status.color = ft.Colors.ORANGE + + except Exception as exc: + self.form_status.value = f"Unable to request file access: {exc}" + self.form_status.color = ft.Colors.RED + + self.page.update() + + async def _open_android_permission_settings(self, _e=None) -> None: + """Open Hyperkey's Android app settings as a fallback.""" + if not self._is_android(): + return + + try: + self.page.pop_dialog() + except Exception: + pass + + try: + opened = await self.permission_handler.open_app_settings() + if not opened: + raise RuntimeError("Android app settings could not be opened.") + except Exception as exc: + self.form_status.value = f"Unable to open Android settings: {exc}" + self.form_status.color = ft.Colors.RED + self.page.update() + + def _show_android_storage_permission_dialog(self) -> None: + """Explain All files access before sending the user to Android settings.""" + if not self._is_android(): + return + + dialog = ft.AlertDialog( + modal=True, + title=ft.Text("Allow Hyperkey file access"), + content=ft.Column( + tight=True, + spacing=12, + controls=[ + ft.Text( + "Hyperkey can work with files selected through Android's " + "pickers without this permission." + ), + ft.Text( + "All files access is requested so Hyperkey can also read " + "and write direct custom paths that you enter manually, " + "including folders in shared internal storage." + ), + ft.Container( + padding=12, + border=ft.Border.all(1, ft.Colors.GREY_700), + border_radius=10, + content=ft.Column( + tight=True, + spacing=6, + controls=[ + ft.Text( + "Why Hyperkey needs it", + weight=ft.FontWeight.BOLD, + ), + ft.Text("• Read metadata CSV and spectral files from custom paths."), + ft.Text("• Read raw-data folders supplied as direct paths."), + ft.Text("• Save generated outputs to custom shared-storage folders."), + ft.Text("• Keep direct filesystem paths usable without copying files into app cache."), + ], + ), + ), + ft.Text( + "Android will open a system settings screen. Enable " + "“Allow access to manage all files” for Hyperkey, then " + "return to the app." + ), + ft.Text( + "This permission does not replace Hyperkey's secure " + "FileProvider-based Open action. Files handed to Excel, " + "PDF viewers, Gallery, and other apps still receive only " + "temporary access to the specific file being opened.", + theme_style=ft.TextThemeStyle.BODY_SMALL, + ), + ], + ), + actions=[ + ft.TextButton("Not now", on_click=lambda _e: self.page.pop_dialog()), + ft.Button( + content="Grant file access", + icon=ft.Icons.FOLDER_OPEN, + on_click=self._request_android_all_files_access, + ), + ], + actions_alignment=ft.MainAxisAlignment.END, + ) + self.page.show_dialog(dialog) + + async def ensure_android_storage_permission_on_startup(self) -> None: + """ + On Android first launch (and later launches while permission is absent), + explain why Hyperkey requests All files access before opening settings. + """ + if not self._is_android(): + return + + if await self._android_all_files_access_granted(): + return + + self._show_android_storage_permission_dialog() + # ------------------------------------------------------------------ # Pickers # ------------------------------------------------------------------ @@ -862,21 +1035,38 @@ def _markdown_report_path(self) -> Path | None: return None async def _open_output_path(self, path: Path) -> None: - """Open a generated file with a compatible external application.""" + """Open a generated file with a compatible external application. + + Android must not expose a raw file:// URI to another application. + Hyperkey therefore delegates Android opening to the bundled + HyperkeyFileOpener extension, which uses an Android FileProvider-backed + ACTION_VIEW intent and grants the chosen viewer temporary read access. + Desktop platforms continue using Flet's UrlLauncher. + """ try: resolved = path.resolve() if not resolved.exists() or not resolved.is_file(): raise FileNotFoundError(f"Generated file not found: {resolved}") - await self.url_launcher.launch_url( - resolved.as_uri(), - mode=( - ft.LaunchMode.EXTERNAL_NON_BROWSER_APPLICATION - if self._is_android() - else ft.LaunchMode.EXTERNAL_APPLICATION - ), - ) + if self._is_android(): + if self.android_file_opener is None: + raise RuntimeError( + "Android file opener extension is not installed. " + "Add hyperkey-file-opener to the app dependencies and rebuild the APK." + ) + + mime_type, _encoding = mimetypes.guess_type(str(resolved)) + await self.android_file_opener.open_file( + str(resolved), + mime_type=mime_type, + ) + else: + await self.url_launcher.launch_url( + resolved.as_uri(), + mode=ft.LaunchMode.EXTERNAL_APPLICATION, + ) + self.output_status.value = f"Opening: {resolved.name}" except Exception as exc: @@ -1688,8 +1878,9 @@ def _show_help(self, _e) -> None: self.page.show_dialog(help_dialog) -def main(page: ft.Page) -> None: - HyperkeyUI(page) +async def main(page: ft.Page) -> None: + ui = HyperkeyUI(page) + await ui.ensure_android_storage_permission_on_startup() if __name__ == "__main__": From f30edb1d5aec555b643a0e92f968568176942ff5 Mon Sep 17 00:00:00 2001 From: Chikith Rishi Maddi Date: Thu, 27 Aug 2026 01:15:43 +1000 Subject: [PATCH 3/6] Packaging requirements for the extension for file permissions --- hyperkey_file_opener/pyproject.toml | 19 +++++++++ .../lib/hyperkey_file_opener.dart | 3 ++ .../lib/src/extension.dart | 22 ++++++++++ .../lib/src/file_opener_service.dart | 42 +++++++++++++++++++ .../flutter/hyperkey_file_opener/pubspec.yaml | 14 +++++++ .../hyperkey_file_opener.egg-info/PKG-INFO | 6 +++ .../hyperkey_file_opener.egg-info/SOURCES.txt | 12 ++++++ .../dependency_links.txt | 1 + .../requires.txt | 1 + .../top_level.txt | 2 + .../src/hyperkey_file_opener/__init__.py | 3 ++ .../src/hyperkey_file_opener/file_opener.py | 17 ++++++++ pyproject.toml | 7 +++- 13 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 hyperkey_file_opener/pyproject.toml create mode 100644 hyperkey_file_opener/src/flutter/hyperkey_file_opener/lib/hyperkey_file_opener.dart create mode 100644 hyperkey_file_opener/src/flutter/hyperkey_file_opener/lib/src/extension.dart create mode 100644 hyperkey_file_opener/src/flutter/hyperkey_file_opener/lib/src/file_opener_service.dart create mode 100644 hyperkey_file_opener/src/flutter/hyperkey_file_opener/pubspec.yaml create mode 100644 hyperkey_file_opener/src/hyperkey_file_opener.egg-info/PKG-INFO create mode 100644 hyperkey_file_opener/src/hyperkey_file_opener.egg-info/SOURCES.txt create mode 100644 hyperkey_file_opener/src/hyperkey_file_opener.egg-info/dependency_links.txt create mode 100644 hyperkey_file_opener/src/hyperkey_file_opener.egg-info/requires.txt create mode 100644 hyperkey_file_opener/src/hyperkey_file_opener.egg-info/top_level.txt create mode 100644 hyperkey_file_opener/src/hyperkey_file_opener/__init__.py create mode 100644 hyperkey_file_opener/src/hyperkey_file_opener/file_opener.py diff --git a/hyperkey_file_opener/pyproject.toml b/hyperkey_file_opener/pyproject.toml new file mode 100644 index 0000000..45cb7e4 --- /dev/null +++ b/hyperkey_file_opener/pyproject.toml @@ -0,0 +1,19 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "hyperkey-file-opener" +version = "0.1.0" +description = "Flet service used by Hyperkey to open local files through Android FileProvider-backed intents." +requires-python = ">=3.10" +dependencies = ["flet>=0.80.0"] + +[tool.flet.extensions.hyperkey_file_opener] +path = "flutter/hyperkey_file_opener" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-data] +"flutter.hyperkey_file_opener" = ["**/*"] diff --git a/hyperkey_file_opener/src/flutter/hyperkey_file_opener/lib/hyperkey_file_opener.dart b/hyperkey_file_opener/src/flutter/hyperkey_file_opener/lib/hyperkey_file_opener.dart new file mode 100644 index 0000000..83e9cec --- /dev/null +++ b/hyperkey_file_opener/src/flutter/hyperkey_file_opener/lib/hyperkey_file_opener.dart @@ -0,0 +1,3 @@ +library hyperkey_file_opener; + +export 'src/extension.dart'; diff --git a/hyperkey_file_opener/src/flutter/hyperkey_file_opener/lib/src/extension.dart b/hyperkey_file_opener/src/flutter/hyperkey_file_opener/lib/src/extension.dart new file mode 100644 index 0000000..7899448 --- /dev/null +++ b/hyperkey_file_opener/src/flutter/hyperkey_file_opener/lib/src/extension.dart @@ -0,0 +1,22 @@ +import 'package:flet/flet.dart'; +import 'package:flutter/widgets.dart'; + +import 'file_opener_service.dart'; + +class Extension extends FletExtension { + @override + void ensureInitialized() {} + + @override + FletService? createService(Control control) { + switch (control.type) { + case 'HyperkeyFileOpener': + return HyperkeyFileOpenerService(control: control); + default: + return null; + } + } + + @override + Widget? createWidget(Key? key, Control control) => null; +} diff --git a/hyperkey_file_opener/src/flutter/hyperkey_file_opener/lib/src/file_opener_service.dart b/hyperkey_file_opener/src/flutter/hyperkey_file_opener/lib/src/file_opener_service.dart new file mode 100644 index 0000000..15b0fd8 --- /dev/null +++ b/hyperkey_file_opener/src/flutter/hyperkey_file_opener/lib/src/file_opener_service.dart @@ -0,0 +1,42 @@ +import 'package:flet/flet.dart'; +import 'package:open_filex/open_filex.dart'; + +class HyperkeyFileOpenerService extends FletService { + HyperkeyFileOpenerService({required super.control}); + + @override + void init() { + super.init(); + control.addInvokeMethodListener(_invokeMethod); + } + + Future _invokeMethod(String name, dynamic args) async { + if (name != 'open_file') { + throw Exception('Unknown HyperkeyFileOpener method: $name'); + } + + final map = Map.from(args as Map); + final path = map['path']?.toString(); + final mimeType = map['mime_type']?.toString(); + + if (path == null || path.trim().isEmpty) { + throw Exception('No file path was provided.'); + } + + final result = await OpenFilex.open( + path, + type: (mimeType == null || mimeType.isEmpty) ? null : mimeType, + ); + + return { + 'result_type': result.type.toString(), + 'message': result.message, + }; + } + + @override + void dispose() { + control.removeInvokeMethodListener(_invokeMethod); + super.dispose(); + } +} diff --git a/hyperkey_file_opener/src/flutter/hyperkey_file_opener/pubspec.yaml b/hyperkey_file_opener/src/flutter/hyperkey_file_opener/pubspec.yaml new file mode 100644 index 0000000..4d3f066 --- /dev/null +++ b/hyperkey_file_opener/src/flutter/hyperkey_file_opener/pubspec.yaml @@ -0,0 +1,14 @@ +name: hyperkey_file_opener +description: FileProvider-backed external file opener for the Hyperkey Flet app. +version: 0.1.0 +publish_to: none + +environment: + sdk: '>=3.4.0 <4.0.0' + flutter: '>=3.22.0' + +dependencies: + flutter: + sdk: flutter + flet: any + open_filex: ^4.7.0 diff --git a/hyperkey_file_opener/src/hyperkey_file_opener.egg-info/PKG-INFO b/hyperkey_file_opener/src/hyperkey_file_opener.egg-info/PKG-INFO new file mode 100644 index 0000000..24f140b --- /dev/null +++ b/hyperkey_file_opener/src/hyperkey_file_opener.egg-info/PKG-INFO @@ -0,0 +1,6 @@ +Metadata-Version: 2.4 +Name: hyperkey-file-opener +Version: 0.1.0 +Summary: Flet service used by Hyperkey to open local files through Android FileProvider-backed intents. +Requires-Python: >=3.10 +Requires-Dist: flet>=0.80.0 diff --git a/hyperkey_file_opener/src/hyperkey_file_opener.egg-info/SOURCES.txt b/hyperkey_file_opener/src/hyperkey_file_opener.egg-info/SOURCES.txt new file mode 100644 index 0000000..9e6de83 --- /dev/null +++ b/hyperkey_file_opener/src/hyperkey_file_opener.egg-info/SOURCES.txt @@ -0,0 +1,12 @@ +pyproject.toml +src/flutter/hyperkey_file_opener/pubspec.yaml +src/flutter/hyperkey_file_opener/lib/hyperkey_file_opener.dart +src/flutter/hyperkey_file_opener/lib/src/extension.dart +src/flutter/hyperkey_file_opener/lib/src/file_opener_service.dart +src/hyperkey_file_opener/__init__.py +src/hyperkey_file_opener/file_opener.py +src/hyperkey_file_opener.egg-info/PKG-INFO +src/hyperkey_file_opener.egg-info/SOURCES.txt +src/hyperkey_file_opener.egg-info/dependency_links.txt +src/hyperkey_file_opener.egg-info/requires.txt +src/hyperkey_file_opener.egg-info/top_level.txt \ No newline at end of file diff --git a/hyperkey_file_opener/src/hyperkey_file_opener.egg-info/dependency_links.txt b/hyperkey_file_opener/src/hyperkey_file_opener.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/hyperkey_file_opener/src/hyperkey_file_opener.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/hyperkey_file_opener/src/hyperkey_file_opener.egg-info/requires.txt b/hyperkey_file_opener/src/hyperkey_file_opener.egg-info/requires.txt new file mode 100644 index 0000000..3a0e960 --- /dev/null +++ b/hyperkey_file_opener/src/hyperkey_file_opener.egg-info/requires.txt @@ -0,0 +1 @@ +flet>=0.80.0 diff --git a/hyperkey_file_opener/src/hyperkey_file_opener.egg-info/top_level.txt b/hyperkey_file_opener/src/hyperkey_file_opener.egg-info/top_level.txt new file mode 100644 index 0000000..a48b2ef --- /dev/null +++ b/hyperkey_file_opener/src/hyperkey_file_opener.egg-info/top_level.txt @@ -0,0 +1,2 @@ +flutter +hyperkey_file_opener diff --git a/hyperkey_file_opener/src/hyperkey_file_opener/__init__.py b/hyperkey_file_opener/src/hyperkey_file_opener/__init__.py new file mode 100644 index 0000000..9975469 --- /dev/null +++ b/hyperkey_file_opener/src/hyperkey_file_opener/__init__.py @@ -0,0 +1,3 @@ +from .file_opener import HyperkeyFileOpener + +__all__ = ["HyperkeyFileOpener"] diff --git a/hyperkey_file_opener/src/hyperkey_file_opener/file_opener.py b/hyperkey_file_opener/src/hyperkey_file_opener/file_opener.py new file mode 100644 index 0000000..9ded3c1 --- /dev/null +++ b/hyperkey_file_opener/src/hyperkey_file_opener/file_opener.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +import flet as ft + + +@ft.control("HyperkeyFileOpener") +class HyperkeyFileOpener(ft.Service): + """Open local files using the host platform's native file-opening mechanism.""" + + async def open_file(self, path: str, mime_type: str | None = None): + return await self._invoke_method( + "open_file", + { + "path": path, + "mime_type": mime_type, + }, + ) diff --git a/pyproject.toml b/pyproject.toml index 0336601..d960631 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,8 @@ dependencies = [ "cycler==0.12.1", "fonttools==4.63.0", "flet==0.86.5", + "flet-permission-handler==0.86.5", + "hyperkey-file-opener @ file:///D:/Masters%20of%20computing/Tech%20Launcher/hyperkey/hyperkey-file-opener", "greenlet==3.5.1", "kiwisolver==1.5.0", "Markdown==3.10.3", @@ -28,4 +30,7 @@ dependencies = [ ] [tool.flet] -module = "hyperkey" \ No newline at end of file +module = "hyperkey" + +[tool.flet.android.permission] +"android.permission.MANAGE_EXTERNAL_STORAGE" = true \ No newline at end of file From 9ae3afa7cba473a2a515444969bea6413cffb2f3 Mon Sep 17 00:00:00 2001 From: Chikith Rishi Maddi Date: Thu, 27 Aug 2026 01:18:08 +1000 Subject: [PATCH 4/6] Temp Changes to keep it running for testing on android --- scripts/workflow.py | 122 ++++++++++++++++++++++---------------------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/scripts/workflow.py b/scripts/workflow.py index 035f282..70b5f5d 100644 --- a/scripts/workflow.py +++ b/scripts/workflow.py @@ -106,67 +106,67 @@ def run_pipeline( effective_outlier_settings: dict[str, object] | None = None try: - # --------------------------- - # 1. Heatmap - # --------------------------- - print("\nRunning visualise_heatmap.py ...") - from visualise_heatmap import main as heatmap_main - - heatmap_arguments = { - "input_path": output_csv, - "output_name": heatmap_output_name, - "dark_mode": dark_mode - } - - if raw_location_path is not None: - heatmap_arguments["raw_location_path"] = raw_location_path - - heatmap_main(**heatmap_arguments) - completed_stages.append("heatmap") - print("visualise_heatmap.py completed successfully.") - - # --------------------------- - # 2. Spectral Measurement Graph - # --------------------------- - print("\nRunning visualise_measurement.py ...") - from visualise_measurement import main as measurement_main - measurement_main( - input_path=output_csv, - output_name=spectral_graph_output_name, - dark_mode=dark_mode - ) - completed_stages.append("spectral_graph") - print("visualise_measurement.py completed successfully.") - - # --------------------------- - # 3. Outlier Analysis (optional) - # --------------------------- - if outlier_analysis: - print("\nRunning outlier_analysis.py ...") - from outlier_analysis import main as outlier_main - - effective_outlier_settings = _resolve_outlier_settings( - outlier_settings - ) - - outlier_main( - input_path=output_csv, - output_path=outlier_output_name, - **effective_outlier_settings, - ) - completed_stages.append("outlier_analysis") - print("outlier_analysis.py completed successfully.") - else: - print("\nOutlier analysis not requested. Skipping outlier_analysis.py.") - - # --------------------------- - # 4. Report - # --------------------------- - print("\nRunning report.py ...") - from report import main as report_main - - report_main(dark_mode=dark_mode) - completed_stages.append("report") + # # --------------------------- + # # 1. Heatmap + # # --------------------------- + # print("\nRunning visualise_heatmap.py ...") + # from visualise_heatmap import main as heatmap_main + + # heatmap_arguments = { + # "input_path": output_csv, + # "output_name": heatmap_output_name, + # "dark_mode": dark_mode + # } + + # if raw_location_path is not None: + # heatmap_arguments["raw_location_path"] = raw_location_path + + # heatmap_main(**heatmap_arguments) + # completed_stages.append("heatmap") + # print("visualise_heatmap.py completed successfully.") + + # # --------------------------- + # # 2. Spectral Measurement Graph + # # --------------------------- + # print("\nRunning visualise_measurement.py ...") + # from visualise_measurement import main as measurement_main + # measurement_main( + # input_path=output_csv, + # output_name=spectral_graph_output_name, + # dark_mode=dark_mode + # ) + # completed_stages.append("spectral_graph") + # print("visualise_measurement.py completed successfully.") + + # # --------------------------- + # # 3. Outlier Analysis (optional) + # # --------------------------- + # if outlier_analysis: + # print("\nRunning outlier_analysis.py ...") + # from outlier_analysis import main as outlier_main + + # effective_outlier_settings = _resolve_outlier_settings( + # outlier_settings + # ) + + # outlier_main( + # input_path=output_csv, + # output_path=outlier_output_name, + # **effective_outlier_settings, + # ) + # completed_stages.append("outlier_analysis") + # print("outlier_analysis.py completed successfully.") + # else: + # print("\nOutlier analysis not requested. Skipping outlier_analysis.py.") + + # # --------------------------- + # # 4. Report + # # --------------------------- + # print("\nRunning report.py ...") + # from report import main as report_main + + # report_main(dark_mode=dark_mode) + # completed_stages.append("report") print("report.py completed successfully.") except Exception as error: From 5a14b0f02c227d7f09f4170869080ae5c5211e2f Mon Sep 17 00:00:00 2001 From: Chikith Rishi Maddi Date: Thu, 27 Aug 2026 01:21:43 +1000 Subject: [PATCH 5/6] Merging updates from the main into ui branch --- .idea/deviceManager.xml | 13 +++++ .idea/vcs.xml | 9 ++++ .idea/workspace.xml | 115 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 137 insertions(+) create mode 100644 .idea/deviceManager.xml create mode 100644 .idea/vcs.xml create mode 100644 .idea/workspace.xml diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml new file mode 100644 index 0000000..91f9558 --- /dev/null +++ b/.idea/deviceManager.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..ee8b7c5 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..7262eb1 --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + { + "customColor": "", + "associatedIndex": 5 +} + + + + { + "keyToString": { + "ModuleVcsDetector.initialDetectionPerformed": "true", + "RunOnceActivity.ShowReadmeOnStart": "true", + "RunOnceActivity.cidr.known.project.marker": "true", + "RunOnceActivity.git.unshallow": "true", + "RunOnceActivity.readMode.enableVisualFormatting": "true", + "cf.first.check.clang-format": "false", + "cidr.known.project.marker": "true", + "git-widget-placeholder": "ui" + } +} + + + + 1787618710223 + + + + \ No newline at end of file From 92b16c32c3c074d2ce36cd1b2bfe68f8626b70ce Mon Sep 17 00:00:00 2001 From: Chikith Rishi Maddi Date: Thu, 27 Aug 2026 01:24:36 +1000 Subject: [PATCH 6/6] Restored workflow --- scripts/workflow.py | 122 ++++++++++++++++++++++---------------------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/scripts/workflow.py b/scripts/workflow.py index 7410949..aa3533a 100644 --- a/scripts/workflow.py +++ b/scripts/workflow.py @@ -120,67 +120,67 @@ def run_pipeline( effective_outlier_settings: dict[str, object] | None = None try: - # # --------------------------- - # # 1. Heatmap - # # --------------------------- - # print("\nRunning visualise_heatmap.py ...") - # from visualise_heatmap import main as heatmap_main - - # heatmap_arguments = { - # "input_path": output_csv, - # "output_name": heatmap_output_name, - # "dark_mode": dark_mode - # } - - # if raw_location_path is not None: - # heatmap_arguments["raw_location_path"] = raw_location_path - - # heatmap_main(**heatmap_arguments) - # completed_stages.append("heatmap") - # print("visualise_heatmap.py completed successfully.") - - # # --------------------------- - # # 2. Spectral Measurement Graph - # # --------------------------- - # print("\nRunning visualise_measurement.py ...") - # from visualise_measurement import main as measurement_main - # measurement_main( - # input_path=output_csv, - # output_name=spectral_graph_output_name, - # dark_mode=dark_mode - # ) - # completed_stages.append("spectral_graph") - # print("visualise_measurement.py completed successfully.") - - # # --------------------------- - # # 3. Outlier Analysis (optional) - # # --------------------------- - # if outlier_analysis: - # print("\nRunning outlier_analysis.py ...") - # from outlier_analysis import main as outlier_main - - # effective_outlier_settings = _resolve_outlier_settings( - # outlier_settings - # ) - - # outlier_main( - # input_path=output_csv, - # output_path=outlier_output_name, - # **effective_outlier_settings, - # ) - # completed_stages.append("outlier_analysis") - # print("outlier_analysis.py completed successfully.") - # else: - # print("\nOutlier analysis not requested. Skipping outlier_analysis.py.") - - # # --------------------------- - # # 4. Report - # # --------------------------- - # print("\nRunning report.py ...") - # from report import main as report_main - - # report_main(dark_mode=dark_mode) - # completed_stages.append("report") + # --------------------------- + # 1. Heatmap + # --------------------------- + print("\nRunning visualise_heatmap.py ...") + from visualise_heatmap import main as heatmap_main + + heatmap_arguments = { + "input_path": output_csv, + "output_name": heatmap_output_name, + "dark_mode": dark_mode + } + + if raw_location_path is not None: + heatmap_arguments["raw_location_path"] = raw_location_path + + heatmap_main(**heatmap_arguments) + completed_stages.append("heatmap") + print("visualise_heatmap.py completed successfully.") + + # --------------------------- + # 2. Spectral Measurement Graph + # --------------------------- + print("\nRunning visualise_measurement.py ...") + from visualise_measurement import main as measurement_main + measurement_main( + input_path=output_csv, + output_name=spectral_graph_output_name, + dark_mode=dark_mode + ) + completed_stages.append("spectral_graph") + print("visualise_measurement.py completed successfully.") + + # --------------------------- + # 3. Outlier Analysis (optional) + # --------------------------- + if outlier_analysis: + print("\nRunning outlier_analysis.py ...") + from outlier_analysis import main as outlier_main + + effective_outlier_settings = _resolve_outlier_settings( + outlier_settings + ) + + outlier_main( + input_path=output_csv, + output_path=outlier_output_name, + **effective_outlier_settings, + ) + completed_stages.append("outlier_analysis") + print("outlier_analysis.py completed successfully.") + else: + print("\nOutlier analysis not requested. Skipping outlier_analysis.py.") + + # --------------------------- + # 4. Report + # --------------------------- + print("\nRunning report.py ...") + from report import main as report_main + + report_main(dark_mode=dark_mode) + completed_stages.append("report") print("report.py completed successfully.") except Exception as error: