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 @@
-
-
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()