diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 3a19b95..0000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -*.deb filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore index 54e17b2..9cac494 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,20 @@ +*deb +.vscode +preview-imgs +.buildozer +worked +myenv +*mp3 +.idea +__pycache__ +bin +/lab +/thumbnails +laner-linux +for-download/ +*errors.txt +/.docs-covers + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -159,13 +176,4 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ -.buildozer -worked -myenv -*mp3 -.idea -__pycache__ -bin -lab -thumbnails +#.idea/ \ No newline at end of file diff --git a/README.md b/README.md index 5e20641..30ff5cb 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,24 @@ # Laner A Mobile/Desktop app mainly for Accessing PC files on Phone. + +----- +## How to Run +- Laner PC doesn't need any dependencies to run you can `cd workers` then `python server.py` then it'll run **without a GUI** + +- Or For **GUI Experience** `pip install PyQt5` then `python main.py` + +- or For **All Features** `python install -r requirements.txt` then `python main.py` + +----- + +## Debugging +## General +- Errors.txt For all error logs + +## On windows +- If running from script and not a built exe, You'll be able to discover your PC from Laner mobile but The Windows security app blocks requests from Laner mobile +to stop this + - open the windows security app -> Firewall & network protection section -> turn off private network firewall + - Then in your connected wifi tap properites and change setting to `private network` + > Warning: turn off turn off `private` not `public` diff --git a/assets/fonts/Helvetica.ttf b/assets/fonts/Helvetica.ttf deleted file mode 100755 index 718f22d..0000000 Binary files a/assets/fonts/Helvetica.ttf and /dev/null differ diff --git a/assets/fonts/LiberationSans-Regular.ttf b/assets/fonts/LiberationSans-Regular.ttf new file mode 100644 index 0000000..59d2e25 Binary files /dev/null and b/assets/fonts/LiberationSans-Regular.ttf differ diff --git a/assets/icons/audio.png b/assets/icons/audio.png deleted file mode 100644 index 10012c0..0000000 Binary files a/assets/icons/audio.png and /dev/null differ diff --git a/assets/icons/css.png b/assets/icons/css.png deleted file mode 100644 index 912ba6f..0000000 Binary files a/assets/icons/css.png and /dev/null differ diff --git a/assets/icons/deb.png b/assets/icons/deb.png deleted file mode 100644 index faff678..0000000 Binary files a/assets/icons/deb.png and /dev/null differ diff --git a/assets/icons/file.png b/assets/icons/file.png deleted file mode 100644 index f87abcd..0000000 Binary files a/assets/icons/file.png and /dev/null differ diff --git a/assets/icons/folders/documents.png b/assets/icons/folders/documents.png deleted file mode 100755 index d81687e..0000000 Binary files a/assets/icons/folders/documents.png and /dev/null differ diff --git a/assets/icons/folders/downloads.png b/assets/icons/folders/downloads.png deleted file mode 100644 index a66217c..0000000 Binary files a/assets/icons/folders/downloads.png and /dev/null differ diff --git a/assets/icons/folders/favorites.png b/assets/icons/folders/favorites.png deleted file mode 100755 index bcce3e3..0000000 Binary files a/assets/icons/folders/favorites.png and /dev/null differ diff --git a/assets/icons/folders/folder.png b/assets/icons/folders/folder.png deleted file mode 100755 index 0cdf9f9..0000000 Binary files a/assets/icons/folders/folder.png and /dev/null differ diff --git a/assets/icons/folders/folder1.png b/assets/icons/folders/folder1.png deleted file mode 100755 index d5a2b05..0000000 Binary files a/assets/icons/folders/folder1.png and /dev/null differ diff --git a/assets/icons/folders/home.png b/assets/icons/folders/home.png deleted file mode 100755 index cef4cf8..0000000 Binary files a/assets/icons/folders/home.png and /dev/null differ diff --git a/assets/icons/folders/music.png b/assets/icons/folders/music.png deleted file mode 100755 index aa89663..0000000 Binary files a/assets/icons/folders/music.png and /dev/null differ diff --git a/assets/icons/folders/pictures.png b/assets/icons/folders/pictures.png deleted file mode 100755 index f0cfe90..0000000 Binary files a/assets/icons/folders/pictures.png and /dev/null differ diff --git a/assets/icons/folders/share.png b/assets/icons/folders/share.png deleted file mode 100644 index b048612..0000000 Binary files a/assets/icons/folders/share.png and /dev/null differ diff --git a/assets/icons/folders/templates.png b/assets/icons/folders/templates.png deleted file mode 100755 index 2359ac6..0000000 Binary files a/assets/icons/folders/templates.png and /dev/null differ diff --git a/assets/icons/folders/videos.png b/assets/icons/folders/videos.png deleted file mode 100755 index 20b67ad..0000000 Binary files a/assets/icons/folders/videos.png and /dev/null differ diff --git a/assets/icons/font.png b/assets/icons/font.png deleted file mode 100644 index 5c41dd3..0000000 Binary files a/assets/icons/font.png and /dev/null differ diff --git a/assets/icons/html.png b/assets/icons/html.png deleted file mode 100644 index 47403f8..0000000 Binary files a/assets/icons/html.png and /dev/null differ diff --git a/assets/icons/java.png b/assets/icons/java.png deleted file mode 100644 index 042542a..0000000 Binary files a/assets/icons/java.png and /dev/null differ diff --git a/assets/icons/js.png b/assets/icons/js.png deleted file mode 100644 index fb9d092..0000000 Binary files a/assets/icons/js.png and /dev/null differ diff --git a/assets/icons/json.png b/assets/icons/json.png deleted file mode 100644 index 1041328..0000000 Binary files a/assets/icons/json.png and /dev/null differ diff --git a/assets/icons/loading.svg b/assets/icons/loading.svg deleted file mode 100644 index 45f583e..0000000 --- a/assets/icons/loading.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/assets/icons/md.png b/assets/icons/md.png deleted file mode 100644 index e330fae..0000000 Binary files a/assets/icons/md.png and /dev/null differ diff --git a/assets/icons/might/applications-python.png b/assets/icons/might/applications-python.png deleted file mode 100644 index 0713698..0000000 Binary files a/assets/icons/might/applications-python.png and /dev/null differ diff --git a/assets/icons/might/convert this later.svg b/assets/icons/might/convert this later.svg deleted file mode 100644 index af2d3b5..0000000 --- a/assets/icons/might/convert this later.svg +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - diff --git a/assets/icons/might/dcc_nav_accounts.svg b/assets/icons/might/dcc_nav_accounts.svg deleted file mode 100644 index 1a0684d..0000000 --- a/assets/icons/might/dcc_nav_accounts.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/assets/icons/might/dcc_nav_display.svg b/assets/icons/might/dcc_nav_display.svg deleted file mode 100644 index 2b1d00b..0000000 --- a/assets/icons/might/dcc_nav_display.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/assets/icons/might/polly.png b/assets/icons/might/polly.png deleted file mode 100644 index ca896f6..0000000 Binary files a/assets/icons/might/polly.png and /dev/null differ diff --git a/assets/icons/might/pycad.png b/assets/icons/might/pycad.png deleted file mode 100644 index ff9d1cd..0000000 Binary files a/assets/icons/might/pycad.png and /dev/null differ diff --git a/assets/icons/might/text-x-xges.svg b/assets/icons/might/text-x-xges.svg deleted file mode 100644 index de98fd5..0000000 --- a/assets/icons/might/text-x-xges.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/assets/icons/packed.png b/assets/icons/packed.png deleted file mode 100644 index 3653b6c..0000000 Binary files a/assets/icons/packed.png and /dev/null differ diff --git a/assets/icons/py.png b/assets/icons/py.png deleted file mode 100644 index e510c86..0000000 Binary files a/assets/icons/py.png and /dev/null differ diff --git a/assets/icons/sql.png b/assets/icons/sql.png deleted file mode 100644 index 323e0eb..0000000 Binary files a/assets/icons/sql.png and /dev/null differ diff --git a/assets/icons/trash.png b/assets/icons/trash.png deleted file mode 100755 index d25d3ab..0000000 Binary files a/assets/icons/trash.png and /dev/null differ diff --git a/assets/imgs/README.md b/assets/imgs/README.md new file mode 100644 index 0000000..d14eac2 --- /dev/null +++ b/assets/imgs/README.md @@ -0,0 +1,3 @@ +# Do not delete `image.png` and `video.png` + + they're also useful, They replace thumbnail or preview-img when genrators hit error diff --git a/assets/imgs/executable.png b/assets/imgs/executable.png new file mode 100644 index 0000000..41693a5 Binary files /dev/null and b/assets/imgs/executable.png differ diff --git a/assets/imgs/executable1.png b/assets/imgs/executable1.png new file mode 100644 index 0000000..55a706a Binary files /dev/null and b/assets/imgs/executable1.png differ diff --git a/assets/icons/image.png b/assets/imgs/image.png similarity index 100% rename from assets/icons/image.png rename to assets/imgs/image.png diff --git a/assets/imgs/live.ico b/assets/imgs/live.ico new file mode 100644 index 0000000..4b43796 Binary files /dev/null and b/assets/imgs/live.ico differ diff --git a/assets/imgs/live.png b/assets/imgs/live.png new file mode 100644 index 0000000..4b43796 Binary files /dev/null and b/assets/imgs/live.png differ diff --git a/assets/imgs/pdf.png b/assets/imgs/pdf.png new file mode 100644 index 0000000..c166efc Binary files /dev/null and b/assets/imgs/pdf.png differ diff --git a/assets/imgs/plain.png b/assets/imgs/plain.png new file mode 100644 index 0000000..4d72b27 Binary files /dev/null and b/assets/imgs/plain.png differ diff --git a/assets/imgs/use format to create more icon in future.jpg b/assets/imgs/use format to create more icon in future.jpg new file mode 100644 index 0000000..5788d34 Binary files /dev/null and b/assets/imgs/use format to create more icon in future.jpg differ diff --git a/assets/icons/video.png b/assets/imgs/video.png similarity index 100% rename from assets/icons/video.png rename to assets/imgs/video.png diff --git a/buildozer.spec b/buildozer.spec deleted file mode 100644 index 0da97a4..0000000 --- a/buildozer.spec +++ /dev/null @@ -1,462 +0,0 @@ -[app] - -# (str) Title of your application -title = Laner - -# (str) Package name -package.name = lan_ft - -# (str) Package domain (needed for android/ios packaging) -package.domain = org.laner - -# (str) Source code where the main.py live -source.dir = ./ - -# (list) Source files to include (let empty to include all the files) -source.include_exts = py,png,jpg,kv,atlas,ttf,json - -android.manifest =./AndroidManifest.xml - - -# (list) List of inclusions using pattern matching -#source.include_patterns = assets/*,images/*.png - -# (list) Source files to exclude (let empty to not exclude anything) -#source.exclude_exts = spec - -# (list) List of directory to exclude (let empty to not exclude anything) -source.exclude_dirs = tests, bin, venv, worked, __pycache__, .idea, dist, for-download - -# (list) List of exclusions using pattern matching -# Do not prefix with './' -#source.exclude_patterns = license,images/*/*.jpg - -# (str) Application versioning (method 1) -version = 1.0 - -# (str) Application versioning (method 2) -# version.regex = __version__ = ['"](.*)['"] -# version.filename = %(source.dir)s/main.py - -# (list) Application requirements -# comma separated e.g. requirements = sqlite3,kivy - -#requirements = python3,kivy,kivymd,materialyoucolor,asynckivy,asyncgui,aiohttp,multidict,attrs,yarl,propcache,async_timeout - -#requirements = python3,kivy,https://github.com/kivymd/KivyMD/archive/master.zip,materialyoucolor,asynckivy,asyncgui,jnius -requirements = python3,kivy,kivymd,materialyoucolor,asynckivy,asyncgui,pyjnius - -# (str) Custom source folders for requirements -# Sets custom source for any requirements with recipes -# requirements.source.kivy = ../../kivy - -# (str) Presplash of the application -presplash.filename = %(source.dir)s/assets/imgs/presplash.png - -# (str) Icon of the application -icon.filename = %(source.dir)s/assets/imgs/icon.png - -# (list) Supported orientations -# Valid options are: landscape, portrait, portrait-reverse or landscape-reverse -orientation = portrait - -# (list) List of service to declare -services = Sendnoti:./foreground.py -#services = NAME:ENTRYPOINT_TO_PY,NAME2:ENTRYPOINT2_TO_PY - -# -# OSX Specific -# - -# -# author = © Copyright Info - -# change the major version of python used by the app -osx.python_version = 3 - -# Kivy version to use -osx.kivy_version = 1.9.1 - -# -# Android specific -# - -# (bool) Indicate if the application should be fullscreen or not -fullscreen = 0 - -# (string) Presplash background color (for android toolchain) -# Supported formats are: #RRGGBB #AARRGGBB or one of the following names: -# red, blue, green, black, white, gray, cyan, magenta, yellow, lightgray, -# darkgray, grey, lightgrey, darkgrey, aqua, fuchsia, lime, maroon, navy, -# olive, purple, silver, teal. -#android.presplash_color = #FFFFFF - -# (string) Presplash animation using Lottie format. -# see https://lottiefiles.com/ for examples and https://airbnb.design/lottie/ -# for general documentation. -# Lottie files can be created using various tools, like Adobe After Effect or Synfig. -#android.presplash_lottie = "path/to/lottie/file.json" - -# (str) Adaptive icon of the application (used if Android API level is 26+ at runtime) -#icon.adaptive_foreground.filename = %(source.dir)s/data/icon_fg.png -#icon.adaptive_background.filename = %(source.dir)s/data/icon_bg.png - -# (list) Permissions -# (See https://python-for-android.readthedocs.io/en/latest/buildoptions/#build-options-1 for all the supported syntaxes and properties) -#android.permissions = android.permission.INTERNET,WRITE_EXTERNAL_STORAGE -android.permissions = android.permission.INTERNET, FOREGROUND_SERVICE, READ_EXTERNAL_STORAGE,RECEIVE_BOOT_COMPLETED,NOTIFICATION - -# (list) features (adds uses-feature -tags to manifest) -#android.features = android.hardware.usb.host - -# (int) Target Android API, should be as high as possible. -android.api = 34 - -# (int) Minimum API your APK / AAB will support. -#android.minapi = 21 - -# (int) Android SDK version to use -#android.sdk = 20 - -# (str) Android NDK version to use -#android.ndk = 23b - -# (int) Android NDK API to use. This is the minimum API your app will support, it should usually match android.minapi. -#android.ndk_api = 21 - -# (bool) Use --private data storage (True) or --dir public storage (False) -#android.private_storage = True - -# (str) Android NDK directory (if empty, it will be automatically downloaded.) -#android.ndk_path = - -# (str) Android SDK directory (if empty, it will be automatically downloaded.) -#android.sdk_path = - -# (str) ANT directory (if empty, it will be automatically downloaded.) -#android.ant_path = - -# (bool) If True, then skip trying to update the Android sdk -# This can be useful to avoid excess Internet downloads or save time -# when an update is due and you just want to test/build your package -# android.skip_update = False - -# (bool) If True, then automatically accept SDK license -# agreements. This is intended for automation only. If set to False, -# the default, you will be shown the license when first running -# buildozer. -# android.accept_sdk_license = False - -# (str) Android entry point, default is ok for Kivy-based app -#android.entrypoint = org.kivy.android.PythonActivity - -# (str) Full name including package path of the Java class that implements Android Activity -# use that parameter together with android.entrypoint to set custom Java class instead of PythonActivity -#android.activity_class_name = org.kivy.android.PythonActivity - -# (str) Extra xml to write directly inside the element of AndroidManifest.xml -# use that parameter to provide a filename from where to load your custom XML code -#android.extra_manifest_xml = ./src/android/extra_manifest.xml - -# (str) Extra xml to write directly inside the tag of AndroidManifest.xml -# use that parameter to provide a filename from where to load your custom XML arguments: -#android.extra_manifest_application_arguments = ./src/android/extra_manifest_application_arguments.xml - -# (str) Full name including package path of the Java class that implements Python Service -# use that parameter to set custom Java class which extends PythonService -#android.service_class_name = org.kivy.android.PythonService -android.service = True - -# (str) Android app theme, default is ok for Kivy-based app -# android.apptheme = "@android:style/Theme.NoTitleBar" - -# (list) Pattern to whitelist for the whole project -#android.whitelist = - -# (str) Path to a custom whitelist file -#android.whitelist_src = - -# (str) Path to a custom blacklist file -#android.blacklist_src = - -# (list) List of Java .jar files to add to the libs so that pyjnius can access -# their classes. Don't add jars that you do not need, since extra jars can slow -# down the build process. Allows wildcards matching, for example: -# OUYA-ODK/libs/*.jar -#android.add_jars = foo.jar,bar.jar,path/to/more/*.jar - -# (list) List of Java files to add to the android project (can be java or a -# directory containing the files) -#android.add_src = - -# (list) Android AAR archives to add -#android.add_aars = - -# (list) Put these files or directories in the apk assets directory. -# Either form may be used, and assets need not be in 'source.include_exts'. -# 1) android.add_assets = source_asset_relative_path -# 2) android.add_assets = source_asset_path:destination_asset_relative_path -#android.add_assets = - -# (list) Put these files or directories in the apk res directory. -# The option may be used in three ways, the value may contain one or zero ':' -# Some examples: -# 1) A file to add to resources, legal resource names contain ['a-z','0-9','_'] -# android.add_resources = my_icons/all-inclusive.png:drawable/all_inclusive.png -# 2) A directory, here 'legal_icons' must contain resources of one kind -# android.add_resources = legal_icons:drawable -# 3) A directory, here 'legal_resources' must contain one or more directories, -# each of a resource kind: drawable, xml, etc... -# android.add_resources = legal_resources -#android.add_resources = - -# (list) Gradle dependencies to add -#android.gradle_dependencies = - -# (bool) Enable AndroidX support. Enable when 'android.gradle_dependencies' -# contains an 'androidx' package, or any package from Kotlin source. -# android.enable_androidx requires android.api >= 28 -#android.enable_androidx = True - -# (list) add java compile options -# this can for example be necessary when importing certain java libraries using the 'android.gradle_dependencies' option -# see https://developer.android.com/studio/write/java8-support for further information -# android.add_compile_options = "sourceCompatibility = 1.8", "targetCompatibility = 1.8" - -# (list) Gradle repositories to add {can be necessary for some android.gradle_dependencies} -# please enclose in double quotes -# e.g. android.gradle_repositories = "maven { url 'https://kotlin.bintray.com/ktor' }" -#android.add_gradle_repositories = - -# (list) packaging options to add -# see https://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.PackagingOptions.html -# can be necessary to solve conflicts in gradle_dependencies -# please enclose in double quotes -# e.g. android.add_packaging_options = "exclude 'META-INF/common.kotlin_module'", "exclude 'META-INF/*.kotlin_module'" -#android.add_packaging_options = - -# (list) Java classes to add as activities to the manifest. -#android.add_activities = com.example.ExampleActivity - -# (str) OUYA Console category. Should be one of GAME or APP -# If you leave this blank, OUYA support will not be enabled -#android.ouya.category = GAME - -# (str) Filename of OUYA Console icon. It must be a 732x412 png image. -#android.ouya.icon.filename = %(source.dir)s/data/ouya_icon.png - -# (str) XML file to include as an intent filters in tag -#android.manifest.intent_filters = - -# (list) Copy these files to src/main/res/xml/ (used for example with intent-filters) -#android.res_xml = PATH_TO_FILE, - -# (str) launchMode to set for the main activity -#android.manifest.launch_mode = standard - -# (str) screenOrientation to set for the main activity. -# Valid values can be found at https://developer.android.com/guide/topics/manifest/activity-element -#android.manifest.orientation = fullSensor - -# (list) Android additional libraries to copy into libs/armeabi -#android.add_libs_armeabi = libs/android/*.so -#android.add_libs_armeabi_v7a = libs/android-v7/*.so -#android.add_libs_arm64_v8a = libs/android-v8/*.so -#android.add_libs_x86 = libs/android-x86/*.so -#android.add_libs_mips = libs/android-mips/*.so - -# (bool) Indicate whether the screen should stay on -# Don't forget to add the WAKE_LOCK permission if you set this to True -#android.wakelock = False - -# (list) Android application meta-data to set (key=value format) -#android.meta_data = - -# (list) Android library project to add (will be added in the -# project.properties automatically.) -#android.library_references = - -# (list) Android shared libraries which will be added to AndroidManifest.xml using tag -#android.uses_library = - -# (str) Android logcat filters to use -#android.logcat_filters = *:S python:D - -# (bool) Android logcat only display log for activity's pid -#android.logcat_pid_only = False - -# (str) Android additional adb arguments -#android.adb_args = -H host.docker.internal - -# (bool) Copy library instead of making a libpymodules.so -#android.copy_libs = 1 - -# (list) The Android archs to build for, choices: armeabi-v7a, arm64-v8a, x86, x86_64 -# In past, was `android.arch` as we weren't supporting builds for multiple archs at the same time. -android.archs = arm64-v8a, armeabi-v7a - -# (int) overrides automatic versionCode computation (used in build.gradle) -# this is not the same as app version and should only be edited if you know what you're doing -# android.numeric_version = 1 - -# (bool) enables Android auto backup feature (Android API >=23) -android.allow_backup = True - -# (str) XML file for custom backup rules (see official auto backup documentation) -# android.backup_rules = - -# (str) If you need to insert variables into your AndroidManifest.xml file, -# you can do so with the manifestPlaceholders property. -# This property takes a map of key-value pairs. (via a string) -# Usage example : android.manifest_placeholders = [myCustomUrl:\"org.kivy.customurl\"] -# android.manifest_placeholders = [:] - -# (bool) Skip byte compile for .py files -# android.no-byte-compile-python = False - -# (str) The format used to package the app for release mode (aab or apk or aar). -# android.release_artifact = aab - -# (str) The format used to package the app for debug mode (apk or aar). -# android.debug_artifact = apk - -# -# Python for android (p4a) specific -# - -# (str) python-for-android URL to use for checkout -#p4a.url = - -# (str) python-for-android fork to use in case if p4a.url is not specified, defaults to upstream (kivy) -#p4a.fork = kivy - -# (str) python-for-android branch to use, defaults to master -#p4a.branch = master - -# (str) python-for-android specific commit to use, defaults to HEAD, must be within p4a.branch -#p4a.commit = HEAD - -# (str) python-for-android git clone directory (if empty, it will be automatically cloned from github) -#p4a.source_dir = - -# (str) The directory in which python-for-android should look for your own build recipes (if any) -#p4a.local_recipes = - -# (str) Filename to the hook for p4a -#p4a.hook = - -# (str) Bootstrap to use for android builds -# p4a.bootstrap = sdl2 - -# (int) port number to specify an explicit --port= p4a argument (eg for bootstrap flask) -#p4a.port = - -# Control passing the --use-setup-py vs --ignore-setup-py to p4a -# "in the future" --use-setup-py is going to be the default behaviour in p4a, right now it is not -# Setting this to false will pass --ignore-setup-py, true will pass --use-setup-py -# NOTE: this is general setuptools integration, having pyproject.toml is enough, no need to generate -# setup.py if you're using Poetry, but you need to add "toml" to source.include_exts. -#p4a.setup_py = false - -# (str) extra command line arguments to pass when invoking pythonforandroid.toolchain -#p4a.extra_args = - - - -# -# iOS specific -# - -# (str) Path to a custom kivy-ios folder -#ios.kivy_ios_dir = ../kivy-ios -# Alternately, specify the URL and branch of a git checkout: -ios.kivy_ios_url = https://github.com/kivy/kivy-ios -ios.kivy_ios_branch = master - -# Another platform dependency: ios-deploy -# Uncomment to use a custom checkout -#ios.ios_deploy_dir = ../ios_deploy -# Or specify URL and branch -ios.ios_deploy_url = https://github.com/phonegap/ios-deploy -ios.ios_deploy_branch = 1.10.0 - -# (bool) Whether or not to sign the code -ios.codesign.allowed = false - -# (str) Name of the certificate to use for signing the debug version -# Get a list of available identities: buildozer ios list_identities -#ios.codesign.debug = "iPhone Developer: ()" - -# (str) The development team to use for signing the debug version -#ios.codesign.development_team.debug = - -# (str) Name of the certificate to use for signing the release version -#ios.codesign.release = %(ios.codesign.debug)s - -# (str) The development team to use for signing the release version -#ios.codesign.development_team.release = - -# (str) URL pointing to .ipa file to be installed -# This option should be defined along with `display_image_url` and `full_size_image_url` options. -#ios.manifest.app_url = - -# (str) URL pointing to an icon (57x57px) to be displayed during download -# This option should be defined along with `app_url` and `full_size_image_url` options. -#ios.manifest.display_image_url = - -# (str) URL pointing to a large icon (512x512px) to be used by iTunes -# This option should be defined along with `app_url` and `display_image_url` options. -#ios.manifest.full_size_image_url = - - -[buildozer] - -# (int) Log level (0 = error only, 1 = info, 2 = debug (with command output)) -log_level = 2 - -# (int) Display warning if buildozer is run as root (0 = False, 1 = True) -warn_on_root = 1 - -# (str) Path to build artifact storage, absolute or relative to spec file -# build_dir = ./.buildozer - -# (str) Path to build output (i.e. .apk, .aab, .ipa) storage -# bin_dir = ./bin - -# ----------------------------------------------------------------------------- -# List as sections -# -# You can define all the "list" as [section:key]. -# Each line will be considered as a option to the list. -# Let's take [app] / source.exclude_patterns. -# Instead of doing: -# -#[app] -#source.exclude_patterns = license,data/audio/*.wav,data/images/original/* -# -# This can be translated into: -# -#[app:source.exclude_patterns] -#license -#data/audio/*.wav -#data/images/original/* -# - - -# ----------------------------------------------------------------------------- -# Profiles -# -# You can extend section / key with a profile -# For example, you want to deploy a demo version of your application without -# HD content. You could first change the title to add "(demo)" in the name -# and extend the excluded directories to remove the HD content. -# -#[app@demo] -#title = My Application (demo) -# -#[app:source.exclude_patterns@demo] -#images/hd/* -# -# Then, invoke the command line with the "demo" profile: -# -#buildozer --profile demo android debug diff --git a/data/store.json b/data/store.json deleted file mode 100755 index ceea8ce..0000000 --- a/data/store.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "Dark Mode": true -} \ No newline at end of file diff --git a/desktop_version.py b/desktop_version.py deleted file mode 100644 index 5adf184..0000000 --- a/desktop_version.py +++ /dev/null @@ -1,172 +0,0 @@ -import sys,os -import threading -from PyQt5.QtWidgets import ( - QApplication, QMainWindow, QVBoxLayout, QLabel, QPushButton, QWidget, - QSystemTrayIcon,QMenu,QAction -) -from PyQt5.QtGui import QFont,QIcon -from PyQt5.QtCore import Qt -from workers.server import FileSharingServer -from workers.helper import getSystem_IpAdd - - -class FileShareApp(QMainWindow): - def __init__(self): - super().__init__() - self.setWindowTitle("Laner") - self.setGeometry(100, 100, 500, 500) - self.server_thread = None - # self.server = None - self.running = False - self.hidden_ip = False - self.ip = None - - # Main Widget and Layout - self.central_widget = QWidget() - self.setCentralWidget(self.central_widget) - self.layout = QVBoxLayout(self.central_widget) - - # Title Label - self.title_label = QLabel("Laner") - self.title_label.setAlignment(Qt.AlignCenter) - self.title_label.setFont(QFont("Arial", 24, QFont.Bold)) - self.title_label.setStyleSheet("color: rgb(0, 179, 153);") - self.layout.addWidget(self.title_label) - - # Subtitle Label - self.subtitle_label = QLabel("Fast and Secure File Sharing over LAN") - self.subtitle_label.setAlignment(Qt.AlignCenter) - self.subtitle_label.setFont(QFont("Arial", 16)) - self.subtitle_label.setStyleSheet("color: gray;") - self.layout.addWidget(self.subtitle_label) - - # Hint Message Label - self.hint_message = QLabel("Connect your PC to your HotSpot\nand Press Start to get the code") - self.hint_message.setAlignment(Qt.AlignCenter) - self.hint_message.setFont(QFont("Arial", 20)) - self.layout.addWidget(self.hint_message) - - # Start Button - self.start_button = QPushButton("Start Server") - self.start_button.clicked.connect(self.start_server) - self.start_button.setFixedSize(120, 40) - self.start_button.setStyleSheet("background-color: white; color: rgb(0, 179, 153);") - self.layout.addWidget(self.start_button, alignment=Qt.AlignCenter) - - # Status Label - self.status_label = QLabel("Server not running") - self.status_label.setAlignment(Qt.AlignCenter) - self.status_label.setStyleSheet("color: gray;") - self.layout.addWidget(self.status_label) - - # Hide IP Button - self.hide_ip_button = QPushButton("Hide Code") - self.hide_ip_button.clicked.connect(self.hide_ip) - self.hide_ip_button.setFixedSize(100, 40) - self.hide_ip_button.setStyleSheet("background-color: white; color: rgb(0, 179, 153);") - self.hide_ip_button.setVisible(False) - self.layout.addWidget(self.hide_ip_button, alignment=Qt.AlignRight) - - # System Tray - self.create_system_tray() - - def create_system_tray(self): - self.tray = QSystemTrayIcon(self) - - icon_path = "icon.png" - if hasattr(sys, "_MEIPASS"): - icon_path = os.path.join(sys._MEIPASS, "assets", "imgs", "icon.png") - - self.tray.setIcon(QIcon(icon_path)) - - # Create tray menu - tray_menu = QMenu() - - show_action = QAction("Show", self) - show_action.triggered.connect(self.show) - tray_menu.addAction(show_action) - - quit_action = QAction("Quit", self) - quit_action.triggered.connect(QApplication.quit) - tray_menu.addAction(quit_action) - - self.tray.setContextMenu(tray_menu) - self.tray.show() - - def start_server(self): - if self.running: - self.status_label.setText("Server is already running! Don't share the code.") - self.on_stop() - return - - self.ip = getSystem_IpAdd() - if self.ip is None: - self.hint_message.setText("Connect your PC to your Local Network.\nNo need for Internet Capability.") - self.hint_message.setStyleSheet("color: black;") - self.hint_message.setFont(QFont("Arial", 20)) - - return - else: - self.hint_message.setText("Hidden Code" if self.hidden_ip else self.ip) - self.hint_message.setStyleSheet("color: gray;") - - # Start the server in a separate thread - port = 8000 - self.server_thread = threading.Thread(target=self.run_server, args=(port,)) - self.server_thread.daemon = True - self.server_thread.start() - self.running = True - self.hint_message.setFont(QFont("Arial", 34)) - - self.hide_ip_button.setVisible(True) - self.start_button.setText("End Server") - self.status_label.setText("Server Running. Don't share the code.\nWrite the exact code in the Link Tab on Your Phone.") - - def run_server(self, port): - # Initialize the server - self.server = FileSharingServer(port, '/') - try: - # Start the server - self.server.start() - print("Press Ctrl+C to stop the server.") - except KeyboardInterrupt: - # Stop the server on Ctrl+C - print("\nStopping the server...") - self.server.stop() - - def on_stop(self): - self.hint_message.setText("Goodbye!") - - self.running = False - self.start_button.setText("Start Server") - self.status_label.setText("Server Ended!") - self.hide_ip_button.setVisible(False) - - # Stop the server - if self.server_thread: - self.server_thread.join() - self.server.stop() - - def hide_ip(self): - if not self.running: - return - - if not self.hidden_ip: - self.hint_message.setText("Hidden Code") - self.hide_ip_button.setText("Show Code") - else: - self.hint_message.setText(self.ip) - self.hide_ip_button.setText("Hide Code") - - self.hidden_ip = not self.hidden_ip - def closeEvent(self, event): - print('peek',event) - self.hide() - event.ignore() - -if __name__ == "__main__": - app = QApplication(sys.argv) - window = FileShareApp() - window.show() - # app.exec_() - sys.exit(app.exec_()) diff --git a/for-download/laner-linux.deb b/for-download/laner-linux.deb deleted file mode 100644 index 8cb44b4..0000000 --- a/for-download/laner-linux.deb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f4075677f12b71be6e7a7ca8b1ceceec387a04887bebc223bb80c1d08f3c12bd -size 149196150 diff --git a/main.py b/main.py index bdd96e0..1b8b83e 100644 --- a/main.py +++ b/main.py @@ -1,462 +1,214 @@ -from kivymd.app import MDApp -from kivymd.uix.button import MDButton, MDButtonText -from kivy.clock import Clock -from kivy.properties import ( BooleanProperty, ListProperty, StringProperty) -from kivymd.app import MDApp -from kivy.core.window import Window -from kivymd.uix.label import MDIcon, MDLabel -from kivy.metrics import dp,sp -from kivymd.uix.boxlayout import MDBoxLayout -from kivy.uix.screenmanager import ScreenManager, SlideTransition,NoTransition -from kivy.uix.label import Label -from kivymd.uix.behaviors import RectangularRippleBehavior -from kivy.uix.behaviors import ButtonBehavior -from kivymd.uix.relativelayout import MDRelativeLayout -from kivymd.uix.screen import MDScreen -from kivy.lang import Builder -from kivymd.uix.textfield import MDTextField -from kivy.uix.recycleview.views import RecycleDataViewBehavior -from kivy.uix.recyclegridlayout import RecycleGridLayout -from kivy.uix.checkbox import CheckBox - -from kivy.utils import platform # OS -from kivymd.material_resources import DEVICE_TYPE # if mobile or PC - -import requests -import os, sys, json - -from widgets.popup import Snackbar -from widgets.templates import DisplayFolderScreen, Header -from workers.helper import getSERVER_IP, makeDownloadFolder, setHiddenFilesDisplay, setSERVER_IP - -# For Dev -if DEVICE_TYPE != "mobile": - Window.size = (400, 1000) - -# TODO For Theme -THEME_COLOR_TUPLE=(.6, .9, .8, 1) -__DIR__ = getattr(sys, '_MEIPASS', os.path.abspath(os.path.dirname(__file__))) -MY_DATABASE_PATH = os.path.join(__DIR__, 'data', 'store.json') - - -#Making/Getting Downloads Folder -my_downloads_folder=makeDownloadFolder() - -if platform == 'android': - setSERVER_IP('') - try: - from jnius import autoclass - from android import mActivity # type: ignore - context = mActivity.getApplicationContext() - SERVICE_NAME = str(context.getPackageName()) + '.Service' + 'Sendnoti' - service = autoclass(SERVICE_NAME) - service.start(mActivity,'') - print('returned service') - except Exception as e: - print(f'Foreground service failed {e}') - - - from android.permissions import request_permissions, Permission,check_permission # type: ignore - from android.storage import app_storage_path, primary_external_storage_path # type: ignore - - - print('Asking permission...') - def check_permissions(permissions): - for permission in permissions: - if check_permission(permission) != True: - return False - return True - - permissions=[Permission.WRITE_EXTERNAL_STORAGE,Permission.READ_EXTERNAL_STORAGE] - if check_permissions(permissions): - request_permissions(permissions) - - - - -Builder.load_string(''' -: - radius: dp(5) - size_hint:(1,1) - theme_bg_color: "Custom" - on_release: app.my_screen_manager.current_screen.setPath(self.path) - - AsyncImage: - id: test_stuff - source: root.icon - size_hint: [.9,.7] - fit_mode: 'contain' - mipmap: True - pos_hint: {"top":1} - radius: (dp(5),dp(5),0,0) - MDButton: - theme_bg_color: "Custom" - theme_height: "Custom" - theme_width: "Custom" - opacity: 0 if root.is_dir else 1 - radius: '15sp' - size_hint: [None, None] - width: '35sp' - height: '35sp' - md_bg_color: [.7,.6,.9,1] - pos_hint: {"top": .979, "right": .97} - on_release: app.my_screen_manager.current_screen.showDownloadDialog(root.path) - - MDButtonIcon: - icon: "download" - pos_hint: {'x':.23,'y':.2} - theme_icon_color: "Custom" - icon_color: [1,1,1,1] - - Label: - text: root.myFormat(root.text) - font_size: '11sp' - size_hint: [None, None] - size: (root.width, 40) - text_size: (root.width, None) - # max_lines: 2 - -: - viewclass: 'MyCard' - size_hint: (1, .9) - My_RecycleGridLayout: - default_size: 1, '140sp' - default_size_hint: 1, None - spacing:18 - padding:"10dp" - size_hint: (1, None) - height: self.minimum_height -''') - - -class My_RecycleGridLayout(RecycleGridLayout): - screen_history = [] # Stack to manage visited screens - def __init__(self, **kwargs): - # print(Window.width) - super().__init__(**kwargs) - if Window.width > 800: - self.cols=5 - else: - try: - self.cols= int(str(Window.width)[0]) -2 - except: - self.cols=2 - def on_size(self, *args): - if Window.width > 800: - self.cols=5 - elif Window.width < 300: - self.cols=3 - elif Window.width < 200: - self.cols=2 - else: - self.cols=4 - - -class WindowManager(ScreenManager): - screen_history = [] # Stack to manage visited screens - def __init__(self, **kwargs): - super().__init__(**kwargs) - Window.bind(on_keyboard=self.Android_back_click) - - def changeScreenAnimation(self, screen_name): - if self.screen_names.index(screen_name) > self.screen_names.index(self.current): - self.transition=SlideTransition(direction='left') - else: - self.transition=SlideTransition(direction='right') - def change_screen(self, screen_name): - """Navigate to a specific screen and record history.""" - self.changeScreenAnimation(screen_name) - - if self.current != screen_name: - self.screen_history.append(self.current) - self.current = screen_name - - def findTabButtonsAndChangeDesign(self): - # TODO Google a way to select children by id prop - # print([type(widget) for widget in self.walk(loopback=True)][0].ids) - # test = [type(widget) for widget in self.walk(loopback=True)][0].ids - # print(test) - # print(test.att()) - tabs_buttons_list=self.parent.children[0].children - for each_btn in tabs_buttons_list: - each_btn.checkWidgetDesign(self.current) - def Android_back_click(self, window, key, *largs): - """Handle the Android back button.""" - if key == 27: # Back button key code - if self.current != 'settings' and len(self.current_screen.screen_history): - # might switch to "if []:" since it works on python but "if len([]):" is more understandable - # print(self.current_screen.screen_history) - last_dir = self.current_screen.screen_history.pop() - self.current_screen.setPath(last_dir, False) - return True - - if len(self.screen_history): # Navigate back to the previous screen - last_screen = self.screen_history.pop() - self.changeScreenAnimation(last_screen) - self.current = last_screen - self.findTabButtonsAndChangeDesign() - return True - else: - # Exit the app if no history - return False - - -class MY_MDIcon(MDIcon): - "MDIcon Font size doesn't work unless creating my own class and passing MDIcon in it." - font_size__ = StringProperty() - - def __init__(self, **kwargs): - super().__init__(**kwargs) - # self.font_size = self.font_size__ - - -class TabButton(RectangularRippleBehavior,ButtonBehavior,MDBoxLayout): - text=StringProperty() - icon=StringProperty() - screen=StringProperty() # screen name - screen_manager_current=StringProperty() # current screen name - # color=ListProperty() - tabs_buttons_list=[] - def __init__(self, **kwargs): - super().__init__(**kwargs) - # self.ripple - self.orientation='vertical' - self.padding=[dp(0),dp(8),dp(0),dp(5)] - self.line_color=(.2, .2, .2, 0) - self._radius=1 - self.id=self.text - self.spacing='-5sp' - self.size_hint=[1,1] - self.label= Label( - text=self.text, halign='center', - font_name='assets/fonts/Helvetica.ttf', - font_size=sp(13), - ) - self.btn_icon = MDIcon( - icon=self.icon, - # font_size__='40sp', - # size_hint=[.5,.5], - pos_hint={'center_x': 0.5,'center_y': 0}, - theme_text_color="Custom", - ) - - self.add_widget(self.btn_icon) - self.add_widget(self.label) - self.tabs_buttons_list.append(self) - self.checkWidgetDesign(self.screen_manager_current) - def on_release(self): - self.designWidgets(self.screen) - return super().on_release() - def designWidgets(self,cur_screen): - for each_btn in self.tabs_buttons_list: - each_btn.checkWidgetDesign(cur_screen) - - def checkWidgetDesign(self,cur_screen): - with open(MY_DATABASE_PATH) as change_mode1: - Bool_theme = json.load(change_mode1) - if Bool_theme['Dark Mode']: - self.btn_icon.text_color=self.label.color = [1, 1, 1, 1] - - else: - self.btn_icon.text_color=self.label.color = [0, 0, 0, 1] - - if self.screen == cur_screen: - # print(self.screen,cur_screen) - # MDIcon.text_color=Label.color = THEME_COLOR_TUPLE - self.btn_icon.text_color=self.label.color = self.theme_cls.backgroundColor - - -class BottomNavigationBar(MDBoxLayout): - screen = StringProperty() - def __init__(self, screen_manager:WindowManager,**kwargs): - super(BottomNavigationBar, self).__init__(**kwargs) - - self.screen_manager=screen_manager - icons = ['home', 'download', 'connection'] - # icons = ['home', 'server-network-outline', 'connection'] - - for_label_text = ['Home','Storage','Link'] - screens=screen_manager.screen_names - self.size_hint =[ 1, .1] - self.padding=0 - self.spacing=0 - self.md_bg_color = (.2, .2, .2, .5) - - for index in range(len(icons)): - self.btn = TabButton( - # id=str(index), - # size_hint=(1, 1), - # color=colors[index], - icon=icons[index], - text=for_label_text[index], - screen=screens[index], - screen_manager_current=screen_manager.current, - on_release=lambda x,cur_index=index: self.setScreen(x,screens[cur_index]) - ) - self.add_widget(self.btn) - - - def setScreen(self,btn:TabButton,screen_name): - self.screen_manager.change_screen(screen_name) - # btn.designWidgets(self.screen_manager.current) - - -class MySwitch(MDBoxLayout): - text=StringProperty() - switch_state=BooleanProperty(False) - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.size_hint=[1,None] - self.height='40sp' - self.padding=[sp(20),0] - # self.md_bg_color=[1,1,0,1] - - self.add_widget(MDLabel( - halign='left',valign='center', - text_color=[1,1,1,1], - text=self.text)) - self.checkbox_=CheckBox(size_hint=(None,1),width='40sp',color=[.6, .9, .8, 2], active=self.switch_state, group='1',pos_hint={'right':1}) - self.add_widget(self.checkbox_) - - -validated_paths=[] -class MyCard(RecycleDataViewBehavior,RectangularRippleBehavior,ButtonBehavior,MDRelativeLayout): - path=StringProperty() - icon=StringProperty() - text=StringProperty() - thumbnail_url=StringProperty() - thumbnail_path=StringProperty() - is_dir=BooleanProperty() - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.ripple_effect=False - # print(self.thumbnail_url,'lll') - # if not self.is_dir: - # print('polrwda---wdewwniw====') - # Clock.schedule_once(lambda dt: self.update_image(), 6) - - def isFile(self,path:str): - - try: - response=requests.get(f"http://{getSERVER_IP()}:8000/api/isfile",json={'path':path},timeout=2) - if response.status_code != 200: - # Clock.schedule_once(lambda dt:Snackbar(h1="Dev pinging for thumb valid")) - return False - # print(response.json()['data']) - return response.json()['data'] - - except Exception as e: - Clock.schedule_once(lambda dt:Snackbar(h1="Dev pinging for thumb valid")) - print(f"isDir method: {e}") - return False - - def on_thumbnail_url(self, instance, value): - """Called whenever thumbnail_url changes.""" - self.event = Clock.schedule_interval(lambda dt: self.update_image(), 1) - - def update_image(self): - global validated_paths - if self.thumbnail_url and (self.thumbnail_path in validated_paths or self.isFile(self.thumbnail_path)): - if self.thumbnail_path not in validated_paths: - validated_paths.append(self.thumbnail_path) - - self.icon = self.thumbnail_url - self.event.cancel() - elif not self.thumbnail_url: - self.event.cancel() - - - def myFormat(self, text:str): - if len(text) > 20: - return text[0:18] + '...' - return text - -class SettingsScreen(MDScreen): - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.name='settings' - self.layout=MDBoxLayout( - # md_bg_color=[1,0,0,1], - # adaptive_height=True, - size_hint=[1,.1], - - pos_hint={'top':1} - ) - self.layout.orientation='vertical' - self.layout.spacing=sp(10) - - self.header=Header( - # size_hint=[1,None],height=sp(50), - size_hint=[1,1], - - text="Settings",text_halign='left') - - self.content=MDBoxLayout(orientation='vertical', - size_hint=[1,.8], - # md_bg_color=[1,0,0,1], - adaptive_height=True, - spacing=sp(20), - padding=[sp(10),0], - pos_hint={'top':.86} - ) - - portInput=MDTextField(theme_text_color= "Custom",text_color_focus=[.9,.9,1,1],text_color_normal=[1,1,1,1],pos_hint={'center_x':.5},size_hint=[.8,None],height=dp(80)) - verifyBtn=MDButton( - theme_height= "Custom", theme_width= "Custom", - on_release=lambda x: self.setIP(portInput.text), - # on_release=lambda x: Snackbar(confirm_txt='Ok'), - pos_hint={'center_x':.5},size_hint=[None,None],size=[sp(120),dp(50)],radius=0) - verifyBtn.add_widget(MDButtonText(text='Verify Code',pos_hint= {"center_x": .5, "center_y": .5})) - self.layout.add_widget(self.header) - # TODO Get PC name when connection verifed and display connected to ... - self.my_switch=MySwitch(text='Show hidden files') - self.my_switch.checkbox_.bind(active=self.on_checkbox_active) - - self.content.add_widget(self.my_switch) - self.content.add_widget(portInput) - self.content.add_widget(verifyBtn) - - self.add_widget(self.layout) - self.add_widget(self.content) - def on_checkbox_active(self,checkbox, value): - setHiddenFilesDisplay(value) - def setIP(self,text): - setSERVER_IP(text) - try: - response=requests.get(f"http://{text}:8000/ping",json={'passcode':'blah blah'},timeout=.2) - if response.status_code == 200: - Snackbar(h1="Verification Successfull") - else: - Snackbar(h1="Bad Code check \"Laner PC\" for right one") - except: - Snackbar(h1="Bad Code check \"Laner PC\" for right one") - - print('My address',text, getSERVER_IP()) - -class Laner(MDApp): - - def build(self): - self.title='Laner' - - self.theme_cls.backgroundColor=THEME_COLOR_TUPLE - root_layout=MDBoxLayout(orientation='vertical') - root_layout.md_bg_color=.3,.3,.3,1 - - self.my_screen_manager = WindowManager() - self.my_screen_manager.add_widget(DisplayFolderScreen(name='upload',current_dir='Home')) - self.my_screen_manager.add_widget(DisplayFolderScreen(name='download',current_dir='.')) - self.my_screen_manager.add_widget(SettingsScreen()) - self.my_screen_manager.transition=NoTransition() - self.my_screen_manager.current='settings' - bottom_navigation_bar=BottomNavigationBar(self.my_screen_manager) - - root_layout.add_widget(self.my_screen_manager) - root_layout.add_widget(bottom_navigation_bar) - - return root_layout - - - -if __name__ == '__main__': - Laner().run() +import sys, os, traceback +from PyQt5.QtWidgets import QApplication, QMainWindow, QSystemTrayIcon, QMenu, QAction, QStackedWidget +from PyQt5.QtGui import QFont, QIcon +from PyQt5.QtCore import Qt,QThread,pyqtSignal, QObject + +from screens import MainScreen,SettingsScreen +from popups import ConnectionRequest + +from workers.server import FileSharingServer +from workers.helper import getUserPCName, getAbsPath +from workers.sword import NetworkManager +import workers.config # Runs App Configurations and sets global exception handler + +DEV=1 + + +class WorkerThread(QThread): + update_signal = pyqtSignal(object) # Define signal + def __init__(self, ip, connection_signal, parent=None): + super().__init__(parent) + self.ip =ip + self.connection_signal = connection_signal + def run(self): + try: + server = FileSharingServer(port=8000,ip=self.ip, directory= '/', connection_signal=self.connection_signal) + server.start() + self.update_signal.emit(server) + + try: + # Using port from started Server + NetworkManager().broadcast_ip(server.port,server.websocket_port) + except Exception as e: + print('BroadCast Failed: ',e) + + except Exception as e: + print("See uncaught Errors on App main Thread: ",e) + +class ConnectionSignal(QObject): + connection_request = pyqtSignal(dict, object) # Emits ({device_name:'',request:''}, websocket) + +class FileShareApp(QMainWindow): + def __init__(self): + super().__init__() + self.running = False + self.hidden_ip = False + self.server = None + self.ip = None + self.port = None + self.icon=getAbsPath("assets", "imgs", "img.ico" if os.name == 'nt' else "icon.png") + self.connection_signal = ConnectionSignal() + self.connection_signal.connection_request.connect(self.show_connection_request) + self.init_ui() + + def init_ui(self): + self.setWindowTitle("Laner") + self.tray_live_icon=getAbsPath("assets", "imgs", "live.ico" if os.name == 'nt' else "live.png") + self.setWindowIcon(QIcon(self.icon)) + self.setGeometry(100, 100, 500, 500) + + # Stacked Widget + self.stacked_widget = QStackedWidget() + self.setCentralWidget(self.stacked_widget) + + # Main Screen + self.main_screen = MainScreen(self) + self.stacked_widget.addWidget(self.main_screen) + + # Settings Screen + self.settings_screen = SettingsScreen(self) + self.stacked_widget.addWidget(self.settings_screen) + + # System Tray + self.create_system_tray() + + # Create PopUps + self.request_connection_popup:ConnectionRequest=None + + def restart_server(self): + QApplication.quit() + import subprocess,sys + py=sys.executable + print('py --> ',py) + script=sys.argv[0] + print('script --> ',script) + os.execv(py, [py, script]) + + # In your Qt main window class + + def show_connection_request(self, device_name, handler): + """Show connection dialog""" + print(device_name,handler, ' name nd handler') + # self.request_connection_popup.show_request(message_object=device_name, websocket=handler.websocket,event_loop=handler.websocket.loop) + # self.request_connection_popup = # + ConnectionRequest(message_object=device_name, handler=handler,parent=self) + # self.request_connection_popup.show_request() + + def create_system_tray(self): + self.tray = QSystemTrayIcon(self) + self.tray.setIcon(QIcon(self.icon)) + + # Create tray menu + tray_menu = QMenu() + if DEV: + self.dev_btn = QAction("Restart Server", self) + self.dev_btn.triggered.connect(self.restart_server) + tray_menu.addAction(self.dev_btn) + + self.start_stop_action = QAction("Quick Connect", self) + self.start_stop_action.triggered.connect(self.start_server) + tray_menu.addAction(self.start_stop_action) + + show_action = QAction("Show Code", self) + show_action.triggered.connect(self.show) + tray_menu.addAction(show_action) + + quit_action = QAction("Quit", self) + quit_action.triggered.connect(QApplication.quit) + tray_menu.addAction(quit_action) + + self.tray.setContextMenu(tray_menu) + self.tray.show() + + def start_server(self): + if self.running: + self.on_stop() + return + + network = NetworkManager() + self.ip = network.get_server_ip() + if self.ip is None: + self.main_screen.hint_message.setText("Connect your PC to your Local Network.\nNo need for Internet Capability.") + self.main_screen.hint_message.setStyleSheet("color: black;") + self.main_screen.hint_message.setFont(QFont("Arial", 20)) + + return + # self.main_screen.hint_message.setText("Hidden Code" if self.hidden_ip else self.ip) + # Start the server in a separate thread + self.run_server(success=self.success, connection_signal=self.connection_signal) + + def success(self,server): + self.running = True + self.server=server + self.port=server.port + + self.main_screen.hint_message.setStyleSheet("color: gray;") + self.main_screen.hint_message.setText("Hidden Code" if self.hidden_ip else str(self.port)) # displaying port instead of id + self.main_screen.hint_message.setFont(QFont("Arial", 34)) + self.main_screen.hide_ip_button.setVisible(True) + self.main_screen.start_button.setText("End Server") + self.main_screen.status_label.setText("Server Running. Don't share the code.\nWrite the exact code in the Link Tab on Your Phone.") + + self.settings_screen.ip_label.setText(str(self.ip)) + + self.start_stop_action.setText("Disconnect") + self.tray.setIcon(QIcon(self.tray_live_icon)) + + def run_server(self, connection_signal,success=None): + self.worker = WorkerThread(self.ip, connection_signal) + self.worker.update_signal.connect(success) + self.worker.start() + + def on_stop(self): + self.running = False + + self.main_screen.hint_message.setText("Goodbye!") + self.main_screen.hide_ip_button.setVisible(False) + self.main_screen.start_button.setText("Start Server") + self.main_screen.status_label.setText("Server Ended!") + + self.settings_screen.ip_label.setText(str(self.ip)) + + self.start_stop_action.setText("Quick Connect") + self.tray.setIcon(QIcon(self.icon)) + + self.server.stop() + NetworkManager().keep_broadcasting = True + + def hide_ip(self): + if not self.running: + return + + if not self.hidden_ip: + self.main_screen.hint_message.setText("Hidden Code") + self.main_screen.hide_ip_button.setText("Show Code") + else: + self.main_screen.hint_message.setText(str(self.port)) + self.main_screen.hide_ip_button.setText("Hide Code") + + self.hidden_ip = not self.hidden_ip + + def open_settings(self): + self.stacked_widget.setCurrentWidget(self.settings_screen) + + def closeEvent(self, event): + # print('peek', event) + self.hide() + event.ignore() + +if __name__ == "__main__": + print(sys.argv) + auto_start = "--autostart" in sys.argv + + app = QApplication(sys.argv) + window = FileShareApp() + + if auto_start or DEV: + window.start_server() + else: + window.show() + try: + sys.exit(app.exec_()) + except KeyboardInterrupt: + # When running from cmd and User try to close App with CTRL+C, i don't want to log error after closing with tray + pass \ No newline at end of file diff --git a/desktop_version.spec b/main.spec similarity index 73% rename from desktop_version.spec rename to main.spec index aa7ac01..2e0e9d9 100644 --- a/desktop_version.spec +++ b/main.spec @@ -1,12 +1,16 @@ # -*- mode: python ; coding: utf-8 -*- +debugging=False + import os -icon_path=os.path.join("assets","imgs","icon.png") +_icon_path=["assets","imgs","img.ico" if os.name == 'nt' else "icon.png"] +icon_path = os.path.join(*_icon_path) +print("Wine test: ",icon_path) a = Analysis( - ['desktop_version.py'], + ['main.py'], pathex=[], binaries=[], datas=[("assets",'assets')], @@ -26,14 +30,14 @@ exe = EXE( a.binaries, a.datas, [], - name='Laner PC', - debug=False, + name='laner-pc', + debug=debugging, bootloader_ignore_signals=False, strip=False, upx=True, upx_exclude=[], runtime_tmpdir=None, - console=True, + console=debugging, disable_windowed_traceback=False, argv_emulation=False, target_arch=None, diff --git a/popups/ConnectionRequest.py b/popups/ConnectionRequest.py new file mode 100644 index 0000000..8b6e2df --- /dev/null +++ b/popups/ConnectionRequest.py @@ -0,0 +1,175 @@ +import asyncio +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QHBoxLayout, QPushButton +from PyQt5.QtCore import Qt,QEventLoop +from PyQt5.QtGui import QFont +from workers.web_socket import WebSocketConnectionHandler + +# Add Don't show again for this device and then i settings add said device with a checkbox to off/on incoming requests +class ConnectionRequest(QWidget): + def __init__(self,handler,message_object={'name':'','request':''},parent=None): + super().__init__(parent) + self.handler: WebSocketConnectionHandler = handler + device_name = message_object['name'] + # request = message_object['request'] use in upcoming in-app terminal + self._made_choice = False + self.setWindowTitle("Incoming Connection Request") + self.setFixedSize(350, 450) + self.setStyleSheet(""" + background-color: #F8FAFC; + """) + + # border-radius: 10px; + layout = QVBoxLayout() + layout.setContentsMargins(20, 20, 20, 20) + layout.setSpacing(15) + + # Title + self.title = QLabel("Incoming Connection Request") + self.title.setFont(QFont("Arial", 14, QFont.Bold)) + self.title.setAlignment(Qt.AlignCenter) + + # Subtitle + self.subtitle = QLabel("A mobile device is trying to connect to your PC") + self.subtitle.setAlignment(Qt.AlignCenter) + self.subtitle.setStyleSheet("color: #64748B; font-size: 12px;") + + # Device Icon + icon_container = QWidget() + icon_layout = QVBoxLayout(icon_container) + self.icon_label = QLabel() + self.icon_label.setFixedSize(80, 80) + self.icon_label.setStyleSheet(""" + background-color: #E0F2FE; + border-radius: 40px; + border: 2px solid #BAE6FD; + """) + icon_layout.addWidget(self.icon_label, alignment=Qt.AlignCenter) + + # Device Name + self.device_label = QLabel(device_name) + self.device_label.setObjectName("device_name") + self.device_label.setFont(QFont("Arial", 12, QFont.Bold)) + self.device_label.setAlignment(Qt.AlignCenter) + self.device_label.setStyleSheet("color: #1E293B; margin-top: 10px;") + + # Connection Message + self.connection_msg = QLabel("Wants to connect to this device") + self.connection_msg.setAlignment(Qt.AlignCenter) + self.connection_msg.setStyleSheet("color: #64748B; font-size: 12px;") + + # Buttons + button_layout = QHBoxLayout() + + self.reject_btn = QPushButton("✗ Reject") + self.reject_btn.setObjectName("reject_btn") + self.reject_btn.setFixedSize(120, 40) + self.reject_btn.setStyleSheet(""" + QPushButton { + color: #DC2626; + border: 2px solid #DC2626; + border-radius: 5px; + padding: 8px; + } + QPushButton:hover { + background-color: #FEE2E2; + } + """) + + self.accept_btn = QPushButton("✔ Accept") + self.accept_btn.setObjectName("accept_btn") + self.accept_btn.setFixedSize(120, 40) + self.accept_btn.setStyleSheet(""" + QPushButton { + background-color: #2563EB; + color: white; + border-radius: 5px; + padding: 8px; + } + QPushButton:hover { + background-color: #1D4ED8; + } + """) + + button_layout.addWidget(self.reject_btn) + button_layout.addWidget(self.accept_btn) + + # Footer + self.footer = QLabel("Only accept connection requests from devices you trust") + self.footer.setAlignment(Qt.AlignCenter) + self.footer.setStyleSheet("color: #94A3B8; font-size: 10px; margin-top: 20px;") + + # Assemble layout + layout.addWidget(self.title) + layout.addWidget(self.subtitle) + layout.addWidget(icon_container) + layout.addWidget(self.device_label) + layout.addWidget(self.connection_msg) + layout.addLayout(button_layout) + layout.addWidget(self.footer) + + self.setLayout(layout) + + # Connect buttons + self.accept_btn.clicked.connect(self.accept_connection) + self.reject_btn.clicked.connect(self.reject_connection) + # Window flags to keep on top + self.setWindowModality(Qt.ApplicationModal) + self.setWindowFlags(Qt.Window | Qt.WindowStaysOnTopHint) + # self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint) + self.show() + + # def show_request(self, device_name, websocket, event_loop): + # def show_request(self, handler: WebSocketConnectionHandler, message_object={'name':'','request':''}): + # """Show the widget with new connection details""" + # self.handler = handler + # self.websocket = websocket + # self.event_loop = event_loop + # device_name = message_object['name'] + # request = message_object['request'] + # self.device_label.setText(device_name) + # self.show() + + # def hide_request(self): + # """Hide the widget""" + # self.hide() + def closeEvent(self, event): + """ + For When User Closes Popup Without Choosing 'Yes' or 'No' + """ + if not self._made_choice: + self._made_choice=True + # `accept_connection` and `reject_connection` method till call close, this will help + self.reject_connection() + self.hide() + event.ignore() + def accept_connection(self): + """Handle accept connection""" + self._made_choice = True + if self.handler: + loop = QEventLoop() + asyncio.set_event_loop(asyncio.new_event_loop()) + asyncio.get_event_loop().run_until_complete(self.handler.accept()) + # asyncio.run_coroutine_threadsafe(self._send_response(True), self.event_loop) + # self.hide_request() + self.close() + + def reject_connection(self): + """Handle reject connection""" + self._made_choice = True + if self.handler: + loop = QEventLoop() + asyncio.set_event_loop(asyncio.new_event_loop()) + asyncio.get_event_loop().run_until_complete(self.handler.reject()) + # asyncio.run_coroutine_threadsafe(self._send_response(False), self.event_loop) + # self.hide_request() + self.close() + + # async def _send_response(self, accepted): + # """Send response to websocket""" + # if self.websocket: + # response = "accept" if accepted else "reject" + # await self.websocket.send(response) + +# Window flags to keep on top +# self.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.Dialog) +# self.setWindowModality(Qt.ApplicationModal) diff --git a/popups/__init__.py b/popups/__init__.py new file mode 100644 index 0000000..f9438a1 --- /dev/null +++ b/popups/__init__.py @@ -0,0 +1 @@ +from .ConnectionRequest import ConnectionRequest \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e60c763..2399119 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,11 @@ -kivy -kivymd pyinstaller opencv-python-headless -PyQt5 \ No newline at end of file +PyQt5 +pillow +netifaces +websockets +cairosvg +icoextract +pefile +pymupdf +python-docx \ No newline at end of file diff --git a/screens/MainScreen.py b/screens/MainScreen.py new file mode 100644 index 0000000..1dc19c0 --- /dev/null +++ b/screens/MainScreen.py @@ -0,0 +1,62 @@ +from PyQt5.QtWidgets import QWidget,QVBoxLayout,QLabel,QPushButton +from PyQt5.QtCore import Qt, QSize +from PyQt5.QtGui import QFont,QIcon +from workers.helper import getAbsPath + +class MainScreen(QWidget): + def __init__(self, parent): + super().__init__() + self.parent = parent + self.main_layout = QVBoxLayout(self) + + # Title Label + self.title_label = QLabel("Laner") + self.title_label.setAlignment(Qt.AlignCenter) + self.title_label.setFont(QFont("Arial", 24, QFont.Bold)) + self.title_label.setStyleSheet("color: rgb(0, 179, 153);") + self.main_layout.addWidget(self.title_label) + + # Subtitle Label + self.subtitle_label = QLabel("Fast and Secure File Sharing over LAN") + self.subtitle_label.setAlignment(Qt.AlignCenter) + self.subtitle_label.setFont(QFont("Arial", 16)) + self.subtitle_label.setStyleSheet("color: gray;") + self.main_layout.addWidget(self.subtitle_label) + + # Hint Message Label + self.hint_message = QLabel("Connect your PC to your HotSpot\nand Press Start to get the code") + self.hint_message.setAlignment(Qt.AlignCenter) + self.hint_message.setFont(QFont("Arial", 20)) + self.main_layout.addWidget(self.hint_message) + + # Start Button + self.start_button = QPushButton("Start Server") + # TODO bind to end when server start works + self.start_button.clicked.connect(self.parent.start_server) + self.start_button.setFixedSize(120, 40) + self.start_button.setStyleSheet("background-color: white; color: rgb(0, 179, 153);") + self.main_layout.addWidget(self.start_button, alignment=Qt.AlignCenter) + + # Status Label + self.status_label = QLabel("Server not running") + self.status_label.setAlignment(Qt.AlignCenter) + self.status_label.setStyleSheet("color: gray;") + self.main_layout.addWidget(self.status_label) + + # Hide IP Button + self.hide_ip_button = QPushButton("Hide Code") + self.hide_ip_button.clicked.connect(self.parent.hide_ip) + self.hide_ip_button.setFixedSize(100, 40) + self.hide_ip_button.setStyleSheet("background-color: white; color: rgb(0, 179, 153);") + self.hide_ip_button.setVisible(False) + self.main_layout.addWidget(self.hide_ip_button, alignment=Qt.AlignRight) + + # Settings Button + self.settings_button = QPushButton() + settings_icon=getAbsPath('assets','imgs','icon.png') + self.settings_button.setIcon(QIcon(settings_icon)) # Add settings Icon + self.settings_button.setIconSize(QSize(24, 24)) + self.settings_button.setFixedSize(40, 40) + # self.settings_button.setStyleSheet("background-color: transparent;") + self.settings_button.clicked.connect(self.parent.open_settings) + self.main_layout.addWidget(self.settings_button, alignment=Qt.AlignRight) diff --git a/screens/SettingsScreen.py b/screens/SettingsScreen.py new file mode 100644 index 0000000..6a4684a --- /dev/null +++ b/screens/SettingsScreen.py @@ -0,0 +1,71 @@ +from PyQt5.QtWidgets import QWidget,QVBoxLayout,QLabel,QPushButton,QHBoxLayout, QFormLayout +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QFont +from workers.helper import getUserPCName + +class SettingsScreen(QWidget): + def __init__(self, parent): + super().__init__() + self.parent = parent + self.setWindowTitle("Settings") + self.setGeometry(150, 150, 400, 300) + + layout = QVBoxLayout() + + # Back Button at the top left corner + back_button_layout = QHBoxLayout() + self.back_button = QPushButton("Back") + self.back_button.clicked.connect(self.go_back) + self.back_button.setFixedSize(70, 30) + self.back_button.setStyleSheet("background-color: white; color: rgb(0, 179, 153);") + back_button_layout.addWidget(self.back_button, alignment=Qt.AlignLeft) + layout.addLayout(back_button_layout) + + title_label = QLabel("Settings") + title_label.setAlignment(Qt.AlignCenter) + title_label.setFont(QFont("Arial", 24, QFont.Bold)) + title_label.setStyleSheet("color: rgb(0, 179, 153);") + layout.addWidget(title_label) + + form_layout = QFormLayout() + form_layout.setSpacing(10) + form_layout.setContentsMargins(20, 10, 20, 10) + + # Create and style port title label + ip_title = QLabel("Server ip:") + ip_title.setFont(QFont("Arial", 18)) + ip_title.setStyleSheet("color: gray;") + ip_title.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) + + # Create and style port value label + # self.port_label = QSpinBox() + # self.port_label.setRange(0,30000) + # self.port_label.setValue(8000) + self.ip_label = QLabel(self.parent.port if self.parent.port else "Server not running") + + self.ip_label.setFont(QFont("Arial", 18)) + self.ip_label.setStyleSheet("color: gray;") + self.ip_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) + + # Create and style User name + + user_title = QLabel("User:") + user_title.setFont(QFont("Arial", 18)) + user_title.setStyleSheet("color: gray;") + user_title.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) + + self.username_label = QLabel(getUserPCName()) + self.username_label.setFont(QFont("Arial", 18)) + self.username_label.setStyleSheet("color: gray;") + self.username_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) + + # Add both to form layout + form_layout.addRow(user_title,self.username_label) + form_layout.addRow(ip_title, self.ip_label) + + layout.addLayout(form_layout) + + self.setLayout(layout) + + def go_back(self): + self.parent.stacked_widget.setCurrentWidget(self.parent.main_screen) diff --git a/screens/__init__.py b/screens/__init__.py new file mode 100644 index 0000000..bbebc2b --- /dev/null +++ b/screens/__init__.py @@ -0,0 +1,2 @@ +from .MainScreen import MainScreen +from .SettingsScreen import SettingsScreen \ No newline at end of file diff --git a/services/foreground.py b/services/foreground.py deleted file mode 100755 index dbf128a..0000000 --- a/services/foreground.py +++ /dev/null @@ -1,124 +0,0 @@ -# from jnius import autoclass - -# # Android classes -# Context = autoclass('android.content.Context') -# NotificationManager = autoclass('android.app.NotificationManager') -# NotificationChannel = autoclass('android.app.NotificationChannel') -# NotificationBuilder = autoclass('android.app.Notification$Builder') -# AndroidString = autoclass('java.lang.String') - -# # Constants -# channel_id = "download_channel" -# channel_name = "Download Notifications" - -# # Initialize NotificationManager -# service = autoclass('org.kivy.android.PythonService').mService -# context = service.getApplication().getApplicationContext() -# notification_manager = context.getSystemService(Context.NOTIFICATION_SERVICE) - -# # Create Notification Channel (required for Android 8.0+) -# if notification_manager.getNotificationChannel(channel_id) is None: -# channel = NotificationChannel( -# channel_id, -# AndroidString(channel_name), -# NotificationManager.IMPORTANCE_LOW, -# ) -# notification_manager.createNotificationChannel(channel) - -# # Create the Notification Builder -# builder = NotificationBuilder(context, channel_id) -# builder.setContentTitle(AndroidString("Downloading...")) -# builder.setSmallIcon(autoclass("android.R$drawable").ic_menu_save) -# builder.setProgress(100, 0, False) # Set initial progress - -# # Display the notification -# notification_id = 1 -# notification_manager.notify(notification_id, builder.build()) - -# # Simulate Download Progress -# import time -# for progress in range(0, 101, 10): # Increment progress in steps of 10% -# time.sleep(1) # Simulate time taken for download -# builder.setProgress(100, progress, False) -# builder.setContentText(AndroidString(f"{progress}% downloaded")) -# notification_manager.notify(notification_id, builder.build()) - -# # Finish Notification -# builder.setContentText(AndroidString("Download complete!")) -# builder.setProgress(0, 0, False) # Remove the progress bar -# notification_manager.notify(notification_id, builder.build()) - - - -# import jnius -# Context = jnius.autoclass('android.content.Context') -# Intent = jnius.autoclass('android.content.Intent') -# PendingIntent = jnius.autoclass('android.app.PendingIntent') -# AndroidString = jnius.autoclass('java.lang.String') -# NotificationBuilder = jnius.autoclass('android.app.Notification$Builder') -# Notification = jnius.autoclass('android.app.Notification') -# PythonActivity = jnius.autoclass('org.kivy.android' + '.PythonActivity') -# service = jnius.autoclass('org.kivy.android.PythonService').mService - -# notification_service = service.getSystemService(Context.NOTIFICATION_SERVICE) -# app_context = service.getApplication().getApplicationContext() - - -# notification_builder = NotificationBuilder(app_context) -# title = AndroidString("EzTunes".encode('utf-8')) -# message = AndroidString("Ready to play music.".encode('utf-8')) -# notification_builder.setContentTitle(title) -# notification_builder.setContentText(message) - -# notification_intent = Intent(app_context, PythonActivity) -# notification_intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_NEW_TASK) -# notification_intent.setAction(Intent.ACTION_MAIN) -# notification_intent.addCategory(Intent.CATEGORY_LAUNCHER) - -# intent = PendingIntent.getActivity(service, 0, notification_intent,PendingIntent.FLAG_IMMUTABLE) -# notification_builder.setContentIntent(intent) -# Drawable = jnius.autoclass("android.R$drawable") -# icon = getattr(Drawable, 'ic_menu_info_details',None) -# notification_builder.setSmallIcon(icon) -# notification_builder.setAutoCancel(True) - -# new_notification = notification_builder.getNotification() -# # try: -# # service.startForeground(1, new_notification, service.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK) -# # except Exception as e: -# # print(e,'----IT has failed!!!') - -# try: -# service.startForeground(1, new_notification,1) -# except Exception as e: -# print(f"Error starting foreground service: {e}") - -# # service.startForeground(1, new_notification) -# # app_class = service.getApplication().getClass() -# # PendingIntent pendingIntent = PendingIntent.getActivity( -# # getApplicationContext(), -# # REQUEST_CODE, intent, -# # /* flags */ PendingIntent.FLAG_IMMUTABLE); -# # print(service,"||", code,"||" notification_intent,'|||', 0,'|||',PendingIntent.FLAG_IMMUTABLE) -# # print(service,"||", 0,"||", notification_intent,'|||','|||',PendingIntent.FLAG_IMMUTABLE) -# #Below sends the notification to the notification bar; nice but not a foreground service. -# # notification_service.notify(0, new_notification) - -# # if notification_builder.VERSION.SDK_INT >= 34: - -# # service.startForeground(1,new_notification) - - - - - -from jnius import autoclass -import time - - -PythonService = autoclass('org.kivy.android.PythonService') -PythonService.mService.setAutoRestartService(True) - -while True: - print('this is my service and its running') - time.sleep(5) diff --git a/template used for-packaging/README.md b/template used for-packaging/README.md new file mode 100644 index 0000000..8e05b06 --- /dev/null +++ b/template used for-packaging/README.md @@ -0,0 +1,28 @@ +# Packing Docs for Devs + + +## For Linux + +### To Package + +Run `pyinstaller main.spec` in Project Main Folder + +Copy `../dist/laner-pc` to `./template used for-packaging/laner-pc/usr/local/bin`, replace `excutable.txt` + +Then in `template used for-packaging` folder Run `dpkg-deb --build laner-pc` + +### Speed Ticket + +`rm -rf dist && rm -rf dist build && pyinstaller main.spec && cp dist/laner-pc "template used for-packaging/laner-pc/usr/local/bin" && cd "template used for-packaging" && dpkg-deb --build laner-pc` + +### To install + +`sudo dpkg -i laner-pc.deb` + +### Finally to run Enter + +`laner-pc` + +### To uninstall properly + +`sudo apt remove laner-pc && sudo rm -rf /etc/xdg/autostart/laner-pc.desktop` diff --git a/template used for-packaging/laner-pc/DEBIAN/control b/template used for-packaging/laner-pc/DEBIAN/control new file mode 100644 index 0000000..40eef2b --- /dev/null +++ b/template used for-packaging/laner-pc/DEBIAN/control @@ -0,0 +1,7 @@ +Package: laner-pc +Version: 1.0 +Architecture: all +Maintainer: Fabian +Description: A Desktop app for Accessing PC files on Phone. +Section: utils +Installed-Size: 115784 diff --git a/template used for-packaging/laner-pc/DEBIAN/postinst b/template used for-packaging/laner-pc/DEBIAN/postinst new file mode 100755 index 0000000..a85325a --- /dev/null +++ b/template used for-packaging/laner-pc/DEBIAN/postinst @@ -0,0 +1,23 @@ +#!/bin/bash + +echo "post install script running.." + +# Copy the .desktop file to the autostart directory +if [ -f "/usr/share/applications/laner-pc.desktop" ]; then + # Coping the .desktop file to the autostart directory + cp /usr/share/applications/laner-pc.desktop "/etc/xdg/autostart/" + + # Edit the .desktop file to hide the app window + sed -i 's|^Exec=.*|Exec=/usr/local/bin/laner-pc --autostart|' /etc/xdg/autostart/laner-pc.desktop + + # Set the correct permissions (for the .desktop file) + chmod 644 /etc/xdg/autostart/laner-pc.desktop + + # Update desktop database + update-desktop-database + + echo "Autostart setup completed for all users." + +else + echo "Desktop file not found, skipping autostart setup" +fi \ No newline at end of file diff --git a/template used for-packaging/laner-pc/usr/share/applications/laner-pc.desktop b/template used for-packaging/laner-pc/usr/share/applications/laner-pc.desktop new file mode 100644 index 0000000..0fdac76 --- /dev/null +++ b/template used for-packaging/laner-pc/usr/share/applications/laner-pc.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Type=Application +Name=Laner PC +Exec=/usr/local/bin/laner-pc +Icon=laner-pc +X-GNOME-Autostart-enabled=true +NoDisplay=false +Hidden=false +Comment=A Desktop app for Accessing PC files on Phone diff --git a/icon.png b/template used for-packaging/laner-pc/usr/share/icons/hicolor/512x512/apps/laner-pc.png similarity index 100% rename from icon.png rename to template used for-packaging/laner-pc/usr/share/icons/hicolor/512x512/apps/laner-pc.png diff --git a/template used for-packaging/testing/from_browser/index.html b/template used for-packaging/testing/from_browser/index.html new file mode 100644 index 0000000..46d0565 --- /dev/null +++ b/template used for-packaging/testing/from_browser/index.html @@ -0,0 +1,20 @@ + + + + + + Document + + + + + \ No newline at end of file diff --git a/template used for-packaging/testing/from_browser/javascript_test.js b/template used for-packaging/testing/from_browser/javascript_test.js new file mode 100644 index 0000000..273ea42 --- /dev/null +++ b/template used for-packaging/testing/from_browser/javascript_test.js @@ -0,0 +1,26 @@ +async function test(ip,folder='assets') { + const server_ip = ip; + const port = 8000; +// const path = `C:\\Users\\hp\\Downloads`; + const path = `C:\\Users\\hp\\Desktop\\Linux\\my_code\\Laner\\${folder}`; + const encodedPath = encodeURIComponent(path); // encode backslashes and special characters + const url = `http://${server_ip}:${port}/api/getpathinfo?path=${encodedPath}`; + + const data = await fetch(url, { method: 'GET' }); + console.log('got_request') + const res = await data.json(); + console.log(res); +} + + +// def _get_path_from_url(self): +// query = urlparse(self.path).query +// params = parse_qs(query) +// path_list = params.get("path") +// return path_list[0] if path_list else None + +// def do_GET(self): +// """Handle GET requests for various API endpoints.""" +// try: + +// # if self.path.startswith("/api/getpathinfo"): diff --git a/todo.todo b/todo.todo index 7486e5b..b6fced1 100644 --- a/todo.todo +++ b/todo.todo @@ -1,3 +1,40 @@ -Use Plyer to query open pictures in other apps -Create Two branchs First for (Linux|Macos|Windows) second for Android -Make Code Cleaner \ No newline at end of file +add popup for confirming new user +[done] on autostart make app not popup on screen and autostart server +[about to]Use Plyer to query open pictures in other apps +[done]Create Two branchs First for (Linux|Macos|Windows) second for Android +Make Code Cleaner + + +------------------------ +In Mobile +1. When user deletes folder on PC and is still in folder they cant go back, + instead of listening and remove user from folder just make it possible to go back + [Important this breaks app, it stops all movement out of folder in said screen] + +2. When uploading file from Laner editor folder_path to save it not sent +Error: +```py +Doing Post +Folder to save upload -----> +File Upload Error: [WinError 3] The system cannot find the path specified: '' +====== File Upload Error LOG ==== +Traceback (most recent call last): + File "C:\Users\hp\Desktop\Linux\my_code\Laner\workers\server.py", line 96, in do_POST + os.makedirs(folder_path, exist_ok=True) + ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "", line 227, in makedirs +FileNotFoundError: [WinError 3] The system cannot find the path specified: '' +``` + +3. Add toggle button for using service or in-app downloads/uploads + + +Add and use ctrl+c to end app in dev +design icon like pdf.png with python + +if __name__ == "__main__": + print("Running as a script") +elif __package__ == "" or __package__ is None: + print("Running as a standalone file (not in a package)") +else: + print(f"Running as part of package: {__package__}") \ No newline at end of file diff --git a/widgets/popup.py b/widgets/popup.py deleted file mode 100644 index 2a534b5..0000000 --- a/widgets/popup.py +++ /dev/null @@ -1,102 +0,0 @@ -from kivy.uix.widget import Widget -from kivymd.uix.button import MDButton, MDButtonText -from kivymd.uix.snackbar import MDSnackbar,MDSnackbarSupportingText,MDSnackbarButtonContainer,MDSnackbarActionButton,MDSnackbarActionButtonText,MDSnackbarCloseButton -from kivymd.uix.dialog import MDDialog, MDDialogHeadlineText, MDDialogButtonContainer, MDDialogSupportingText -from kivy.properties import ( StringProperty, ObjectProperty) -from kivymd.uix.widget import MDWidget -from kivy.metrics import dp,sp - - -class PopupDialog(MDWidget): - h1=StringProperty() - caption=StringProperty() - cancel_txt=StringProperty() - confirm_txt=StringProperty() - failedCallBack=ObjectProperty() - successCallBack=ObjectProperty() - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.md_bg_color=self.theme_cls.backgroundColor - self.dialog = MDDialog( - MDDialogHeadlineText( - text=self.h1, - halign="left", - ), - MDDialogSupportingText( - text=self.caption, - halign="left", - ), - MDDialogButtonContainer( - Widget(), - MDButton( - MDButtonText(text=self.cancel_txt), - style="text", - on_release=lambda x: self.cancel(), - ), - MDButton( - MDButtonText(text=self.confirm_txt), - style="text", - on_release=lambda x: self.ok(), - ), - spacing="8dp", - - ), - ) - self.dialog.open() - self.dialog.update_width() - - def ok(self): - self.successCallBack() - self.close() - def cancel(self): - self.failedCallBack() - self.close() - - def close(self): - self.dialog.dismiss() - - - -class Snackbar(MDWidget): - h1=StringProperty() - # caption=StringProperty() - # cancel_txt=StringProperty() - confirm_txt=StringProperty('') - # failedCallBack=ObjectProperty() - # successCallBack=ObjectProperty() - def __init__(self, **kwargs): - super().__init__(**kwargs) - - self.snackbar = MDSnackbar( - MDSnackbarSupportingText( - text=self.h1 or "Saved", - theme_text_color="Custom", - text_color=(0, 40/255, 0, 1) - ), - MDSnackbarButtonContainer( - MDSnackbarActionButton( - MDSnackbarActionButtonText( - text=self.confirm_txt, - theme_text_color="Custom", - text_color=(0, 40/255, 0, 1) - ), - # theme_bg_color="Custom", - - # md_bg_color= - ), - MDSnackbarCloseButton( - icon="close", - # theme_text_color="Custom", - theme_icon_color="Custom", - icon_color=(0.2, 40/255, 0.3, 1), - on_release=lambda _:self.snackbar.dismiss() - ), - pos_hint={"center_y": 0.5} - ), - y=sp(80), - orientation="horizontal", - pos_hint={"center_x": 0.5}, - size_hint_x=0.9, - background_color=(.85, .95, .88, .9) - ) - self.snackbar.open() diff --git a/widgets/templates.py b/widgets/templates.py deleted file mode 100644 index 4714e51..0000000 --- a/widgets/templates.py +++ /dev/null @@ -1,175 +0,0 @@ -from kivymd.uix.screen import MDScreen -from kivymd.uix.boxlayout import MDBoxLayout -from kivymd.uix.label import MDLabel -from kivy.uix.recycleview import RecycleView -from kivy.metrics import dp,sp -from kivy.properties import ( ListProperty, StringProperty) -from kivy.clock import Clock -# from app_settings import SHOW_HIDDEN_FILES - -import threading -import asyncio -import requests -import os -from pathlib import Path - -from widgets.popup import PopupDialog,Snackbar -from workers.helper import getSERVER_IP, getSystem_IpAdd, makeDownloadFolder, truncateStr,getHiddenFilesDisplay_State - - - -my_downloads_folder=makeDownloadFolder() - - -class Header(MDBoxLayout): - text=StringProperty() - text_halign=StringProperty() - title_color=ListProperty([1,1,1,1]) - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.md_bg_color = (.2, .2, .2, .5) - self.header_label=MDLabel( - text_color=self.title_color, - text='~ '+self.text if self.text == 'Home' else self.text, - halign=self.text_halign, - valign='center', - shorten_from='center', - shorten=True, - - ) - if self.text_halign == 'left': - self.header_label.padding=[sp(40),0,0,0] - else: - self.header_label.padding=[sp(10),0,sp(10),0] - - self.add_widget(self.header_label) - def changeTitle(self,text): - self.header_label.text='~ '+ text if text == 'Home' else text - - - -class RV(RecycleView): - def __init__(self, **kwargs): - super(RV,self).__init__(**kwargs) - - -async def async_download_file(url, save_path): - try: - response = requests.get(url) - file_name = save_path - file_path = os.path.join(my_downloads_folder, file_name) - with open(file_path, "wb") as file: - file.write(response.content) - Clock.schedule_once(lambda dt: Snackbar(confirm_txt='Open',h1=f'Successfully Saved { truncateStr(Path(file_path).parts[-1],10) }')) - except Exception as e: - Clock.schedule_once(lambda dt: Snackbar(confirm_txt='Open',h1="Download Failed try Checking Laner on PC")) - print(e,"Failed Download") - -class DisplayFolderScreen(MDScreen): - current_dir = StringProperty('.') - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.screen_history = [] - self.current_dir_info:list[dict]=[] - self.could_not_open_path_msg="Couldn't Open Folder Check Laner on PC" - self.layout=MDBoxLayout(md_bg_color=[.4,.4,.4,1],orientation='vertical') - self.header=Header( - text=self.current_dir, - size_hint=[1,.1], - text_halign='center', - title_color=self.theme_cls.backgroundColor, - ) - self.layout.add_widget(self.header) - - self.screen_scroll_box = RV() - self.screen_scroll_box.data=self.current_dir_info - Clock.schedule_once(lambda dt: self.startSetPathInfo_Thread()) - - - self.layout.add_widget(self.screen_scroll_box) - self.add_widget(self.layout) - - def startSetPathInfo_Thread(self): - threading.Thread(target=self.querySetPathInfoAsync).start() - - def querySetPathInfoAsync(self): - # Run the async function in the event loop - - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - loop.run_until_complete(self.asyncSetPathInfo()) - loop.close() - async def asyncSetPathInfo(self): - try: - response = requests.get(f"http://{getSERVER_IP()}:8000/api/getpathinfo",json={'path':self.current_dir},timeout=5) - # requests.get(server,data='to be sent',auth=(username,password)) - print(f"Clicked {response}") - if response.status_code != 200: - Clock.schedule_once(lambda dt:Snackbar(h1=self.could_not_open_path_msg)) - return - self.current_dir_info=[] - for each_name in response.json()['data']: - if not getHiddenFilesDisplay_State() and each_name['text'][0] != '.': - self.current_dir_info.append(each_name) - elif getHiddenFilesDisplay_State(): - self.current_dir_info.append(each_name) - - self.screen_scroll_box.data=self.current_dir_info - - except Exception as e: - Clock.schedule_once(lambda dt:Snackbar(h1=self.could_not_open_path_msg)) - print(e,"Failed opening Folder async") - - def on_pre_enter(self, *args): - Clock.schedule_once(lambda dt: self.startSetPathInfo_Thread()) - - - def isDir(self,path:str): - - try: - response=requests.get(f"http://{getSERVER_IP()}:8000/api/isdir",json={'path':path},timeout=3) - if response.status_code != 200: - Clock.schedule_once(lambda dt:Snackbar(h1=self.could_not_open_path_msg)) - return False - return response.json()['data'] - - except Exception as e: - Clock.schedule_once(lambda dt:Snackbar(h1=self.could_not_open_path_msg)) - print(f"isDir method: {e}") - return False - def setPath(self,path,add_to_history=True): - if not self.isDir(path): - return - if add_to_history: # Saving Last directory for screen history - self.screen_history.append(self.current_dir) - - self.current_dir = path - self.header.changeTitle(path) - # self.setPathInfo() - Clock.schedule_once(lambda dt: self.startSetPathInfo_Thread()) - - def showDownloadDialog(self,path:str): - """Shows Dialog box with choosen path and calls async download if ok press""" - if (self.isDir(path)): - return - - file_name = os.path.basename(path.replace('\\', '/')) - def failedCallBack():... - def successCallBack(): - needed_file = f"http://{getSERVER_IP()}:8000/{path}" - url = needed_file.replace(' ', '%20').replace('\\', '/') - - saving_path = os.path.join(my_downloads_folder, file_name) - threading.Thread(target=self.b_c,args=(url, saving_path)).start() - - PopupDialog( - failedCallBack=failedCallBack,successCallBack=successCallBack, - h1="Verify Download", - caption=f"{file_name} -- Will be saved in \"Laner\" Folder in your device \"Downloads\"", - cancel_txt="Cancel",confirm_txt="Ok", - ) - def b_c(self,url, save_path): - loop=asyncio.new_event_loop() - asyncio.set_event_loop(loop) - loop.run_until_complete(async_download_file(url, save_path)) diff --git a/workers/config.py b/workers/config.py new file mode 100644 index 0000000..3ac89ed --- /dev/null +++ b/workers/config.py @@ -0,0 +1,50 @@ +import sys, traceback, os + +if __name__ == 'config': + from helper import getAppFolder +else: + from workers.helper import getAppFolder + +import builtins +import inspect +import os + +def custom_print(*args, **kwargs): + frame = inspect.currentframe().f_back + filename = os.path.basename(frame.f_code.co_filename) + builtins._print(f"[{filename}]", *args, **kwargs) + +# Backup original +builtins._print = builtins.print +builtins.print = custom_print + +# Now all prints will include file name +# print("This will include the filename.") + +def create_preview_folder(): + """Creates a folder for preview images if it doesn't exist.""" + preview_folder = os.path.join(getAppFolder(), 'preview-imgs') + os.makedirs(preview_folder, exist_ok=True) + return preview_folder + +create_preview_folder() + +# Global exception handler to catch all unhandled exceptions +def global_exception_handler(exc_type, exc_value, exc_traceback): + # Default traceback print + traceback.print_exception(exc_type, exc_value, exc_traceback) + # Custom line after every exception + print(f'------------------------{exc_type.__name__} Log end------------------------') + +# Set the global exception handler +sys.excepthook = global_exception_handler + +# Custom exception handler for threading +# This will catch exceptions in threads and print them +import threading + +def thread_exception_handler(args): + traceback.print_exception(args.exc_type, args.exc_value, args.exc_traceback) + print(f'------------------------Thread Exception ({args.exc_type.__name__}) Log end------------------------') + +threading.excepthook = thread_exception_handler diff --git a/workers/helper.py b/workers/helper.py index 7faed05..19d6927 100644 --- a/workers/helper.py +++ b/workers/helper.py @@ -1,28 +1,62 @@ import subprocess +import re +import shutil +import socket +import platform import os import json -import socket import sys import hashlib -# import subprocess +import urllib.parse +from os.path import join as _joinPath -# /home/fabian/Documents/my projects code/mobile dev/Laner/venv/lib64/python3.12/site-packages/kivymd/uix/menu/menu.py -# Run this from VSCode content menu (Run Python) --> /home/fabian/Documents/my projects code/mobile dev/Laner/venv/lib64/python3.12/site-packages/kivymd/icon_definitions.py def getSystemName(): # Windows USER_HOME_PATH=os.getenv('HOMEPATH') os_name='Win' - if(USER_HOME_PATH == None): + if USER_HOME_PATH is None: USER_HOME_PATH=os.getenv('HOME') os_name='Linux' return os_name def getHomePath(): - # Windows - USER_HOME_PATH=os.getenv('HOMEPATH') # Can also be editable to downloads path or something else - if(USER_HOME_PATH == None): - USER_HOME_PATH=os.getenv('HOME') - return USER_HOME_PATH + """ + Get user's home directory path across different platforms. + Returns: str - Path to user's home directory + """ + # Try platform-independent method first + home = os.path.expanduser('~') + if home and os.path.exists(home): + return home + + # Fallback to environment variables + for env_var in ['HOME', 'USERPROFILE', 'HOMEPATH']: + path = os.getenv(env_var) + if path and os.path.exists(path): + return path + + # Last resort - current directory + return os.getcwd() + +def getdesktopFolder(): + """ + Get path to user's desktop folder across different platforms. + Returns: str - Path to desktop directory + """ + # First try standard desktop location + home = getHomePath() + desktop = os.path.join(home, 'Desktop') + if os.path.exists(desktop): + return desktop + + # Try Windows-specific location + desktop = os.path.join(home, 'OneDrive', 'Desktop') + if os.path.exists(desktop): + return desktop + + # Fallback to home directory + return home + def findClosestParent(path:str):... @@ -49,47 +83,7 @@ def writeIntoDB(data): dictionary_words = json.dumps(Dict_Structure, indent=4) with open("public/data.json", mode="w") as new_word: new_word.write(dictionary_words) -import subprocess -import platform -import re -import shutil -def getSystem_IpAdd(): - def tryOtherFormat(standard_output): - ip_pattern = re.compile(r'IPv4 address.*?:\s*([\d.]+)') - return ip_pattern.findall(standard_output) - os_name = platform.system() - if os_name == 'Linux' or os_name == 'Darwin': # Linux or macOS - if shutil.which('ifconfig'): - command = ['ifconfig'] - ip_pattern = re.compile(r'inet\s([\d.]+)') - elif shutil.which('ip'): # Fallback to 'ip addr' if 'ifconfig' is missing - command = ['ip', 'addr'] - ip_pattern = re.compile(r'inet\s([\d.]+)') - else: - raise FileNotFoundError("Neither 'ifconfig' nor 'ip' command found on the system") - elif os_name == 'Windows': - command = ['ipconfig'] - ip_pattern = re.compile(r'IPv4 Address.*?:\s*([\d.]+)') - else: - raise OSError("Unsupported operating system") - - # Run the command and capture output - result = subprocess.run(command, capture_output=True, text=True) - # print('peek',result.stdout) - # Extract IP addresses - ip_addresses = ip_pattern.findall(result.stdout) - ip_addresses = tryOtherFormat(result.stdout) if os_name == 'Windows' and len(ip_addresses) == 0 else ip_addresses - # Exclude loopback addresses like 127.0.0.1 - ip_addresses = [ip for ip in ip_addresses if not ip.startswith('127.')] - - return ip_addresses[0] if len(ip_addresses) else None - -# Print the results -try: - print("Extracted IP addresses:", getSystem_IpAdd()) -except Exception as e: - print(f"Error: {e}") @@ -103,7 +97,10 @@ def getAppFolder(): path__ = os.path.abspath(sys._MEIPASS) else: # Running from source code - path__ = os.path.abspath(os.path.dirname(__file__)) + # function_folder_formatted_to_get_app_folder = os.path.join(os.path.dirname(__file__),'..') + # path__=os.path.abspath(function_folder_formatted_to_get_app_folder) + venv_root = os.path.abspath(os.path.join(sys.executable, '..', '..','..')) # Go up one level from sys.executable + path__=venv_root # Normalize path for Wine compatibility if is_wine(): @@ -184,26 +181,13 @@ def gen_unique_filname(file_path:str): return unique_name def removeFirstDot(path:str): + """Removes first dot safely""" if path[0] == '.': return path[1:] else: return path -def makeDownloadFolder(): - """Makes downlod folder and returns path - """ - from kivy.utils import platform - - folder_path = os.getcwd() - if platform == 'android': - from android.storage import app_storage_path, primary_external_storage_path # type: ignore - folder_path=os.path.join(primary_external_storage_path(),'Download','Laner') - makeFolder(folder_path) - return folder_path - - - def truncateStr(text:str,limit=20): if len(text) > limit: return text[0:limit] + '...' @@ -217,9 +201,66 @@ def setHiddenFilesDisplay(state): def getHiddenFilesDisplay_State(): return SHOW_HIDDEN_FILES -SERVER_IP=getSystem_IpAdd() -def setSERVER_IP(value): - global SERVER_IP - SERVER_IP=value -def getSERVER_IP(): - return SERVER_IP \ No newline at end of file + +def getUserPCName(): + """ + Get the current user's PC name. + Returns: str - PC name + """ + pc_name=None + try: + # Try socket hostname first + pc_name = socket.gethostname() + + # Clean and validate hostname + if pc_name and isinstance(pc_name, str): + # Remove special characters and extra spaces + cleaned_name = ' '.join(pc_name.split()) + # Limit length and capitalize + pc_name= cleaned_name[:30] + # Fallback methods if socket failed + + except Exception as e: + print(f"Error in getUserPCName: {e}") + def fallbackPCName(): + """Helper function to get PC name using environment variables""" + pc_name= 'Unknown-PC' + try: + # Try different environment variables + for env_var in ['COMPUTERNAME', 'HOSTNAME', 'HOST', 'USER']: + name = os.environ.get(env_var) + if name: + pc_name= name.strip()[:30] + break + + except Exception as e: + print(f"Error in fallbackPCName: {e}") + pc_name= 'Unknown-PC' + return pc_name + + return pc_name or fallbackPCName() + +def urlSafePath(path:str): + # TODO combine urlSafePath and removeFirstDot + path_without_drive=os.path.splitdrive(path)[1] + # Normalizing Windows Path Forward Slashes for Url '\\' ---> '/' + normalized_path= path_without_drive.replace('\\','/') + # For URL encoding + url_safe_path=urllib.parse.quote(normalized_path) + return url_safe_path + +def inHomePath(request_path,folder): + print(os.path.join(getHomePath(),folder), "==", os.path.join(request_path,folder)) + return os.path.join(getHomePath(),folder) == os.path.join(request_path,folder) + +def getAbsPath(*path_list): + """Get file real path + + Args: + path_list (list | str): path or list of paths + + Returns: + str: Actual Path + """ + return _joinPath(getAppFolder(),*path_list) + diff --git a/workers/lab/getting_excetuable_icon.py b/workers/lab/getting_excetuable_icon.py new file mode 100644 index 0000000..bad1a1d --- /dev/null +++ b/workers/lab/getting_excetuable_icon.py @@ -0,0 +1,104 @@ +# from icoextract import IconExtractor +# from PIL import Image + +# def extract_exe_icon(exe_path, output_path=None): +# """ +# Extract icon from EXE file +# :param exe_path: Path to the EXE file +# :param output_path: Path to save the icon (optional) +# :return: PIL Image object if output_path is None, otherwise None +# """ +# extractor = IconExtractor(exe_path) +# ico_path = exe_path + '.ico' +# extractor.export_icon(ico_path) + +# with Image.open(ico_path) as img: +# if output_path: +# img.save(output_path) +# return None +# return img + +# Example usage +# extract_exe_icon('laner.exe', 'icon.png') + +#-------------- worked but tiny icon ---------------- +# from PIL import Image +# import subprocess +# import os + +# def get_exe_icon_pil(exe_path, output_path): +# try: +# # Use PowerShell to extract icon on Windows +# ps_command = f''' +# Add-Type -AssemblyName System.Drawing +# $icon = [System.Drawing.Icon]::ExtractAssociatedIcon("{exe_path}") +# $icon.ToBitmap().Save("{output_path}") +# ''' + +# subprocess.run(["powershell", "-Command", ps_command], +# capture_output=True, text=True) + +# # Load and return the image +# return Image.open(output_path) + +# except Exception as e: +# print(f"Error: {e}") +# return None + +# get_exe_icon_pil = get_exe_icon_pil('laner.exe', 'icon.png') + + +#--------------------------- also worked but tiny icon ---------------- +# import subprocess +# import sys +# import os + +# def get_file_icon(file_path, output_path): +# if sys.platform == "win32": +# # Windows: Use PowerShell +# cmd = f''' +# Add-Type -AssemblyName System.Drawing +# [System.Drawing.Icon]::ExtractAssociatedIcon("{file_path}").ToBitmap().Save("{output_path}") +# ''' +# subprocess.run(["powershell", "-Command", cmd]) + +# elif sys.platform == "darwin": +# # macOS: Use sips command +# subprocess.run(["sips", "-s", "format", "png", file_path, "--out", output_path]) + +# else: +# # Linux: More complex, might need custom solution +# print("Linux icon extraction requires additional tools") + +# get_file_icon('laner.exe', 'icon.png') + + +#--------------------------- also worked but tiny icon ---------------- + +# from PIL import Image +# import subprocess +# import os + +# def get_exe_icon_pil(exe_path, output_path): +# try: +# # Use PowerShell to extract icon on Windows +# ps_command = f''' +# Add-Type -AssemblyName System.Drawing +# $icon = [System.Drawing.Icon]::ExtractAssociatedIcon("{exe_path}") +# $icon.ToBitmap().Save("{output_path}") +# ''' + +# subprocess.run(["powershell", "-Command", ps_command], +# capture_output=True, text=True) + +# # Load and return the image +# return Image.open(output_path) + +# except Exception as e: +# print(f"Error: {e}") +# return None +# get_exe_icon_pil('laner.exe', 'icon.png') + +#------------------------------------ threading default exception handler ---------------- + + diff --git a/workers/lab/working_pdf_reader_app.py b/workers/lab/working_pdf_reader_app.py new file mode 100644 index 0000000..f36db66 --- /dev/null +++ b/workers/lab/working_pdf_reader_app.py @@ -0,0 +1,106 @@ +import fitz # PyMuPDF +from PIL import Image, ImageTk +import io +import tkinter as tk +from tkinter import ttk + +class PDFViewer: + def __init__(self, root, pdf_path): + self.root = root + self.root.title("PDF Viewer") + + # Set fixed window size (you can adjust these values) + self.window_width = 1000 + self.window_height = 600 + self.root.geometry(f"{self.window_width}x{self.window_height}") + self.root.minsize(800, 600) # Minimum size the user can resize to + + self.pdf_path = pdf_path + self.doc = fitz.open(pdf_path) + self.current_page = 0 + self.total_pages = len(self.doc) + + # Create main container with scrollbars + self.main_frame = ttk.Frame(root) + self.main_frame.pack(fill=tk.BOTH, expand=True) + + # Add scrollbars + self.hscroll = ttk.Scrollbar(self.main_frame, orient=tk.HORIZONTAL) + self.vscroll = ttk.Scrollbar(self.main_frame, orient=tk.VERTICAL) + + # Create canvas with scrollbars + self.canvas = tk.Canvas( + self.main_frame, + bg="white", + xscrollcommand=self.hscroll.set, + yscrollcommand=self.vscroll.set + ) + + self.hscroll.config(command=self.canvas.xview) + self.vscroll.config(command=self.canvas.yview) + + # Grid layout for proper resizing + self.canvas.grid(row=0, column=0, sticky="nsew") + self.vscroll.grid(row=0, column=1, sticky="ns") + self.hscroll.grid(row=1, column=0, sticky="ew") + + self.main_frame.grid_rowconfigure(0, weight=1) + self.main_frame.grid_columnconfigure(0, weight=1) + + # Navigation controls (separate frame at bottom) + self.controls = ttk.Frame(root) + self.controls.pack(fill=tk.X, padx=5, pady=5) + + self.prev_btn = ttk.Button(self.controls, text="Previous", command=self.prev_page) + self.prev_btn.pack(side=tk.LEFT, padx=5) + + self.next_btn = ttk.Button(self.controls, text="Next", command=self.next_page) + self.next_btn.pack(side=tk.LEFT, padx=5) + + self.page_label = ttk.Label(self.controls, text=f"Page {self.current_page + 1} of {self.total_pages}") + self.page_label.pack(side=tk.LEFT, padx=5) + + # Display first page + self.display_page() + + def display_page(self): + # Get the page + page = self.doc.load_page(self.current_page) + + # Calculate zoom to fit the fixed window size + zoom_width = (self.window_width - 40) / page.rect.width + zoom_height = (self.window_height - 100) / page.rect.height + zoom = min(zoom_width, zoom_height) + + mat = fitz.Matrix(zoom, zoom) + pix = page.get_pixmap(matrix=mat) + + # Convert to ImageTk + img = Image.open(io.BytesIO(pix.tobytes())) + self.tk_img = ImageTk.PhotoImage(img) + + # Update canvas + self.canvas.delete("all") + self.canvas.config(scrollregion=(0, 0, pix.width, pix.height)) + self.canvas.create_image(0, 0, anchor=tk.NW, image=self.tk_img) + + # Center the image in the canvas + self.canvas.xview_moveto(0.5 - (self.window_width/pix.width)/2) + self.canvas.yview_moveto(0.5 - (self.window_height/pix.height)/2) + + self.page_label.config(text=f"Page {self.current_page + 1} of {self.total_pages}") + + def next_page(self): + if self.current_page < self.total_pages - 1: + self.current_page += 1 + self.display_page() + + def prev_page(self): + if self.current_page > 0: + self.current_page -= 1 + self.display_page() + +if __name__ == "__main__": + root = tk.Tk() + app = PDFViewer(root, "test.pdf") + root.mainloop() \ No newline at end of file diff --git a/workers/server.py b/workers/server.py index f7fd172..5ce664f 100644 --- a/workers/server.py +++ b/workers/server.py @@ -1,203 +1,405 @@ +import urllib.parse + +if __name__=='__main__': + import config + + +import sys,os,tempfile +import json,threading,traceback +try: + from websockets import ServerConnection # for type + import websockets +except ImportError: + print("-- run pip install websockets") +except Exception as e: + print("Exeception accorded during import of websockets") +import asyncio +from os.path import join as _joinPath from http.server import SimpleHTTPRequestHandler, HTTPServer -import os -import json -import threading +from socketserver import ThreadingMixIn +from urllib.parse import urlparse, parse_qs -from workers.helper import gen_unique_filname, getAppFolder, getFileExtension, getHomePath,getSystem_IpAdd, makeFolder, removeFileExtension, removeFirstDot, sortedDir -from workers.thumbmailGen import generateThumbnails +# Worker imports +if __name__=='__main__': + # For Tests + from helper import ( + getAppFolder, getHomePath, getdesktopFolder, + makeFolder, sortedDir, getUserPCName + ) + from thumbnails import get_icon_for_file + try: + from thumbnails.video import VideoThumbnailExtractor + except Exception as e: + print("Exception while importing VideoThumbnailExtractor",e) + VideoThumbnailExtractor=False + from sword import NetworkManager, NetworkConfig + try: + from web_socket import WebSocketConnectionHandler + except Exception as e: + pass + import config +else: + from workers.helper import ( + getAppFolder, getHomePath, getdesktopFolder, + makeFolder, sortedDir, getUserPCName + ) + from workers.thumbnails import get_icon_for_file,VideoThumbnailExtractor + from workers.sword import NetworkManager, NetworkConfig + try: + from workers.web_socket import WebSocketConnectionHandler + except Exception as e: + pass +SERVER_IP = None -my_owned_icons=['.py','.js','.css','.html','.json','.deb','.md','.sql','.md','.java'] -zip_formats=['.zip','.7z','.tar','.bzip2','.gzip','.xz','.lz4','.zstd','.bz2','.gz'] -video_formats=('.mkv','.mp4', '.avi', '.mkv', '.mov') -picture_formats=('.png','.jpg','.jpeg','.tif','.bmp','.gif') -special_folders=['home','pictures','templates','videos','documents','music','favorites','share','downloads'] - +# Utility Functions +def writeErrorLog(title, value): + """Logs errors to a file.""" + error_log_path = os.path.join(getAppFolder(), 'errors.txt') + with open(error_log_path, 'a') as log_file: + log_file.write(f'====== {title} LOG ====\n{value}\n\n') + print(f'====== {title} LOG ====\n{value}\n\n') -SERVER_IP = getSystem_IpAdd() -no = 1 -generated_thumbnails=[] -def inHomePath(request_path,folder): - print(os.path.join(getHomePath(),folder), "==", os.path.join(request_path,folder)) - return os.path.join(getHomePath(),folder) == os.path.join(request_path,folder) - -from socketserver import ThreadingMixIn + +# Handle stderr when compiled to a single file +if sys.stderr is None: + sys.stderr = open(os.path.join(getAppFolder(), 'errors.log'), 'at') + + +# Threaded HTTP Server class ThreadingHTTPServer(ThreadingMixIn, HTTPServer): - pass + """Threaded HTTP Server for handling multiple requests simultaneously.""" +def do_lag(): + print('Doing Lag.......') + import time + time.sleep(90*60) +# Custom HTTP Handler class CustomHandler(SimpleHTTPRequestHandler): - def do_GET(self): - global no, generated_thumbnails - # print(no) - if self.path == "/api/getpathinfo": - request_path=self.getRequestBody('path') - if (request_path == None): - return - elif (request_path == 'Home'): - request_path = getHomePath() - print('dev home path',request_path) + video_paths = [] + request_count = 0 + + def _get_path_from_url(self): + query = urlparse(self.path).query + params = parse_qs(query) + path_list = params.get("path") + return path_list[0] if path_list else None + + def do_POST(self): + """Handle POST requests for file uploads.""" + print("Doing Post") + if self.path == "/api/upload": try: - path_list:list[str] =os.listdir(request_path) - dir_info=[] - videos_paths=[] + # Get content length and read raw data + content_length = int(self.headers.get('Content-Length', 0)) - for each in path_list: - thumbnail_url='' - thumbnail_path='' - each_path=os.path.join(request_path,each) - is_dir=os.path.isdir(each_path) - img_source=None - format_=getFileExtension(each).lower() - - if is_dir: - if each.lower() in special_folders: - img_source=f"assets/icons/folders/{each.lower()}.png" - else: - img_source="assets/icons/folders/folder.png" - - elif each.lower().endswith(picture_formats): - img_url=removeFirstDot(each_path).replace(' ','%20').replace('\\','/') - img_source=f"http://{SERVER_IP}:8000{img_url}" - - elif format_ in my_owned_icons: - img_source=f"assets/icons/{format_[1:]}.png" - - elif format_ in zip_formats: - img_source=f"assets/icons/packed.png" - - elif each.lower().endswith(video_formats): - image_path = gen_unique_filname(each_path) - thumbnail_path = os.path.join(getAppFolder(),'thumbnails',image_path+'_thumbnail.jpg') - - formatted_path_4_url=removeFirstDot(thumbnail_path).replace('\\','/') - thumbnail_url=f"http://{SERVER_IP}:8000{formatted_path_4_url}" - - img_source="assets/icons/video.png" - if each_path not in generated_thumbnails: - generated_thumbnails.append(each_path) - videos_paths.append(each_path) - - else: - img_source="assets/icons/file.png" - - cur_obj={ - 'text':each, - 'path':each_path, - 'is_dir':is_dir,'icon':img_source, - 'thumbnail_url':thumbnail_url, - 'thumbnail_path':thumbnail_path, - 'validated_path':False, - - } - dir_info.append(cur_obj) + # Extract boundary from Content-Type + content_type = self.headers.get('Content-Type') + if not content_type or 'boundary=' not in content_type: + raise ValueError("Content-Type header is missing or invalid.") + boundary = content_type.split('=')[1].encode() + if not boundary: + self._send_json_response({'error': "Bad Request: Missing boundary 101"}, status=400) + return + data = self.rfile.read(content_length) + parts = data.split(b'--' + boundary) - dir_info=sortedDir(dir_info) - response_data={'data':dir_info} - - if len(videos_paths): - th=threading.Thread(target=generateThumbnails,args=(videos_paths, os.path.join(getAppFolder(),'thumbnails'),1,10)) - th.daemon = True - th.start() - self.wfile.write(json.dumps(response_data).encode("utf-8")) - print('Handled -----',no) - - + folder_path = getdesktopFolder() + found_folder=False + save_path = None + # print("Whole data: ",parts,'\n') + for part in parts: + # print("This is a part: ",part) + if b'name="save_path"' in part and not found_folder: + found_folder=True + folder_path = ( + part.split(b'\r\n')[3] # More precise splitting + .decode() + .strip() + ) + print("Folder to save upload -----> ", folder_path,'<-----') + os.makedirs(folder_path, exist_ok=True) + + if b'filename=' in part: + # Extract filename + headers, file_content = part.split(b'\r\n\r\n', 1) + filename = ( + headers.split(b'filename="')[1] + .split(b'"')[0] + .decode() + ) + print("Uploaded File name: -----> ", filename,'<-----') + + save_path = os.path.join(folder_path, filename) + + # Remove any trailing boundary markers + file_content = file_content.rstrip(b'\r\n--') + # Write file safely + with open(save_path, 'wb') as f: + f.write(file_content) + if save_path: + print("File Upload Successful:", save_path) + self._send_json_response({'message': 'File uploaded successfully'}) + else: + self._send_json_response({'error': "No file content found in the uploaded data."}, status=400) + # raise ValueError("No file content found in the uploaded data.") + except Exception as e: - self.send_response(400) - self.send_header("Content-type", "application/json") - self.end_headers() - self.wfile.write(json.dumps({'error':type(e).__name__}).encode("utf-8")) - - elif self.path == "/api/isdir": - request_path=self.getRequestBody('path') - if (request_path != None): - if (request_path == 'Home'): + print("File Upload Error:", e) + writeErrorLog('File Upload Error', traceback.format_exc()) + self._send_json_response({'error': str(e)}, status=400) + + def do_GET(self): + """Handle GET requests for various API endpoints.""" + try: + if self.path == "/api/getpathinfo": + request_path = self._get_request_body('path') + if request_path == 'Home': request_path = getHomePath() - self.wfile.write(json.dumps({'data':os.path.isdir(request_path)}).encode("utf-8")) - - elif self.path == "/api/isfile": - request_path=self.getRequestBody('path') - if (request_path != None): - self.wfile.write(json.dumps({'data':os.path.isfile(request_path)}).encode("utf-8")) - - - elif self.path == "/ping": - res=self.getRequestBody('passcode') - print(res) - if (res == None): - return - else: - super().do_GET() - no+=1 - def getRequestBody(self,request_key): - content_length = int(self.headers['Content-Length']) # This will be None when no path requested (i.e no json= in request) + elif request_path is None: # Checking for None incase i send '' as path in future and not './' + self._send_json_response({'error': "Server didn't receive any data, i.e no requested_path"}, status=400) + return + dir_info = [] + self.video_paths = [] + + # for each in os.listdir(request_path): + # each_path = os.path.join(request_path, each) + # is_dir=os.path.isdir(each_path) + # img_source, thumbnail_url = get_icon_for_file(each_path,name=each,video_paths=self.video_paths,is_dir=is_dir) + # cur_obj = { + # 'text': each, + # 'path': each_path, + # 'is_dir': is_dir, + # 'icon': img_source, + # 'thumbnail_url': thumbnail_url, + # 'validated_path': False + # } + # dir_info.append(cur_obj) + + # Using scandir() instead of listdir() + isdir() + with os.scandir(request_path) as entries: + for entry in entries: + try: + is_dir=entry.is_dir() + name=entry.name + img_source, thumbnail_url = get_icon_for_file(entry.path,name=name,video_paths=self.video_paths,is_dir=is_dir) + + cur_obj = { + 'text': name, + 'path': entry.path, + 'is_dir': is_dir, # Much faster than os.path.isdir() which slowed doen server + 'validated_path': False, # I Can't remember why i added this deepseek changed it to True Since we're reading directly from filesystem + 'icon': img_source, + 'thumbnail_url': thumbnail_url, + } + dir_info.append(cur_obj) + + except OSError as e: + print(f"Error processing {entry.path}: {e}") + continue + + # dir_info = sortedDir(dir_info) + # print('dta ',dir_info) + self._send_json_response({'data': dir_info}) + print('responding back....') + if VideoThumbnailExtractor and self.video_paths: + VideoThumbnailExtractor(self.video_paths, 1, 10).extract() + + elif self.path == "/api/isdir": + self._send_json_response({'data': os.path.isdir(self.parseMyPath())}) + elif self.path == "/api/isfile": + request_path = self._get_request_body('path') + is_file=os.path.isfile(request_path) or os.path.isfile('/'+urllib.parse.unquote(request_path)) + if not is_file: + file_abspath=os.path.abspath(request_path) + drive=os.path.splitdrive(file_abspath)[0] + real_file_path= _joinPath(drive,request_path) + is_file=os.path.isfile(real_file_path) + print("Check of existence: ",real_file_path) + + self._send_json_response({'data': is_file}) + + elif self.path == "/ping": + NetworkManager().keep_broadcasting=False + self._send_json_response({'data': getUserPCName()}) + else: + super().do_GET() + # self._send_json_response({'error': "Endpoint not found."}, status=404) + except PermissionError as e: + writeErrorLog('Permission Error', traceback.format_exc()) + self._send_json_response({'error': "Permission denied. Please check your access rights."}, status=403) + except Exception as e: + writeErrorLog('Error Handling for All Requests', traceback.format_exc()) + self._send_json_response({'error': str(e)}, status=400) + + self.request_count += 1 + + def parseMyPath(self): + """ Takes unreal_path from app and format to real path eg Home --> ~ Home :) TODO Remove this""" + app_requested_path=self._get_request_body('path') + return getHomePath() if app_requested_path == 'Home' else app_requested_path + + def _get_request_body(self, key): + """Parses JSON from the request body.""" + extracted_length = self.headers['Content-Length'] + if extracted_length is None: # explicty checking None incase maybe it can be 0,test more and remove line + return extracted_length + content_length = int(extracted_length) request_data = self.rfile.read(content_length) + # print('request_data',request_data) + # print('Why',json.loads(request_data).get(key)) + return json.loads(request_data).get(key) + + def _set_cors_headers(self): + self.send_header('Access-Control-Allow-Origin', '*') + self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') + self.send_header('Access-Control-Allow-Headers', 'Content-Type') + + def _send_json_response(self, data, status=200): + """Sends a JSON response.""" + self.send_response(status) + # self._set_cors_headers() # <--- For testing from javascript|brower (Don't package with this line, maybe package because of offline website docs) + self.send_header('Content-Type', 'application/json') + self.end_headers() try: - request_path=json.loads(request_data)[request_key] - self.send_response(200) - self.send_header("Content-type", "application/json") - self.end_headers() - return request_path - except Exception as e: - self.send_response(400) - self.send_header("Content-type", "application/json") - self.end_headers() - self.wfile.write(json.dumps({'error': type(e).__name__ }).encode("utf-8")) + self.wfile.write(json.dumps(data).encode('utf-8')) + except ConnectionAbortedError as connection_aborted_error: + writeErrorLog(f'Connection was aborted: {connection_aborted_error}', traceback.format_exc()) + except BrokenPipeError: + writeErrorLog('Client closed connection early\n hint probably timer out on client side:', traceback.format_exc()) + + +# Server Class class FileSharingServer: - def __init__(self, port=8000, directory="/"): + def __init__(self, ip, connection_signal=None, port=8000, directory="/"): + self.ip = ip self.port = port + self._server = None self.directory = directory - self.server = None - self.server_thread = None - makeFolder(os.path.join(getAppFolder(),'thumbnails')) + makeFolder(os.path.join(getAppFolder(), 'thumbnails')) + if not connection_signal: + print("No 'connection_signal' Running without UI") + self.connection_signal = connection_signal + self.loop = None + self.websocket_port = None + self.websocket_server = None + self.ws_thread=None + + async def websocket_handler(self, websocket): + """Handle new WebSocket connections""" + handler = WebSocketConnectionHandler( + websocket=websocket, + connection_signal=self.connection_signal, + ip=self.ip, + main_server_port=self.port + ) + await handler.handle_connection() # Make sure to await the connection handler + + # Modify the WebSocket server setup in the FileSharingServer class + async def start_websocket_server(self): + # Create a wrapper function to properly bind the instance method + async def handler(websocket): + await self.websocket_handler(websocket) + + self.websocket_port=self.port + 1 + self.websocket_server = await websockets.serve( + handler, + self.ip, + self.websocket_port + ) + print(f"WebSocket server started at ws://{self.ip}:{self.port+1}") def start(self): global SERVER_IP - SERVER_IP = getSystem_IpAdd() - + NetworkConfig.server_ip = SERVER_IP = self.ip or SERVER_IP + + # print(SERVER_IP,self.ip) os.chdir(self.directory) - # Create the HTTP server - self.server = ThreadingHTTPServer(("", self.port), CustomHandler) - # self.server.serve_forever() - - # Start the server in a separate thread - self.server_thread = threading.Thread(target=self.server.serve_forever) - self.server_thread.daemon = True - self.server_thread.start() + # self.server = ThreadingHTTPServer((self.ip, self.port), CustomHandler) + # threading.Thread(target=self.server.serve_forever, daemon=True).start() + # print(f"Server started at http://{SERVER_IP}:{self.port}") + self._server =None + + ports = [ + 8000, 8080, 9090, 10000, 11000, 12000, 13000, 14000, + 15000, 16000, 17000, 18000, 19000, + 20000, 22000, 23000, 24000, 26000, + 27000, 28000, 29000, 30000 + ] + # TODO ping Server from PC and Check for Unquie Generated Code from maybe Singleton + for port in ports: + print('Trying ',self.ip,port) + try: + # self._server = ThreadingHTTPServer(("", port), CustomHandler) + # self._server = ThreadingHTTPServer(("0.0.0.0", port), CustomHandler) + self._server = ThreadingHTTPServer((self.ip, port), CustomHandler) + self.port=port + threading.Thread(target=self._server.serve_forever, daemon=True).start() + break # Exit the loop if the server starts successfully + except OSError as e: + print(f"Port {port} is unavailable, trying the next one...") + writeErrorLog(f'{e} -- Port :{port}',traceback.format_exc()) + traceback.print_exc() + except Exception as e: + print(f"Error: {e}") + writeErrorLog(f'{e} -- Port :{port}',traceback.format_exc()) + # Start WebSocket server in event loop + if __name__ != '__main__': + self.ws_thread = threading.Thread(target=self.run_websocket_server) + self.ws_thread.daemon = True + self.ws_thread.start() + else: + print("Running server.file without GUI, Didn't start the websocket server, To Maybe save Battery") print(f"Server started at http://{SERVER_IP}:{self.port}") - print(f"API endpoint available at http://{SERVER_IP}:{self.port}/api/getpathinfo") def stop(self): - if self.server: - self.server.shutdown() - self.server.server_close() - self.server_thread.join() - print("Server stopped.") - -# if __name__ == "__main__": -# # Specify the port and directory -# port = 8000 -# directory = "/" - -# # Initialize the server -# server = FileSharingServer(port, directory) - -# try: -# # Start the server -# server.start() - -# # Keep the program running until interrupted -# print("Press Ctrl+C to stop the server.") -# while True: -# pass -# except KeyboardInterrupt: -# # Stop the server on Ctrl+C -# print("\nStopping the server...") -# server.stop() -# /home/fabian/Documents/my-projects-code/mobile-dev/Laner/thumbnails/2.%20Reading%20from%20Your%20Database%20with%20Mongoose_thumbnail.jpg -# /home/fabian/Documents/my-projects-code/mobile-dev/Laner/thumbnails/2.%20Reading%20from%20Your%20Database%20with%20Mongoose_thumbnail.jpg \ No newline at end of file + # Stop HTTP server + if self._server: + self._server.shutdown() + self._server.server_close() + + # Stop WebSocket server + if self.loop: + # Schedule the server closure and loop stop + async def close_server_and_stop_loop(): + if self.websocket_server: + self.websocket_server.close() + await self.websocket_server.wait_closed() + print("WebSocket server stopped") + self.loop.stop() # Stop the loop only after server is closed + + # Schedule the entire shutdown sequence + self.loop.call_soon_threadsafe( lambda: asyncio.create_task(close_server_and_stop_loop()) ) + print("Server stopped.") + def run_websocket_server(self): + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.loop) + self.loop.run_until_complete(self.start_websocket_server()) + self.loop.run_forever() + +# print(__name__,'==','__main__') +if __name__ == "__main__": + # Specify the port and directory + port = 8000 + directory = "/" + + # Initialize the server + server = FileSharingServer(port=port,directory=directory,ip=NetworkManager().get_server_ip()) + + try: + # Start the server + server.start() + + # Keep the program running until interrupted + print("Press Ctrl+C to stop the server.") + while True: + pass + except KeyboardInterrupt: + # Stop the server on Ctrl+C + print("\nStopping the server...") + server.stop() diff --git a/workers/sword.py b/workers/sword.py new file mode 100644 index 0000000..4765b88 --- /dev/null +++ b/workers/sword.py @@ -0,0 +1,204 @@ +import json +import traceback +from typing import Optional,List +import platform +import subprocess +import re +import shutil +from dataclasses import dataclass +import socket +import time + +try: + import netifaces +except ImportError: + print('-- run pip install netifaces') +except Exception as e: + print("Exeception accorded during import of netifaces") + +if __name__=='sword' or __name__=='__main__': + from helper import getUserPCName +else: + from workers.helper import getUserPCName + + + +@dataclass +class NetworkConfig: + """Store network configuration settings""" + server_ip: str = "" + port: int = 8000 + +class NetworkManager: + """Manage network settings and IP detection""" + _instance = None + keep_broadcasting = True + + def __new__(cls): + if cls._instance is None: + cls._instance = super(NetworkManager, cls).__new__(cls) + return cls._instance + + def __init__(self): + self.config = NetworkConfig() + self.config.server_ip = self._get_system_ip() + + def set_server_ip(self, ip: str) -> None: + """Set server IP address""" + self.config.server_ip = ip + + def get_server_ip(self) -> str: + """Get current server IP address""" + # self._get_system_ip() is been called in init block + return self.config.server_ip + + def set_port(self, port: str) -> None: + """Set server port""" + self.config.port = port + + def get_port(self) -> str: + """Get current server port""" + return self.config.port + + def _get_system_ip(self) -> Optional[str]: + """Get system IP address using available methods""" + os_name = platform.system() + + # Try primary method + ip = self._get_ip_from_commands(os_name) + if not ip: + ip=self._get_local_ip() + if ip: + return ip + print("Dev using netifaces") + # Fallback to netifaces + return self._get_ip_from_netifaces() + + def _get_ip_from_commands(self, os_name: str) -> Optional[str]: + """Get IP using system commands""" + try: + if os_name in ('Linux', 'Darwin'): + return self._get_unix_ip() + elif os_name == 'Windows': + return self._get_windows_ip() + raise OSError("Unsupported operating system") + except Exception: + return None + + def _get_unix_ip(self) -> Optional[str]: + """Get IP on Unix-like systems""" + command = ['ifconfig'] if shutil.which('ifconfig') else ['ip', 'addr'] + pattern = re.compile(r'inet\s([\d.]+)') + + result = subprocess.run(command, capture_output=True, text=True) + ips = [ip for ip in pattern.findall(result.stdout) + if not ip.startswith('127.')] + + return self._select_best_ip(ips) + + def _get_windows_ip(self) -> Optional[str]: + """Get IP on Windows systems""" + result = subprocess.run(['ipconfig'], capture_output=True, text=True, shell=True,creationflags=subprocess.CREATE_NO_WINDOW) + pattern = re.compile(r'IPv4.*?:\s*([\d.]+)') + + ips = [ip for ip in pattern.findall(result.stdout) + if not ip.startswith('127.')] + + return self._select_best_ip(ips) + + def _get_ip_from_netifaces(self) -> Optional[str]: + """Fallback method using netifaces""" + try: + for iface in netifaces.interfaces(): + # Prioritize wireless/ethernet + print('Dev see face',iface) + if iface.startswith(('wl', 'en')): + ip = self._get_interface_ip(iface) + if ip: + return ip + + # Try other interfaces + print('Trying Something apart from Wireless') + return None + # for iface in netifaces.interfaces(): + # ip = self._get_interface_ip(iface) + # if ip: + # return ip + + except Exception: + print("Dev neatifaces main failed") + return None + + def _get_interface_ip(self, iface: str) -> Optional[str]: + """Get IP from specific interface""" + addrs = netifaces.ifaddresses(iface) + if netifaces.AF_INET in addrs: + # print(addrs,'\n',netifaces.AF_INET,'\n','addrs netfaces') + for addr in addrs[netifaces.AF_INET]: + ip = addr['addr'] + if ip and not ip.startswith('127.'): + return ip + return None + + def _select_best_ip(self, ips: List[str]) -> Optional[str]: + """Select best IP from list""" + if not ips: + return None + # Prefer 192.168.x.x addresses + for ip in ips: + if ip.startswith('192.168.'): + return ip + return ips[-1] if len(ips) > 1 else ips[0] # for when linux subsystem or vpn is running and command get a lot of ip addresses + + def setSERVER_IP(self, value: str) -> None: + """Set server IP address (public method)""" + self.set_server_ip(value) + + def getSERVER_IP(self) -> str: + """Get current server IP address (public method)""" + return self.get_server_ip() + + def broadcast_ip(self,port,websocket_port): + server_ip = self._get_system_ip() + msg=json.dumps({'ip':server_ip,'name':getUserPCName(),'websocket_port':websocket_port}) + # message = f"SERVER_IP:{server_ip}" + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + print(f"Broadcasting server IP: {server_ip} on port {port}") + try: + while True: + sock.sendto(msg.encode(), ('', port)) + # sock.sendto(message.encode(), ('', port)) + time.sleep(.08) # Broadcast every second + except Exception as e: + print(f"Broadcasting error: {e}") + traceback.print_exc() + finally: + sock.close() + print('Ended BroadCast !!!') + + + def _get_local_ip(self): + # First attempt using hostname + ip = socket.gethostbyname(socket.gethostname()) + if ip == "127.0.0.1": + # If it only returns localhost, try socket connect method + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + s.connect(("8.8.8.8", 80)) # Google DNS + ip = s.getsockname()[0] + except OSError: + pass + finally: + s.close() + return ip + +# Create singleton instance +# # Usage example: +# network = NetworkManager() +# ip = network.getSERVER_IP() +# print(ip) + +# network.setSERVER_IP('') +# ip = network.getSERVER_IP() diff --git a/workers/thumbnails/__init__.py b/workers/thumbnails/__init__.py new file mode 100644 index 0000000..27e6231 --- /dev/null +++ b/workers/thumbnails/__init__.py @@ -0,0 +1,146 @@ +import os +from itertools import chain + +if __name__ == 'thumbnails': + # For Tests + from .testing.sword import NetworkConfig + from .testing.helper import getAppFolder, urlSafePath, _joinPath, getFileExtension +else: + from workers.helper import getAppFolder, urlSafePath, _joinPath, getFileExtension + from workers.sword import NetworkConfig + +# Thumbnailer availability flags + +VideoThumbnailExtractor=use_pc_for_static=JPEGWorker=ExecutableIconExtractor=DocumentIconExtractor=None +try: + from .video import VideoThumbnailExtractor +except Exception as e: + pass +try: + from .base import use_pc_for_static +except Exception as e: + pass +try: + from .doc import DocumentIconExtractor +except Exception as e: + pass + +try: + from .executable import ExecutableIconExtractor +except Exception as e: + pass + +try: + from .image import JPEGWorker +except Exception as e: + print("Failed getting a thumbnailer error:", e) + +# print(use_pc_for_static,VideoThumbnailExtractor,JPEGWorker,ExecutableIconExtractor,DocumentIconExtractor) +# File Type Definitions +MY_OWNED_ICONS = ('.py', '.js', '.css', '.html', '.json', '.deb', '.md', '.sql', '.java') +ZIP_FORMATS = ("rar",'.zip', '.7z', '.tar', '.bzip2', '.gzip', '.xz', '.lz4', '.zstd', '.bz2', '.gz') +VIDEO_FORMATS = ('.mkv', '.mp4', '.avi', '.mov') +AUDIO_FORMATS = ('.mp3', '.wav', '.aac', '.ogg', '.m4a', '.flac', '.wma', '.aiff', '.opus') +PICTURE_FORMATS = ('.png', '.jpg', '.jpeg', '.tif', '.bmp', '.gif', '.svg', '.ico') +SPECIAL_FOLDERS = {'home', 'pictures', 'templates', 'videos', 'documents', 'music', 'favorites', 'share', 'downloads'} +SUBTITLE_EXTENSIONS = ( + ".srt", ".sub", ".sbv", ".smi", ".rt", ".ttml", ".xml", ".vtt", ".lrc", ".stl", + ".sub", ".idx", ".sup", ".pgs", + ".stl", ".cap", ".890", ".pac", ".dci", ".xml", ".fab", + ".scc", ".mcc", ".dfxp", ".imsc", + ".jss", ".ssa", ".ass", ".usf", ".aqt", ".pjs", ".bas" +) +EXECUTABLE_FORMATS = ('.exe', '.dll', '.mun') +DOCUMENT_EXTENSIONS = (".pdf", '.docx', ".doc") + +COMBINED_EXTENSIONS = set(chain( + MY_OWNED_ICONS, ZIP_FORMATS, VIDEO_FORMATS, + SUBTITLE_EXTENSIONS, AUDIO_FORMATS, + PICTURE_FORMATS, EXECUTABLE_FORMATS, + DOCUMENT_EXTENSIONS +)) + +# Main function +def get_icon_for_file(path, is_dir, name, video_paths=None) -> tuple[str, str]: + """Returns appropriate icon and thumbnail based on file type. + And adds more paths to video_paths if need be + """ + if video_paths is None: + video_paths = [] + + if is_dir: + return ( + f"assets/icons/folders/{name.lower()}.png" + if name.lower() in SPECIAL_FOLDERS + else "assets/icons/folders/folder.png", + '' + ) + + ext = os.path.splitext(path)[1].lower() + + if ext not in COMBINED_EXTENSIONS: + return "assets/icons/file.png", '' + + if ext in MY_OWNED_ICONS: + return f"assets/icons/{ext[1:]}.png", '' + + if ext in ZIP_FORMATS: + return "assets/icons/packed.png", '' + + if ext in VIDEO_FORMATS: + video_paths.append(path) + if VideoThumbnailExtractor: + try: + instance = VideoThumbnailExtractor( + path, + server_ip=NetworkConfig.server_ip, + server_port=NetworkConfig.port + ) + return "assets/icons/video.png", instance.thumbnail_url + except Exception as e: + print(f"Video thumbnail error: {e}") + return "assets/icons/video.png", '' + + if ext in SUBTITLE_EXTENSIONS: + return "assets/icons/subtitle.png", '' + + if ext in AUDIO_FORMATS: + return "assets/icons/audio.png", '' + + if ext in PICTURE_FORMATS: + if JPEGWorker: + try: + instance = JPEGWorker(path, NetworkConfig.server_ip) + return "assets/icons/image.png", instance.thumbnail_url + except Exception as e: + print(f"Image thumbnail error: {e}") + return "assets/icons/image.png", '' + + if ext in EXECUTABLE_FORMATS: + if ExecutableIconExtractor: + try: + instance = ExecutableIconExtractor( + path, + NetworkConfig.server_ip, + NetworkConfig.port + ) + return use_pc_for_static("executable.png"), instance.thumbnail_url + except Exception as e: + print(f"Executable icon error: {e}") + return "assets/icons/file.png", '' + + if ext in DOCUMENT_EXTENSIONS: + if ext == '.pdf' and DocumentIconExtractor: + try: + extractor = DocumentIconExtractor( + server_ip=NetworkConfig.server_ip, + server_port=NetworkConfig.port + ) + extractor.add_document(path) + print('pppp',extractor.thumbnail_url) + return extractor.pc_static_img, extractor.thumbnail_url + except Exception as e: + print(f"PDF icon error: {e}") + return "assets/icons/file.png", '' + + return "assets/icons/file.png", '' diff --git a/workers/thumbnails/base.py b/workers/thumbnails/base.py new file mode 100644 index 0000000..f75e283 --- /dev/null +++ b/workers/thumbnails/base.py @@ -0,0 +1,145 @@ +from abc import ABC, abstractmethod +import os +from typing import LiteralString +from PIL import Image + +if __name__ in ['thumbnails.base','base','__main__']: + from testing.helper import gen_unique_filname, getAppFolder, urlSafePath,removeFirstDot,_joinPath + from testing.sword import NetworkConfig +else: + from workers.helper import gen_unique_filname, _joinPath, getAppFolder, urlSafePath, removeFirstDot + from workers.sword import NetworkConfig + +def use_pc_for_static(img_file_name): + return f"http://{NetworkConfig.server_ip}:{NetworkConfig.port}/{urlSafePath(_joinPath(getAppFolder(),"assets","imgs",img_file_name))}" + +class BaseGenException(ABC): + """Handles Creating Failed Img + Needs `thumbnail_path` + Handles creating Fail safe img + """ + @property + @abstractmethod + def thumbnail_path(self): + """Path to the thumbnail image.""" + # Children classes must implement this property. + pass + + def getfailSafeImg(self): + """Returns a fail-safe image Path.""" + # The Placeholder image is used when the original image cannot be processed. + # And is gotten from the assets folder. + # if not self.main_to_display_path: + getfailSafeImg(self.thumbnail_path,'BaseGenException') + + +def getfailSafeImg(thumbnail_path,_abs_class,default_img_name='image.png'): + """Returns a fail-safe image Path.""" + # The Placeholder image is used when the original image cannot be processed. + # And is gotten from the assets folder. + # if not self.main_to_display_path: + try: + if os.path.exists(thumbnail_path): + return + if thumbnail_path.endswith('.jpg'): + default_img_name='image.jpg' + print('entered') + img = Image.open(_joinPath(getAppFolder(),'assets','imgs',default_img_name)) + + img.save(thumbnail_path, quality=60) + + except Exception as e: + print('Error getting fall back:',e) + print(f'-------Error from {_abs_class}--------') + +class BaseGen(ABC): + """Base class for thumbnail generation. + Needs `item_path` + Handles creating `thumbnail_url`, `thumbnail_path` + also `preview_folder` if need be and returing path + """ + thumbnail_folder = 'preview-imgs' + def __init__(self, server_ip: str=NetworkConfig.server_ip, server_port: int=NetworkConfig.port): + """Initializes the BaseGen class.""" + # No path or specfic argument class recives should be formatted in the init block, Beacuse in VideoGen i change dymically + self.server_ip = server_ip + self.server_port = server_port + self.create_preview_folder() + + @property + @abstractmethod + def item_path(self): + """Path to the item being processed.""" + # Children classes must implement this property. + pass + + @property + def img_format(self): + """Returns the image format.""" + # Default image format is PNG, but can be overridden by subclasses. + return 'jpg' + + @property + def preview_folder(self): + """Creates preview folder + + Returns: path to the preview folder.""" + return self.create_preview_folder() + + def create_preview_folder(self): + """Creates a folder for preview images if it doesn't exist.""" + preview_folder__ = _joinPath(getAppFolder(), self.thumbnail_folder) + if not os.path.exists(preview_folder__): + print('Creating preview folder from thumbnail.base.BaseGen: ', preview_folder__) + os.makedirs(preview_folder__, exist_ok=True) + return preview_folder__ + + @property + def thumbnail_url(self): + """Returns the URL for the thumbnail image.""" + return f"http://{self.server_ip}:{self.server_port}/{urlSafePath(removeFirstDot(self.thumbnail_path))}" + + @property + def thumbnail_path(self) -> LiteralString | str | bytes: + """Joins preview folder with unique file name for path + Returns the path to the thumbnail image. + """ + return thumbnail_path_logic(self.item_path,self.img_format,self.preview_folder,_abs_class='BaseGen')#new_img_path + +def thumbnail_path_logic(file_full_path,img_format,preview_folder,_abs_class): + if not file_full_path: + print(f'No actual value for `self.item_path` in {_abs_class}, probarly an empty string: {file_full_path}') + raise ValueError('No actual value for `self.item_path` in BaseGen') + + new_file_name = gen_unique_filname(file_full_path) + '.' + img_format + return _joinPath(preview_folder, new_file_name) + +class DocExtractorABS(ABC): + """ Requires `extract`, `default_img_name` and `thumbnail_path` + Adds Order to Document Extactors and also + + """ + @abstractmethod + def extract(self,item_path,thumbnail_path,config): + """Does extraction.""" + # Children classes must implement this property. + pass + + @property + @abstractmethod + def default_img_name(self): + return 'image.png' + + @property + def thumbnail_path(self): + """Path to the thumbnail being processed.""" + pass + + def getfailSafeImg(self): + """Returns a fail-safe image Path.""" + # The Placeholder image is used when the original image cannot be processed. + # And is gotten from the assets folder. + # if not self.main_to_display_path: + getfailSafeImg(self.thumbnail_path,'DocExtractorABS',self.default_img_name) + + diff --git a/workers/thumbnails/claude-apk.py b/workers/thumbnails/claude-apk.py new file mode 100644 index 0000000..ec2549a --- /dev/null +++ b/workers/thumbnails/claude-apk.py @@ -0,0 +1,147 @@ +python +from http.server import HTTPServer, BaseHTTPRequestHandler +from concurrent.futures import ThreadPoolExecutor +import json +import zipfile +from pyaxmlparser import APK +import base64 +import os + +# Thread pool for background tasks +executor = ThreadPoolExecutor(max_workers=4) + +# Track task status +task_status = {} + +def extract_apk_icon(apk_path, task_id): + """Extract icon from APK file""" + try: + task_status[task_id] = {'status': 'processing', 'progress': 0} + + apk = APK(apk_path) + + # Get icon path from manifest + icon_path = apk.get_app_icon() + + if icon_path: + # Extract icon from APK (APK is a zip file) + with zipfile.ZipFile(apk_path, 'r') as zip_ref: + icon_data = zip_ref.read(icon_path) + + # Save or process icon + output_path = f"icons/{task_id}.png" + os.makedirs("icons", exist_ok=True) + with open(output_path, 'wb') as f: + f.write(icon_data) + + task_status[task_id] = { + 'status': 'complete', + 'icon_path': output_path, + 'package': apk.package + } + else: + task_status[task_id] = {'status': 'error', 'message': 'No icon found'} + + except Exception as e: + task_status[task_id] = {'status': 'error', 'message': str(e)} + +def process_multiple_apks(apk_paths, task_id): + """Process multiple APKs""" + try: + results = [] + total = len(apk_paths) + + for i, apk_path in enumerate(apk_paths): + apk = APK(apk_path) + icon_path = apk.get_app_icon() + + if icon_path: + with zipfile.ZipFile(apk_path, 'r') as zip_ref: + icon_data = zip_ref.read(icon_path) + output_path = f"icons/{apk.package}.png" + os.makedirs("icons", exist_ok=True) + with open(output_path, 'wb') as f: + f.write(icon_data) + + results.append({ + 'package': apk.package, + 'icon': output_path, + 'app_name': apk.application + }) + + # Update progress + task_status[task_id] = { + 'status': 'processing', + 'progress': int((i + 1) / total * 100), + 'processed': i + 1, + 'total': total + } + + task_status[task_id] = { + 'status': 'complete', + 'results': results + } + + except Exception as e: + task_status[task_id] = {'status': 'error', 'message': str(e)} + +class Handler(BaseHTTPRequestHandler): + def do_POST(self): + if self.path == '/extract-icon': + # Single APK + content_length = int(self.headers['Content-Length']) + post_data = json.loads(self.rfile.read(content_length)) + + task_id = post_data.get('task_id', 'task_' + str(len(task_status))) + apk_path = post_data.get('apk_path') + + # Submit to thread pool + executor.submit(extract_apk_icon, apk_path, task_id) + + self.send_response(202) # Accepted + self.send_header('Content-type', 'application/json') + self.end_headers() + self.wfile.write(json.dumps({ + 'task_id': task_id, + 'message': 'Task started' + }).encode()) + + elif self.path == '/extract-multiple': + # Multiple APKs + content_length = int(self.headers['Content-Length']) + post_data = json.loads(self.rfile.read(content_length)) + + task_id = post_data.get('task_id', 'batch_' + str(len(task_status))) + apk_paths = post_data.get('apk_paths', []) + + executor.submit(process_multiple_apks, apk_paths, task_id) + + self.send_response(202) + self.send_header('Content-type', 'application/json') + self.end_headers() + self.wfile.write(json.dumps({ + 'task_id': task_id, + 'message': f'Processing {len(apk_paths)} APKs' + }).encode()) + + def do_GET(self): + if self.path.startswith('/status/'): + # Check task status + task_id = self.path.split('/')[-1] + + status = task_status.get(task_id, {'status': 'not_found'}) + + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.end_headers() + self.wfile.write(json.dumps(status).encode()) + else: + self.send_response(200) + self.send_header('Content-type', 'text/plain') + self.end_headers() + self.wfile.write(b'APK Icon Extractor API') + +if __name__ == '__main__': + server = HTTPServer(('localhost', 8000), Handler) + print("Server running on http://localhost:8000") + server.serve_forever() diff --git a/workers/thumbnails/doc.py b/workers/thumbnails/doc.py new file mode 100644 index 0000000..9024bef --- /dev/null +++ b/workers/thumbnails/doc.py @@ -0,0 +1,463 @@ +# from typing import LiteralString +# -> LiteralString | str | bytes +try: + import psutil +except ImportError: + print("-- run pip install psutil fitz docx") +from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor +from PIL import Image +import os, threading, io, traceback, time, queue +try: + import fitz # PyMuPDF +except ImportError: + pass +try: + from docx import Document +except ImportError: + pass + +from PIL import ImageDraw, ImageFont +from typing import Dict, List, Optional, Type + +import atexit + +if __name__ in ['thumbnails.doc']: + from .base import BaseGen,DocExtractorABS + from .testing.sword import NetworkManager,NetworkConfig + from .base import use_pc_for_static + from .testing.helper import getFileExtension,removeFirstDot,getAppFolder + NetworkConfig.server_ip=NetworkManager().get_server_ip() # NetworkConfig has an import mismatch from different file when running without GUI and straight from server file +else: + from workers.thumbnails.base import BaseGen,DocExtractorABS + from workers.sword import NetworkConfig + from workers.thumbnails.base import use_pc_for_static + from workers.helper import getFileExtension,removeFirstDot,getAppFolder + + +# Import base classes +# (Same import logic as before) +def get_windows_font_path(font_name="arial.ttf"): + """Get the full path to a font file on Windows""" + # Get the Windows directory from environment variables + windir = os.environ.get('WINDIR', 'C:\\Windows') + font_path = os.path.join(windir, 'Fonts', font_name) + + if os.path.exists(font_path): + return font_path + + # Check alternative locations (some systems may differ) + alt_paths = [ + os.path.join(os.environ.get('SystemRoot', 'C:\\Windows'), 'Fonts', font_name), + 'C:\\Windows\\Fonts\\' + font_name, + 'D:\\Windows\\Fonts\\' + font_name # For rare multi-drive installations + ] + + for path in alt_paths: + if os.path.exists(path): + return path + + return None + + +def get_system_font(): + """Get the best available font path""" + # Try Windows first + if os.name == 'nt': + font_path = get_windows_font_path() + if font_path: + return font_path + + # Try Linux/Mac locations if not found on Windows + font_paths = [ + '/usr/share/fonts/truetype/msttcorefonts/arial.ttf', + '/Library/Fonts/Arial.ttf', + os.path.join(os.path.dirname(__file__), 'fonts/arial.ttf') + ] + + for path in font_paths: + if os.path.exists(path): + return path + + bundled_font = os.path.join(getAppFolder(), 'fonts', + 'LiberationSans-Regular.ttf') # LiberationSans-Regular.ttf is free + if os.path.exists(bundled_font): + return bundled_font + + return None # Will use PIL's basic font + + +# ===================== +# Resource-Aware Task Queue +# ===================== +class ThumbnailTaskQueue: + """Centralized queue system with resource monitoring""" + _instance = None + _lock = threading.Lock() + + def __new__(cls): + with cls._lock: + if cls._instance is None: + cls._instance = super().__new__(cls) + cls._instance._init_queue() + return cls._instance + + def _init_queue(self): + self.task_queue = queue.Queue() + self.resource_monitor = ResourceMonitor() + self.config = ThumbnailConfig() + self.executor = None + self.is_running = False + self.processing_thread = None + self.extractor_registry = ExtractorRegistry() + atexit.register(self.shutdown) + + def add_task(self, document_path: str,thumbnail_path:str): + """Add a document to the processing queue""" + ext = self._get_extension(document_path) + + if not self.extractor_registry.get_extractor(ext): + print(f'Thumbnailer not available: {ext}') + return + self.task_queue.put((ext, document_path, thumbnail_path)) + self._ensure_processing() + + def _ensure_processing(self): + """Start processing thread if not already running""" + if not self.is_running and not self.processing_thread: + print('_ensure_processing') + self.is_running = True + self.processing_thread = threading.Thread( + target=self._process_queue, + daemon=True + ) + self.processing_thread.start() + + def _process_queue(self): + """Process tasks from the queue with resource awareness""" + try: + while self.is_running: + # Small sleep to prevent tight looping when queue is empty + time.sleep(0.1) + + # Only check resources when we actually have work to do + if not self.task_queue.empty(): + # Check system resources before processing + if self.resource_monitor.is_system_overloaded(): + time.sleep(5) + continue + + # Process batch of tasks + batch = self._get_next_batch() + print('This is batch:',len(batch)) + if not batch: + continue + + # Group by extension for efficient processing + grouped = self._group_by_extension(batch) + + # Process each group + # print('grouped',grouped) + for ext, tasks in grouped.items(): + self._process_task_group(ext, tasks) + except Exception as e: + print(f"Queue processing failed: {e}") + traceback.print_exc() + finally: + self.is_running = False + self.processing_thread = None + + def _get_next_batch(self) -> list: + """Get next batch of tasks respecting batch size""" + batch = [] + while len(batch) < self.config.batch_size and not self.task_queue.empty(): + try: + batch.append(self.task_queue.get_nowait()) + except queue.Empty: + break + return batch + + def _group_by_extension(self, batch: list) -> dict: + """Group tasks by file extension""" + grouped = {} + for ext, doc_path, thumb_path in batch: + if ext not in grouped: + grouped[ext] = [] + grouped[ext].append((doc_path, thumb_path)) + return grouped + + def _process_task_group(self, ext: str, tasks: list): + """Process a group of tasks for the same extension""" + extractor_class = self.extractor_registry.get_extractor(ext) + if not extractor_class: + return + + try: + # Create executor based on task type + if ext == 'pdf' and os.name == 'nt': # Windows + # On Windows, use threads for PDF processing + executor_class = ThreadPoolExecutor + max_workers = min(len(tasks), self.config.max_threads) + elif self._should_use_multiprocessing(ext): + executor_class = ProcessPoolExecutor + max_workers = min(len(tasks), os.cpu_count() or 1) + else: + executor_class = ThreadPoolExecutor + max_workers = min(len(tasks), self.config.max_threads) + + with executor_class(max_workers) as executor: + # Process all tasks in this group + futures = [] + for doc_path, thumb_path in tasks: + # print('_process_task_group') + # future = executor.submit( + # extractor_class().extract, + # doc_path, + # thumb_path, + # self.config + # ) + future = executor.submit( + self._safe_extract, + extractor_class, + doc_path, + thumb_path + ) + futures.append(future) + + # Wait for completion but don't block queue processing + for future in futures: + try: + future.result(timeout=self.config.task_timeout) + except Exception as e: + print(f"Task failed: {e}") + except Exception as e: + print(f"Failed to process {ext} tasks: {e}") + + def _safe_extract(self, extractor_class, doc_path, thumb_path): + """Wrapper for safe extraction""" + try: + return extractor_class().extract(doc_path, thumb_path, self.config) + except Exception as e: + print(f"Extraction failed for {doc_path}: {e}") + return False + + def _should_use_multiprocessing(self, ext: str) -> bool: + """Determine optimal processing method""" + return ext == 'pdf' # PDF is CPU-bound + + def _get_extension(self, path: str) -> str: + """Get normalized file extension""" + return removeFirstDot(getFileExtension(path)).lower() + + def shutdown(self): + """Clean shutdown of processing system""" + self.is_running = False + if self.processing_thread and self.processing_thread.is_alive(): + self.processing_thread.join(timeout=5) + if self.executor: + self.executor.shutdown(wait=False) + + +# ===================== +# Resource Monitor + +class ResourceMonitor: + """Monitors system resources to prevent overload""" + + def __init__(self, max_mem_usage=0.95, max_cpu_usage=0.8): + self.max_mem = max_mem_usage + self.max_cpu = max_cpu_usage + self.last_check = 0 + self.last_result = False + self.check_interval = 2 # seconds between checks + + def is_system_overloaded(self) -> bool: + """Check if system resources are critically low""" + now = time.time() + if now - self.last_check < self.check_interval: + return self.last_result + + try: + mem = psutil.virtual_memory() + cpu = psutil.cpu_percent(interval=0.5) / 100 # Longer interval for more accuracy + + self.last_result = (mem.percent / 100 > self.max_mem) or (cpu > self.max_cpu) + self.last_check = now + + if self.last_result: + print(f"System overloaded - Mem: {mem.percent}%, CPU: {cpu * 100:.1f}%") + + return self.last_result + except Exception as e: + print(f"Resource check failed: {e}") + return False + +# ===================== +# Configuration +# ===================== +class ThumbnailConfig: + def __init__(self): + # Processing parameters + self.max_threads = 4 + self.batch_size = 10 + self.task_timeout = 120 # seconds + + # PDF settings + self.pdf_zoom = 2.0 + + # DOCX settings + self.docx_zoom = 3.0 + self.docx_image_size = (800, 600) + + +# ===================== +# Extractor Registry +# ===================== +class ExtractorRegistry: + _extractors: Dict[str, Type[DocExtractorABS]] = {} + _lock = threading.Lock() + + @classmethod + def register(cls, extensions: List[str]): + """Decorator to register extractors""" + + def decorator(extractor_class: Type[DocExtractorABS]): + with cls._lock: + for ext in extensions: + cls._extractors[ext] = extractor_class + return extractor_class + + return decorator + + @classmethod + def get_extractor(cls, extension: str) -> Optional[Type[DocExtractorABS]]: + """Get extractor class for file extension""" + return cls._extractors.get(extension.lower()) + + @classmethod + def supported_extensions(cls) -> List[str]: + """Get all supported extensions""" + return list(cls._extractors.keys()) + + +# ===================== +# DocumentIconExtractor (Simplified Client) +# ===================== +class DocumentIconExtractor(BaseGen): + """Simplified client interface to the thumbnail system""" + # Note this logic Set item_path to path if only trying to gen thumbnail_url + + def __init__(self, doc_path: str = '', + server_ip=NetworkConfig.server_ip, server_port=NetworkConfig.port): + super().__init__(server_ip, server_port) + self.task_queue = ThumbnailTaskQueue() + + self._extension=None + self.document_path=doc_path + if doc_path: + self.add_document(doc_path) + self.extension = self.document_path + + @property + def item_path(self): + return self.document_path + + def add_document(self, document_path: str): + """Add document to processing queue""" + self.document_path = document_path + self.task_queue.add_task(document_path, self.thumbnail_path) + + def extract(self): + """For API compatibility - processing happens automatically""" + print('got here 2') + + pass # Processing is managed by the queue system + + @property + def pc_static_img(self): + if self.extension not in self.task_queue.extractor_registry.supported_extensions(): + return "assets/icons/file.png" + return use_pc_for_static(f"{self.extension}.png") + + @property + def extension(self): + return self._extension + + @extension.setter + def extension(self, document_path): + if document_path == '': + self._extension = '' + return + self._extension = removeFirstDot(getFileExtension(document_path).lower()) + + +# ===================== +# Document Extractors +# ===================== +@ExtractorRegistry.register(['pdf']) +class PdfExtractor(DocExtractorABS): + def default_img_name(self): + return 'pdf.png' + def __init__(self): + super().__init__() + # Remove any thread locks or unpickleable attributes + self._config = None # Will be set in extract() + + def extract(self, absolute_filepath: str, thumbnail_path: str, config: ThumbnailConfig): + try: + # Make config available for this call + self._config = config + + doc = fitz.open(absolute_filepath) + page = doc.load_page(0) + mat = fitz.Matrix(self._config.pdf_zoom, self._config.pdf_zoom) + pix = page.get_pixmap(matrix=mat) + img = Image.open(io.BytesIO(pix.tobytes())) + img.save(thumbnail_path) + return True + except Exception as e: + print(f"PDF extraction failed for file_path: {absolute_filepath} - {e}") + traceback.print_exc() + return self.getfailSafeImg() + finally: + self._config = None # Clean up + +@ExtractorRegistry.register(['docx', 'doc']) +class DocxExtractor(DocExtractorABS): + def __init__(self): + super().__init__() + self._font_path = get_system_font() + + @property + def default_img_name(self): + return 'image.png' + + def extract(self, absolute_filepath: str, thumbnail_path: str, config: ThumbnailConfig): + print('got here 1') + try: + doc = Document(absolute_filepath) + img = Image.new('RGB', config.docx_image_size, (255, 255, 255)) + draw = ImageDraw.Draw(img) + + # Get first paragraph text + text = doc.paragraphs[0].text if doc.paragraphs else "Word Document" + + # Configure font + font_size = int(40 * config.docx_zoom) + try: + font = ImageFont.truetype(self._font_path, font_size) if self._font_path else ImageFont.load_default() + except Exception as e: + print(f"Custom PDF font paths not found: {self._font_path} - {e}") + font = ImageFont.load_default() + + draw.text((50, 50), text, fill=(0, 0, 0), font=font) + img.save(thumbnail_path) + return True + except Exception as e: + print(f"DOCX extraction failed: {absolute_filepath} - {e}") + return self.getfailSafeImg() + + +# ===================== +# Initialization +# ===================== +# Load any plugin extractors (implementation would be similar to before) diff --git a/workers/thumbnails/document.py b/workers/thumbnails/document.py new file mode 100644 index 0000000..e65677a --- /dev/null +++ b/workers/thumbnails/document.py @@ -0,0 +1,342 @@ +from concurrent.futures import ThreadPoolExecutor +from PIL import Image +from abc import ABC, abstractmethod +import os,threading,io,traceback,time +import fitz # PyMuPDF + + +if __name__=='image' or __name__=='__main__': + from base import BaseGen,BaseGenException,use_pc_for_static,DocExtractorABS + from testing.sword import NetworkManager,NetworkConfig + from testing.helper import getAppFolder, urlSafePath,_joinPath, getFileExtension,removeFirstDot +else: + from workers.thumbnails.base import BaseGen,BaseGenException,DocExtractorABS + from workers.sword import NetworkConfig + from workers.thumbnails.base import use_pc_for_static + from workers.helper import gen_unique_filname, urlSafePath, _joinPath, getFileExtension,removeFirstDot + + +__server_ip = None +__server_port = None + + + +class DocumentIconExtractor(BaseGen): + # list_of_collected_docs_tuples:list[tuple[str,str]] = [] #(input_path, thumbnail_path) + ordered_object: dict[str,list[tuple[str,str]]]= {} #{'pdf':[(actual_path,thumbnail_path),(actual_path,thumbnail_path)],'docx':[(actual_path,thumbnail_path),(actual_path,thumbnail_path),...]} + + def __init__(self, doc_path:str='',max_threads=4, server_ip = NetworkConfig.server_ip, server_port = NetworkConfig.port,_thread=True): + """ + A document path to get thumbnail_url (the only purpose of this arg is to get thumbnail_url) + The real work is done by `list_of_collected_docs_tuples` that is share btw instances and clear after `extract` method + when i set a `document_path` str for `documents_collection` + """ + super().__init__(server_ip, server_port) + global __server_ip,__server_port + __server_ip = server_ip + __server_port = server_port + # print('entered docs extractor:',doc_path) + + self.thumbnail_folder = '.docs-covers' + self.max_threads = max_threads + self._thread = _thread + + if doc_path: + self.document_path = doc_path # Set item_path to path if only trying to gen thumbnail_url + self._extension = None + self.extension = self.document_path + + @property + def item_path(self): + return self.document_path + + @property + def documents_collection(self)-> dict[str,list[tuple[str,str]]]: + """Getter method""" + return self.ordered_object + + @documents_collection.setter + def documents_collection(self, document_path:str): + """Setter method with pre-processing logic""" + # print("Running logic before setting value") + + # Example validation/pre-processing + if self.extension not in extractors.keys(): # Important so i remember to store all class in `extractors` object + print(f'Document Thumbnailer not yet avaliable: {self.extension}') + return + + if self.extension not in self.ordered_object: + self.ordered_object[self.extension]=[] + + self.document_path=document_path # Set item_path to path if only trying to gen thumbnail_url + self.ordered_object[self.extension].append((document_path,self.thumbnail_path)) + + # self.document_path=document_path # Set item_path to path if only trying to gen thumbnail_url + # self.list_of_collected_docs_tuples.append((document_path,self.thumbnail_path)) + + @property + def extension(self): + return self._extension + + @extension.setter + def extension(self,document_path): + if document_path == '': + self._extension='' + return + self._extension = removeFirstDot(getFileExtension(document_path).lower()) + + def extract(self): + if self._thread: + if not isinstance(self.documents_collection,object): + print(f'Bad input in DocIconExtractor: {self.documents_collection}, excepted object of format: [tuples] of str') + return + + if self.documents_collection.values(): + threading.Thread( + target=self.__assignThreads, + daemon=True + ).start() + else: + container = self.documents_collection + self.ordered_object = {} # Clears self.documents_collection, Don't change this its using a setter referencing `self.ordered_object` + for extension, list_of_tuples in container.items(): + class__ = extractors[extension] + if issubclass(class__,DocExtractorABS): + class__().extract(*list_of_tuples) + + def __assignThreads(self): + """ + Generate thumbnails from multiple videos in parallel. + """ + container = self.documents_collection + self.ordered_object = {} # Clears self.documents_collection, Don't change this its using a setter referencing `self.ordered_object` + try: + for extension, list_of_tuples in container.items(): + try: + with ThreadPoolExecutor(self.max_threads) as executor: + # args_list = [("a", 1), ("b", 2), ("c", 3)] # List of tuples (arg1, arg2) + # executor.map(lambda args: my_func(*args), args_list) + # print('documents_collection: ',list_of_tuples) + results= list(executor.map(lambda args: extractors[extension]().extract(*args), list_of_tuples)) + # print("Thread completed processing", results) # This Actually returns list of values + except Exception as e: + print(f'\nDocumentIconExtractor Failed in badge for {extension}: {e}') + traceback.print_exc() + # self.ordered_object[extension] = [] + # print('Done getting thumbnails for:',extension) + except Exception as e: + print(f"ThreadPool error: {e}") + traceback.print_exc() + # self.ordered_object={} + finally: + pass + # self.list_of_collected_docs_tuples=[] # this will clear in coming stack so bad + # print("Thread cleanup complete") + @property + def pc_static_img(self): + if self.extension not in extractors.keys(): + return "assets/icons/file.png" + return use_pc_for_static(f"{self.extension}.png") + + + +class PdfExtractor(DocExtractorABS): + def __init__(self): + super().__init__() + self._thumbnail_path = '' + + @property + def thumbnail_path(self): + return self._thumbnail_path + + @property + def default_img_name(self): + return removeFirstDot(getFileExtension(self.thumbnail_path).lower()) + + def extract(self,absolute_filepath,thumbnail_path,zoom=1.0): + + """ + Save the first page of a PDF as an image file + + Args: + absolute_filepath: Complete Path to the input PDF file + thumbnail_path: Complete Path to save the output image + zoom: Zoom factor for higher resolution (default 2.0) + """ + self._thumbnail_path = thumbnail_path + try: + # Open the PDF + doc = fitz.open(absolute_filepath) + + # Get the first page + page = doc.load_page(0) + + # Create a matrix for zooming (higher resolution) + mat = fitz.Matrix(zoom, zoom) + + # Get the pixmap (image) of the page + pix = page.get_pixmap(matrix=mat) + + # Convert to PIL Image + img = Image.open(io.BytesIO(pix.tobytes())) + + # Save the image + img.save(self.thumbnail_path) + # print(f"First page saved as {self.thumbnail_path}") + return 'Sam' #Can return value + except Exception as e: + print(f"\nUnexpected PDFExtractor Error---> {e}\nPDF File: {absolute_filepath}\n") + traceback.print_exc() + + # happen when extraction not saved + self.getfailSafeImg() + + +from docx import Document +from PIL import Image +import io +import traceback + +class DocxExtractor(DocExtractorABS): + def __init__(self): + super().__init__() + self._thumbnail_path = '' + self._font_path = self._get_system_font() + + def _get_system_font(self): + """Get the best available font path""" + # Try Windows first + if os.name == 'nt': + font_path = get_windows_font_path() + if font_path: + return font_path + + # Try Linux/Mac locations if not found on Windows + font_paths = [ + '/usr/share/fonts/truetype/msttcorefonts/arial.ttf', + '/Library/Fonts/Arial.ttf', + os.path.join(os.path.dirname(__file__), 'fonts/arial.ttf') + ] + + for path in font_paths: + if os.path.exists(path): + return path + + bundled_font = os.path.join(getAppFolder(), 'fonts', 'LiberationSans-Regular.ttf') # LiberationSans-Regular.ttf is free + if os.path.exists(bundled_font): + return bundled_font + + return None # Will use PIL's basic font + + @property + def thumbnail_path(self): + return self._thumbnail_path + + @property + def default_img_name(self): + return removeFirstDot(getFileExtension(self.thumbnail_path).lower()) + + def extract(self, absolute_filepath, thumbnail_path, zoom=3.0): + """ + Save a thumbnail representation of a DOCX file as an image + + Args: + absolute_filepath: Complete path to the input DOCX file + thumbnail_path: Complete path to save the output image + zoom: Zoom factor for higher resolution (default 2.0) + """ + self._thumbnail_path = thumbnail_path + # print('Received Path:', absolute_filepath) + + try: + # Open the DOCX file + doc = Document(absolute_filepath) + + # Create a blank image (since DOCX doesn't have direct page images like PDF) + # We'll create a representation of the first page's text + img = Image.new('RGB', (800, 600), color=(255, 255, 255)) + + # For demonstration - we'll just show the first paragraph + # In a real implementation, you might want to render text properly + first_paragraph = doc.paragraphs[0].text if len(doc.paragraphs) > 0 else "DOCX Document" + + # Add text to image (simple representation) + from PIL import ImageDraw, ImageFont + draw = ImageDraw.Draw(img) + font_size = int(40 * zoom) + try: + if self._font_path: + font = ImageFont.truetype(self._font_path, font_size) + # print(f'Using {self._font_path}') + # font = ImageFont.truetype("arial.ttf", 40) + else: + font = ImageFont.load_default() + print("Could Find Font Using basic default font - install Arial for better results") + except: + font = ImageFont.load_default() + + + # text = doc.paragraphs[0].text if len(doc.paragraphs) > 0 else "Word Document" + # draw.text((50, 50), text, fill=(0, 0, 0), font=font) + draw.text((50, 50), first_paragraph, fill=(0, 0, 0), font=font) + + # Save the image + img.save(self.thumbnail_path) + # print(f"DOCX thumbnail saved as {self.thumbnail_path}") + return 'Success' # Can return value + + except Exception as e: + print(f"\nUnexpected DocxExtractor Error---> {e}\nDOCX File: {absolute_filepath}\n") + traceback.print_exc() + self.getfailSafeImg() + return 'failed' + +def get_windows_font_path(font_name="arial.ttf"): + """Get the full path to a font file on Windows""" + # Get the Windows directory from environment variables + windir = os.environ.get('WINDIR', 'C:\\Windows') + font_path = os.path.join(windir, 'Fonts', font_name) + + if os.path.exists(font_path): + return font_path + + # Check alternative locations (some systems may differ) + alt_paths = [ + os.path.join(os.environ.get('SystemRoot', 'C:\\Windows'), 'Fonts', font_name), + 'C:\\Windows\\Fonts\\' + font_name, + 'D:\\Windows\\Fonts\\' + font_name # For rare multi-drive installations + ] + + for path in alt_paths: + if os.path.exists(path): + return path + + return None + +from typing import TypedDict, Type +class ExtractorsDict(TypedDict): + pdf: Type[PdfExtractor] + docx: Type[DocxExtractor] + # Add other supported extensions here + +extractors: ExtractorsDict = { + 'pdf': PdfExtractor, + 'docx': DocxExtractor +} +extractors['docx']().extract + +if __name__ == '__main__': + server_ip = NetworkManager().get_server_ip() # Replace with actual server IP if needed + doc_path=r'c:\Users\hp\Desktop\Linux\my_code\Laner\workers\thumbnails\test.pdf' + r=DocumentIconExtractor(doc_path,server_ip,_thread=False) + # print(r.thumbnail_url) + + + +# import sys +# def dump_threads(): +# for thread_id, frame in sys._current_frames().items(): +# print(f"\nThread {thread_id}") +# traceback.print_stack(frame) + +# Call dump_threads() when you detect a freeze \ No newline at end of file diff --git a/workers/thumbnails/executable.py b/workers/thumbnails/executable.py new file mode 100644 index 0000000..20b426b --- /dev/null +++ b/workers/thumbnails/executable.py @@ -0,0 +1,97 @@ +""" +This script extracts the icon from an executable file (EXE) and saves it as a PNG image. +It uses the icoextract library to handle the extraction. +icoextract dependencies: pefile +""" +import traceback,threading + +Image=IconExtractor=None +try: + from PIL import Image +except ImportError: + print("-- run pip install pillow") + +try: + from icoextract import IconExtractor +except ImportError: + print("-- run pip install icoextract") + +if not Image: + raise ImportError('executable.py missing pillow') + +if not IconExtractor: + raise ImportError('executable.py missing icoextract') + +if __name__ in ['thumbnails.executable','executable','__main__']: + from .testing.sword import NetworkManager,NetworkConfig + from .base import BaseGen,BaseGenException +else: + from workers.sword import NetworkManager,NetworkConfig + from workers.thumbnails.base import BaseGen,BaseGenException + + +class IconExtractionError(Exception, BaseGenException): + """Custom exception for icon extraction errors.""" + # Custom exception for icon extraction errors + # This will be raised if the icon extraction fails for any reason + thumbnail_path = '' + def __init__(self, message): + self.getfailSafeImg() + super().__init__(message) + print(f'\n------------------------{self.__class__.__name__} Log start------------------------') + self.message = message + # self.args = args # don’t need self.args = args — super().__init__() already stores the message in .args. + + +class ExecutableIconExtractor(BaseGen): + """Class to handle extraction of icons from executable files.""" + def __init__(self, exe_path: str, server_ip: str=NetworkConfig.server_ip,port: int=NetworkConfig.port,_thread=True): + super().__init__(server_ip=server_ip, server_port=port) + self._item_path = exe_path + IconExtractionError.thumbnail_path = self.thumbnail_path + if _thread: + threading.Thread( + target=self.__extract, + daemon=True + ).start() + else: + self.__extract() + @property + def item_path(self): + """Path to the executable file.""" + return self._item_path + @property + def img_format(self): + """Returns the image format.""" + # Overriding super class property + return 'png' + + def __extract(self): + """Extracts the icon from the executable and saves it as a PNG.""" + try: + extractor = IconExtractor(self.item_path) + icon_data = extractor.get_icon() # Get the largest available icon (returns BytesIO) + + icon = Image.open(icon_data) # Convert BytesIO to PIL Image + if icon.mode != 'RGBA': + icon = icon.convert("RGBA") # Ensure PNG compatibility + icon.save(self.thumbnail_path, format='PNG', optimize=True) + # print(f"Icon extracted and saved to {self.thumbnail_path}") + + except FileNotFoundError as e: + raise IconExtractionError(f"{e}\n---->[Executable file not found: {self.item_path}]<----") from None + except OSError as e: + traceback.print_exc() + raise IconExtractionError(f"Error saving icon: {e}") from None + except Exception as e: + print(f"Error extracting icon: {e}") + traceback.print_exc() + raise IconExtractionError(f"Unexpected errorx: {e}\nExe File: {self.item_path}") from None + + +if __name__ == '__main__': + exe_path = r"C:\Users\hp\Desktop\Linux\my_code\Laner\workers\thumbnails\laner.exe" # og:6.21kb optimize.png:5.86kb + server_ip = NetworkManager().get_server_ip() # Replace with actual server IP if needed + # print(f"Server IP: {server_ip}") + extractor = ExecutableIconExtractor(exe_path, server_ip,NetworkConfig.port,_thread=False) + print(f"Icon URL: {extractor.thumbnail_url}") diff --git a/workers/thumbnails/image.py b/workers/thumbnails/image.py new file mode 100644 index 0000000..87a01a3 --- /dev/null +++ b/workers/thumbnails/image.py @@ -0,0 +1,110 @@ +import traceback, threading + + +Image=cairosvg=None +try: + import cairosvg +except ImportError: + #print('No svg previews') + print("-- run pip install cairosvg") + cairosvg=None + #print('Check cairosvg repo issuses section for how to fix or downloaad and install ') +except Exception as e: + print("SVG Preview Issue Visit page https://github.com/Kozea/CairoSVG/issues/388") + print('In dev mode for SVG Preview download: https://github.com/tschoonj/GTK-for-Windows-Runtime-Environment-Installer/releases/download/2022-01-04/gtk3-runtime-3.24.31-2022-01-04-ts-win64.exe') + +try: + import PIL + from PIL import Image +except ImportError: + print("-- run pip install pillow") +except Exception as e: + print("Exception occurred while import from pillow", e) + + +if not Image: + raise ImportError('image.py missing pillow') + + +if __name__ in ['thumbnails.image','image','__main__']: + from base import BaseGen,BaseGenException + from testing.sword import NetworkManager,NetworkConfig +else: + from workers.thumbnails.base import BaseGen,BaseGenException + from workers.sword import NetworkConfig + +# thumbnail_path for BaseGenException is been implemented by BaseGen +class JPEGWorker(BaseGen,BaseGenException): + """Worker class to convert images to JPEG format.""" + + def __init__(self,img_path:str,server_ip:str,_thread=True): + self.server_ip = server_ip + self.server_port=NetworkConfig.port + self.inputted_img_path = img_path + self.__quality = 80 + if _thread: + threading.Thread( + target=self.genrateJPEG, + daemon=True + ).start() + else: + self.genrateJPEG() + + @property + def item_path(self): + """Path to the executable file.""" + return self.inputted_img_path + + @property + def img_format(self): + """Returns the image format.""" + # Overriding super class property + return 'jpg' + + def is_svg(self): + return self.inputted_img_path.lower().endswith('.svg') + + def genrateJPEG(self): + try: + + if self.is_svg(): + if not cairosvg: + self.getfailSafeImg() + return + self.genPNG_from_SVG() + + im = Image.open(self.inputted_img_path) + rgb_im = im.convert("RGB") + rgb_im.save(self.thumbnail_path, format='JPEG', quality=self.__quality) + + except PIL.UnidentifiedImageError: + print(f'Failed getting JPEG {self.inputted_img_path} <-----------------------------------------') + print('Fail safe image path: ',self.thumbnail_path) + self.getfailSafeImg() + except Exception as e: + print('Unplanned Error at JPEGWorker Class: ',e) + traceback.print_exc() + #TODO Probalbly catch all errors + + def genPNG_from_SVG(self): + """Get png for jpg form svg :) + Sets `self.inputted_img_path` parameter + """ + self.__quality=100 + try: + if not cairosvg: + self.getfailSafeImg() + return + cairosvg.svg2png(url=self.inputted_img_path,write_to=self.thumbnail_path,output_width=1000,output_height=1000) + self.inputted_img_path=self.thumbnail_path + except Exception as e: + print('Failed to convert SVG: ',e) + self.getfailSafeImg() + return + + +if __name__ == '__main__': + server_ip = NetworkManager().get_server_ip() # Replace with actual server IP if needed + img_path=r'C:\Users\Blossom Restore\Downloads\Laner-desktop\assets\imgs\image.png' # og: 1.68mb formatted.png:1.68mb, formatted.jpg:379kb + r=JPEGWorker(img_path,server_ip,_thread=False) + print(r.thumbnail_url) diff --git a/workers/thumbnails/testing/helper.py b/workers/thumbnails/testing/helper.py new file mode 100644 index 0000000..df46b0a --- /dev/null +++ b/workers/thumbnails/testing/helper.py @@ -0,0 +1,125 @@ +import urllib.parse +import hashlib,os,sys,platform,socket +from os.path import join as _joinPath + +def gen_unique_filname(file_path:str): + hash_obj=hashlib.sha256(file_path.encode('utf-8')) + unique_name=hash_obj.hexdigest() + return unique_name + + +def is_wine(): + """ + Detect if the application is running under Wine. + """ + # Check environment variables set by Wine + if "WINELOADER" in os.environ: + return True + + # Check platform.system for specific hints + if platform.system().lower() == "windows": + # If running in "Windows" mode but in a Linux environment, it's likely Wine + return "XDG_SESSION_TYPE" in os.environ or "HOME" in os.environ + + return False + + +def wine_path_to_unix(win_path): + """ + Converts a Windows-style path to a Unix-style path in Wine. + """ + # Wine maps Windows paths under ~/.wine/drive_c + unix_path = win_path.replace("\\", "/") + if unix_path.startswith("C:/"): + wine_home_folder=os.environ['WINEHOMEDIR'].replace("\\","/").split(':/') if 'WINEHOMEDIR' in os.environ else '' + wine_home_folder=wine_home_folder[1] if len(wine_home_folder) > 0 else '' + if wine_home_folder: + unix_path = unix_path.replace("C:/",'/'+wine_home_folder+"/.wine/drive_c/") + else: + wine_home_folder=os.environ['WINECONFIGDIR'].replace("\\","/").split(':/') if 'WINECONFIGDIR' in os.environ else '' + wine_home_folder=wine_home_folder[1] if len(wine_home_folder) > 0 else '' + unix_path = unix_path.replace("C:/", '/'+wine_home_folder+"/drive_c/") + return os.path.normpath(unix_path) + + +def getAppFolder(): + """ + Returns the correct application folder path, whether running on native Windows, + Wine, or directly in Linux. + """ + if hasattr(sys, "_MEIPASS"): + # PyInstaller creates a temp folder (_MEIPASS) + path__ = os.path.abspath(sys._MEIPASS) + else: + # Running from source code + + venv_root = os.path.abspath(os.path.join(sys.executable, '..', '..','..')) # Go up one level from sys.executable + path__=venv_root + # function_folder_formatted_to_get_app_folder = os.path.join(os.path.dirname(__file__),'..') + # path__=os.path.abspath(function_folder_formatted_to_get_app_folder) + + # Normalize path for Wine compatibility + if is_wine(): + path__ = wine_path_to_unix(path__) + + return path__ + + +def getUserPCName(): + """ + Get the current user's PC name. + Returns: str - PC name + """ + pc_name=None + try: + # Try socket hostname first + pc_name = socket.gethostname() + + # Clean and validate hostname + if pc_name and isinstance(pc_name, str): + # Remove special characters and extra spaces + cleaned_name = ' '.join(pc_name.split()) + # Limit length and capitalize + pc_name= cleaned_name[:30] + # Fallback methods if socket failed + + except Exception as e: + print(f"Error in getUserPCName: {e}") + def fallbackPCName(): + """Helper function to get PC name using environment variables""" + pc_name= 'Unknown-PC' + try: + # Try different environment variables + for env_var in ['COMPUTERNAME', 'HOSTNAME', 'HOST', 'USER']: + name = os.environ.get(env_var) + if name: + pc_name= name.strip()[:30] + break + + except Exception as e: + print(f"Error in fallbackPCName: {e}") + pc_name= 'Unknown-PC' + return pc_name + + return pc_name or fallbackPCName() + + +def urlSafePath(path:str): + path_without_drive=os.path.splitdrive(path)[1] + # Normalizing Windows Path Forward Slashes for Url '\\' ---> '/' + normalized_path= path_without_drive.replace('\\','/') + # For URL encoding + url_safe_path=urllib.parse.quote(normalized_path) + return url_safe_path + + + +def removeFirstDot(path:str): + if path[0] == '.': + return path[1:] + else: + return path + + +def getFileExtension(file_path:str): + return os.path.splitext(os.path.basename(file_path))[1] \ No newline at end of file diff --git a/workers/thumbnails/testing/sword.py b/workers/thumbnails/testing/sword.py new file mode 100644 index 0000000..c8d538a --- /dev/null +++ b/workers/thumbnails/testing/sword.py @@ -0,0 +1,193 @@ +import json +import traceback +from typing import Optional,List +import platform +import subprocess +import re +import shutil +from dataclasses import dataclass +import socket +import time + +try: + import netifaces +except ImportError: + print('-- run pip install netifaces') +except Exception as e: + print("Exeception accorded during import of netifaces") + + +from .helper import getUserPCName + + +@dataclass +class NetworkConfig: + """Store network configuration settings""" + server_ip: str = "" + port: str = "8000" + +class NetworkManager: + """Manage network settings and IP detection""" + _instance = None + keep_broadcasting = True + + def __new__(cls): + if cls._instance is None: + cls._instance = super(NetworkManager, cls).__new__(cls) + return cls._instance + + def __init__(self): + self.config = NetworkConfig() + self.config.server_ip = self._get_system_ip() + + def set_server_ip(self, ip: str) -> None: + """Set server IP address""" + self.config.server_ip = ip + + def get_server_ip(self) -> str: + """Get current server IP address""" + return self.config.server_ip + + def set_port(self, port: str) -> None: + """Set server port""" + self.config.port = port + + def get_port(self) -> str: + """Get current server port""" + return self.config.port + + def _get_system_ip(self) -> Optional[str]: + """Get system IP address using available methods""" + os_name = platform.system() + + # Try primary method + ip = self._get_ip_from_commands(os_name) or self._get_local_ip() + if ip: + return ip + print("Dev using netifaces") + # Fallback to netifaces + return self._get_ip_from_netifaces() + + def _get_ip_from_commands(self, os_name: str) -> Optional[str]: + """Get IP using system commands""" + try: + if os_name in ('Linux', 'Darwin'): + return self._get_unix_ip() + elif os_name == 'Windows': + return self._get_windows_ip() + raise OSError("Unsupported operating system") + except Exception: + return None + + def _get_unix_ip(self) -> Optional[str]: + """Get IP on Unix-like systems""" + command = ['ifconfig'] if shutil.which('ifconfig') else ['ip', 'addr'] + pattern = re.compile(r'inet\s([\d.]+)') + + result = subprocess.run(command, capture_output=True, text=True) + ips = [ip for ip in pattern.findall(result.stdout) + if not ip.startswith('127.')] + + return self._select_best_ip(ips) + + def _get_windows_ip(self) -> Optional[str]: + """Get IP on Windows systems""" + result = subprocess.run(['ipconfig'], capture_output=True, text=True, shell=True,creationflags=subprocess.CREATE_NO_WINDOW) + pattern = re.compile(r'IPv4.*?:\s*([\d.]+)') + + ips = [ip for ip in pattern.findall(result.stdout) + if not ip.startswith('127.')] + + return self._select_best_ip(ips) + + def _get_ip_from_netifaces(self) -> Optional[str]: + """Fallback method using netifaces""" + try: + for iface in netifaces.interfaces(): + # Prioritize wireless/ethernet + print('Dev see face',iface) + if iface.startswith(('wl', 'en')): + ip = self._get_interface_ip(iface) + if ip: + return ip + + # Try other interfaces + print('Trying Something apart from Wireless') + return None + # for iface in netifaces.interfaces(): + # ip = self._get_interface_ip(iface) + # if ip: + # return ip + + except Exception: + print("Dev neatifaces main failed") + return None + + def _get_interface_ip(self, iface: str) -> Optional[str]: + """Get IP from specific interface""" + addrs = netifaces.ifaddresses(iface) + if netifaces.AF_INET in addrs: + # print(addrs,'\n',netifaces.AF_INET,'\n','addrs netfaces') + for addr in addrs[netifaces.AF_INET]: + ip = addr['addr'] + if ip and not ip.startswith('127.'): + return ip + return None + + def _select_best_ip(self, ips: List[str]) -> Optional[str]: + """Select best IP from list""" + if not ips: + return None + # Prefer 192.168.x.x addresses + for ip in ips: + if ip.startswith('192.168.'): + return ip + return ips[0] + + def setSERVER_IP(self, value: str) -> None: + """Set server IP address (public method)""" + self.set_server_ip(value) + + def getSERVER_IP(self) -> str: + """Get current server IP address (public method)""" + return self.get_server_ip() + + def broadcast_ip(self,port,websocket_port): + server_ip = self._get_system_ip() + msg=json.dumps({'ip':server_ip,'name':getUserPCName(),'websocket_port':websocket_port}) + # message = f"SERVER_IP:{server_ip}" + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + print(f"Broadcasting server IP: {server_ip} on port {port}") + try: + while True: + sock.sendto(msg.encode(), ('', port)) + # sock.sendto(message.encode(), ('', port)) + time.sleep(.08) # Broadcast every second + except Exception as e: + print(f"Broadcasting error: {e}") + traceback.print_exc() + finally: + sock.close() + print('Ended BroadCast !!!') + + + def _get_local_ip(self): + # First attempt using hostname + ip = socket.gethostbyname(socket.gethostname()) + if ip == "127.0.0.1": + # If it only returns localhost, try socket connect method + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + s.connect(("8.8.8.8", 80)) # Google DNS + ip = s.getsockname()[0] + finally: + s.close() + return ip +# Create singleton instance +# # Usage example: +# network = NetworkManager() +# ip = network.getSERVER_IP() +# network.setSERVER_IP('') +# ip = network.getSERVER_IP() diff --git a/workers/thumbmailGen.py b/workers/thumbnails/video.py similarity index 50% rename from workers/thumbmailGen.py rename to workers/thumbnails/video.py index bf09209..0b0d3ce 100644 --- a/workers/thumbmailGen.py +++ b/workers/thumbnails/video.py @@ -1,10 +1,34 @@ -import cv2 -import os +import os, shutil, threading from concurrent.futures import ThreadPoolExecutor from pathlib import Path -from PIL import Image, ImageDraw, ImageFont -from workers.helper import gen_unique_filname - +Image= ImageDraw= cv2 = None +try: + import cv2 +except ImportError: + print("-- run pip install opencv-python-headless") + VideoThumbnailExtractor = None +except Exception as e: + print("Exception occurred while importing cv2 e:", e) + +try: + from PIL import Image, ImageDraw +except ImportError: + print("-- run pip install pillow") + VideoThumbnailExtractor = None +except Exception as e: + print("Exception occurred while importing PIL e:", e) + +if not cv2 or not Image: + raise ImportError('video.py missing pillow and opencv-python-headless') + +if __name__ == 'thumbnails.video': + from .testing.sword import NetworkManager,NetworkConfig + from .base import BaseGen,BaseGenException + from .testing.helper import gen_unique_filname, getAppFolder +else: + from workers.sword import NetworkConfig + from workers.thumbnails.base import BaseGen,BaseGenException + from workers.helper import gen_unique_filname, getAppFolder def add_black_and_white_boxes(image): """ @@ -65,7 +89,9 @@ def generate_thumbnail(video_path, output_path, time=1.0): # cv2.imwrite(output_path, frame, [cv2.IMWRITE_JPEG_QUALITY,10]) # print(f"Thumbnail saved at {output_path}") else: - print(f"Failed to capture frame for {video_path}") + source_file = os.path.join(getAppFolder(),'assets','imgs','video.png') + print(f"Failed to capture frame for {video_path} ---- Using backup Thumbnail {source_file}") + shutil.copy(source_file, output_path) cap.release() @@ -86,12 +112,84 @@ def process_video(video_path:str): thumbnail_name = gen_unique_filname(video_path) print(video_path,'|||',thumbnail_name) - output_path = f"{output_dir}/{thumbnail_name}_thumbnail.jpg" + output_path = f"{output_dir}/{thumbnail_name}.jpg" generate_thumbnail(video_path, output_path, time) with ThreadPoolExecutor(max_threads) as executor: executor.map(process_video, video_paths) + +class VideoThumbnailExtractor(BaseGen): + def __init__(self, video_paths:str|list[str],time=1.0,max_threads=4, server_ip = NetworkConfig.server_ip, server_port = NetworkConfig.port): + """ + video_paths can be list of video paths or a video path to get thumbnail_url + """ + super().__init__(server_ip, server_port) + self.thumbnail_folder = 'thumbnails' + self.video_paths = video_paths + self.time = time + self.max_threads = max_threads + self._item_path = video_paths if isinstance(video_paths,str) else '' # Set item_path to path if only trying to gen thumbnail_url + + @property + def item_path(self): + """Path to the video file.""" + return self._item_path + + def extract(self): + if self.video_paths: + threading.Thread( + target=self.__assignThreads, + daemon=True + ).start() + + def __assignThreads(self): + """ + Generate thumbnails from multiple videos in parallel. + """ + + with ThreadPoolExecutor(self.max_threads) as executor: + executor.map(self.__generate_thumbnail, self.video_paths) + + def __generate_thumbnail(self, video_path:str): + """ + Generate a thumbnail from a single video. + + :param video_path: Path to the input video. + :param time: Time (in seconds) to capture the thumbnail frame. + """ + # important to set before usage of `self.thumbnail_path` + # `self.item_path` helps `self.thumbnail_path` in parent class + self._item_path = video_path + + cap = cv2.VideoCapture(video_path) + fps = cap.get(cv2.CAP_PROP_FPS) + frame_number = int(self.time * fps) + cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number) + + success, frame = cap.read() + if success: + Path(self.thumbnail_path).parent.mkdir(parents=True, exist_ok=True) + + # Convert the frame to RGB (required for Pillow) + frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + image = Image.fromarray(frame_rgb) + + # Add overlay decorations + image_with_overlay = add_black_and_white_boxes(image) + + # Save the thumbnail with decorations + image_with_overlay.save(self.thumbnail_path, "JPEG", quality=10) + # cv2.imwrite(output_path, frame, [cv2.IMWRITE_JPEG_QUALITY,10]) + # print(f"Thumbnail saved at {output_path}") + else: + source_file = os.path.join(getAppFolder(),'assets','imgs','video.png') + print(f"Failed to capture frame for {video_path} ---- Using backup Thumbnail {source_file}") + shutil.copy(source_file, self.thumbnail_path) + + cap.release() + + # Example usage # if __name__ == "__main__": diff --git a/workers/web_socket.py b/workers/web_socket.py new file mode 100644 index 0000000..64dede1 --- /dev/null +++ b/workers/web_socket.py @@ -0,0 +1,229 @@ +import websockets,asyncio,traceback +from websockets.asyncio.connection import Connection # for type +import json,secrets + +if __name__=='web_socket': + from helper import getUserPCName +else: + from workers.helper import getUserPCName + + +# Socket for connection (handling only authns for now) +# Always Send JSON dumps to client +class WebSocketConnectionHandler: + def __init__(self, websocket: Connection,ip,main_server_port, connection_signal=None): + self.websocket = websocket + self.ip=ip + self._main_server_port= main_server_port + self.connection_signal = connection_signal + self.pc_name=getUserPCName() + self.authenticated = False + self.response_event = asyncio.Event() + self.__username='' + # TODO Use secret file to store connected users + self.connected_clients = {} # {ip: token} + + async def handle_connection_request(self, message): + """Handle WebSocket connection requests""" + print('****************') + try: + message_dict = json.loads(message) + request_type = message_dict.get('request') + print(request_type, ' ------') + if request_type == 'password' and self.connection_signal: + # Password-based authentication flow + device_ip = self.websocket.remote_address[0] + print(f"Password request from {device_ip}") + self.__username = message_dict.get('name','') + self.connection_signal.connection_request.emit(message_dict, self) + await self.response_event.wait() # Wait for user decision + + elif request_type == 'auth': + # Token-based authentication flow + print('Starting token authentication') + token = message_dict.get('token', '') + client_ip = self.websocket.remote_address[0] + + if token and self.connected_clients.get(client_ip) == token: + self.authenticated = True + await self.websocket.send(json.dumps({"auth": True,"token":token,'ip':self.ip,'name':self.pc_name,'main_server_port':self._main_server_port})) + else: + await self.websocket.send(json.dumps({"auth": False,"token":token,'ip':self.ip,'name':self.pc_name})) + + return self.authenticated + + except json.JSONDecodeError: + print(f"Invalid JSON received: {message}") + await self.websocket.send(json.dumps({'error':"invalid_request_format send json dumps instead",'name':self.pc_name})) + except Exception as e: + traceback.print_exc() + print(f'Unexpected error: {e}') + await self.websocket.send(json.dumps({'error':"server_error",'name':self.pc_name})) + return False + + async def accept(self): + """Accept connection and generate token""" + token = secrets.token_hex(16) + client_ip = self.websocket.remote_address[0] + self.connected_clients[client_ip] = token + self.authenticated = True + try: + await self.websocket.send(json.dumps({ + "status": "yes", + "token": token,'name':self.pc_name + })) + except websockets.exceptions.ConnectionClosedError: + # This happened when phone screen was closed before PC responded to connection request + print(f"User: {self.__username} disconnected add this to app console or [Do A popup when user click Accepts]") + self.response_event.set() + async def reject(self): + """Reject connection""" + try: + await self.websocket.send(json.dumps({ + "status": "no",'name':self.pc_name + })) + except websockets.exceptions.ConnectionClosedError: + # This happened when phone screen was closed before PC responded to connection request + print(f"User: {self.__username} disconnected add this to app console or [Do A popup when user click Accepts]") + self.response_event.set() + async def handle_connection(self): + """Main connection handling loop""" + try: + # Initial authentication + msg = await self.websocket.recv() + authenticated = await self.handle_connection_request(message=msg) + if not authenticated: # This catches when user rejects + await self.websocket.close() + return + + # Other Incoming Messages + async for message in self.websocket: + await self.handle_connection_request(message=message) + print(f"Received message: {message}") + + except websockets.exceptions.ConnectionClosed: + print("Client disconnected") + except Exception as e: + print(f"WebSocket error: {e}") + traceback.print_exc() + finally: + # Clean up + client_ip = self.websocket.remote_address[0] + self.connected_clients.pop(client_ip, None) + await self.websocket.close() + + + + + + +# class WebSocketConnectionHandler: +# def __init__(self, websocket:Connection, connection_signal=None): +# self.websocket= websocket +# self.connection_signal = connection_signal +# self.authenticated = False +# self.response_event = asyncio.Event() +# # TODO Please secert file to store connected users +# self.connected_clients = {} +# async def handle_connection_request(self,message): +# """Handle WebSocket connection requests""" +# print('didn"t call me') +# # TODO don't remove line use ip later in setting for some other stuff +# try: +# device_ip = f"Device-{self.websocket.remote_address[0]}" +# # message = await self.websocket.recv() +# message_dict = json.loads(message) +# print(message_dict,type(message_dict),message_dict['request']) +# if self.connection_signal and message_dict['request'] == 'password': +# # print('self ---- ',self) +# print('erro') +# self.connection_signal.connection_request.emit(message_dict, self) # Pass the handler instance +# print('erro1') +# await self.response_event.wait() # this keeps connection open +# elif message_dict['request'] == 'auth': +# print('doing authn') +# token = message_dict['token'] if 'token' else '' +# if self.connected_clients.get(self.websocket.remote_address[0]) == token: +# await self.websocket.send("authenticated_successfully") +# else: +# await self.websocket.send("authentication_failed") +# # return self.authenticated +# except json.JSONDecodeError: +# print(f"Received non-JSON message: {message}") +# except Exception as e: +# traceback.print_exc() +# print('unexcepted error from WebSocketConnectionHandler.handle_connection_request ',e) + +# # if self.connection_signal: +# # print('self ---- ',self) +# # self.connection_signal.connection_request.emit(message_dict, self) # Pass the handler instance +# # await self.response_event.wait() +# # print('test0----------------------') +# # return self.authenticated +# # print('test1----------------------') +# return False +# # Seprate accept and reject methods because will add other stuff to each, TO KEEP CLEAN +# async def accept(self): +# password = secrets.token_hex(16) +# self.connected_clients[self.websocket.remote_address[0]] = password +# await self.websocket.send(f"accepted:{password}") +# self.response_event.set() +# # await self.websocket.send("ACCESS_GRANTED") +# async def reject(self): +# await self.websocket.send("ACCESS_DENIED") +# self.response_event.set() + +# async def handle_connection(self): +# """Main connection handling loop""" +# try: +# # Wait for authentication +# msg = await self.websocket.recv() +# print('test0 ',msg) +# authenticated = await self.handle_connection_request(message=msg) +# print('test1') + +# # if not authenticated: +# # await self.websocket.close() +# # return +# # # Keep connection alive +# # while True: +# # message = await self.websocket.recv() +# # print(f"Received: {message}") +# # print('not reachs here') +# # Handle messages here + +# except websockets.exceptions.ConnectionClosed: +# print("Client disconnected") +# except Exception as e: +# print(f"WebSocket error: {e}") +# traceback.print_exc() +# await self.websocket.close() + + +# # async def send_response(self, accepted): +# # """Send acceptance/rejection to client""" +# # self.authenticated = accepted +# # if accepted: +# # await self.websocket.send("ACCESS_GRANTED") +# # else: +# # await self.websocket.send("ACCESS_DENIED") +# # self.response_event.set() + +# # async def on_connect(self): +# # """Handle new WebSocket connections""" +# # try: +# # authenticated = await self.handle_connection_request() +# # if not authenticated: +# # await self.websocket.close() +# # return + +# # # Connection is authenticated, handle messages +# # async for message in self.websocket: +# # print(f"Received: {message}") +# # # Handle your WebSocket messages here + +# # except websockets.exceptions.ConnectionClosed: +# # print("Client disconnected") +# # except Exception as e: +# # print(f"WebSocket error: {e}") +# # await self.websocket.close()