diff options
Diffstat (limited to 'g4f/gui/server')
-rw-r--r-- | g4f/gui/server/android_gallery.py | 67 | ||||
-rw-r--r-- | g4f/gui/server/api.py | 89 |
2 files changed, 155 insertions, 1 deletions
diff --git a/g4f/gui/server/android_gallery.py b/g4f/gui/server/android_gallery.py new file mode 100644 index 00000000..9101bc02 --- /dev/null +++ b/g4f/gui/server/android_gallery.py @@ -0,0 +1,67 @@ +from kivy.logger import Logger +from kivy.clock import Clock + +from jnius import autoclass +from jnius import cast +from android import activity + +PythonActivity = autoclass('org.kivy.android.PythonActivity') +Intent = autoclass('android.content.Intent') +Uri = autoclass('android.net.Uri') + +MEDIA_DATA = "_data" +RESULT_LOAD_IMAGE = 1 + +Activity = autoclass('android.app.Activity') + +def user_select_image(on_selection): + """Open Gallery Activity and call callback with absolute image filepath of image user selected. + None if user canceled. + """ + + currentActivity = cast('android.app.Activity', PythonActivity.mActivity) + + # Forum discussion: https://groups.google.com/forum/#!msg/kivy-users/bjsG2j9bptI/-Oe_aGo0newJ + def on_activity_result(request_code, result_code, intent): + if request_code != RESULT_LOAD_IMAGE: + Logger.warning('user_select_image: ignoring activity result that was not RESULT_LOAD_IMAGE') + return + + if result_code == Activity.RESULT_CANCELED: + Clock.schedule_once(lambda dt: on_selection(None), 0) + return + + if result_code != Activity.RESULT_OK: + # This may just go into the void... + raise NotImplementedError('Unknown result_code "{}"'.format(result_code)) + + selectedImage = intent.getData(); # Uri + filePathColumn = [MEDIA_DATA]; # String[] + # Cursor + cursor = currentActivity.getContentResolver().query(selectedImage, + filePathColumn, None, None, None); + cursor.moveToFirst(); + + # int + columnIndex = cursor.getColumnIndex(filePathColumn[0]); + # String + picturePath = cursor.getString(columnIndex); + cursor.close(); + Logger.info('android_ui: user_select_image() selected %s', picturePath) + + # This is possibly in a different thread? + Clock.schedule_once(lambda dt: on_selection(picturePath), 0) + + # See: http://pyjnius.readthedocs.org/en/latest/android.html + activity.bind(on_activity_result=on_activity_result) + + intent = Intent() + + # http://programmerguru.com/android-tutorial/how-to-pick-image-from-gallery/ + # http://stackoverflow.com/questions/18416122/open-gallery-app-in-android + intent.setAction(Intent.ACTION_PICK) + # TODO internal vs external? + intent.setData(Uri.parse('content://media/internal/images/media')) + # TODO setType(Image)? + + currentActivity.startActivityForResult(intent, RESULT_LOAD_IMAGE)
\ No newline at end of file diff --git a/g4f/gui/server/api.py b/g4f/gui/server/api.py index 4dfc43d4..43bb4250 100644 --- a/g4f/gui/server/api.py +++ b/g4f/gui/server/api.py @@ -1,11 +1,40 @@ +from __future__ import annotations + import logging import json +import os.path from typing import Iterator +from uuid import uuid4 +from functools import partial try: import webview + import platformdirs except ImportError: ... +try: + from plyer import camera + from plyer import filechooser + has_plyer = True +except ImportError: + has_plyer = False +try: + from android.runnable import run_on_ui_thread + from android.storage import app_storage_path + from android.permissions import request_permissions, Permission + from android.permissions import _RequestPermissionsManager + _RequestPermissionsManager.register_callback() + from .android_gallery import user_select_image + has_android = True +except ImportError: + run_on_ui_thread = lambda a : a + app_storage_path = platformdirs.user_pictures_dir + user_select_image = partial( + filechooser.open_file, + path=platformdirs.user_pictures_dir(), + filters=[["Image", "*.jpg", "*.jpeg", "*.png", "*.webp", "*.svg"]], + ) + has_android = False from g4f import version, models from g4f import get_last_provider, ChatCompletion @@ -75,13 +104,71 @@ class Api(): return {'title': ''} def get_conversation(self, options: dict, **kwargs) -> Iterator: - window = webview.active_window() + window = webview.windows[0] + if hasattr(self, "image") and self.image is not None: + kwargs["image"] = open(self.image, "rb") for message in self._create_response_stream( self._prepare_conversation_kwargs(options, kwargs), options.get("conversation_id") ): if not window.evaluate_js(f"if (!this.abort) this.add_message_chunk({json.dumps(message)}); !this.abort && !this.error;"): break + self.image = None + self.set_selected(None) + + @run_on_ui_thread + def choose_file(self): + self.request_permissions() + filechooser.open_file( + path=platformdirs.user_pictures_dir(), + on_selection=print + ) + + @run_on_ui_thread + def choose_image(self): + self.request_permissions() + user_select_image( + on_selection=self.on_image_selection + ) + + @run_on_ui_thread + def take_picture(self): + self.request_permissions() + filename = os.path.join(app_storage_path(), f"chat-{uuid4()}.png") + camera.take_picture(filename=filename, on_complete=self.on_camera) + + def on_image_selection(self, filename): + if filename is not None and os.path.exists(filename): + self.image = filename + else: + self.image = None + self.set_selected(None if self.image is None else "image") + + def on_camera(self, filename): + if filename is not None and os.path.exists(filename): + self.image = filename + else: + self.image = None + self.set_selected(None if self.image is None else "camera") + + def set_selected(self, input_id: str = None): + window = webview.windows[0] + if window is not None: + window.evaluate_js( + f"document.querySelector(`.file-label.selected`)?.classList.remove(`selected`);" + ) + if input_id is not None and input_id in ("image", "camera"): + window.evaluate_js( + f'document.querySelector(`label[for="{input_id}"]`)?.classList.add(`selected`);' + ) + + def request_permissions(self): + if has_android: + request_permissions([ + Permission.CAMERA, + Permission.READ_EXTERNAL_STORAGE, + Permission.WRITE_EXTERNAL_STORAGE + ]) def _prepare_conversation_kwargs(self, json_data: dict, kwargs: dict): """ |