diff --git a/.github/workflows/android-debug.yml b/.github/workflows/android-debug.yml
index 6f72d8c..ad3773e 100644
--- a/.github/workflows/android-debug.yml
+++ b/.github/workflows/android-debug.yml
@@ -87,9 +87,9 @@ jobs:
echo "apk=$APK" >> $GITHUB_OUTPUT
echo "Built APK: $APK"
-
+
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: package
- path: ${{ steps.build.outputs.apk }}
+ path: ${{ steps.build.outputs.apk }}
\ No newline at end of file
diff --git a/README.md b/README.md
index a731ec9..f9e0512 100644
--- a/README.md
+++ b/README.md
@@ -1,63 +1,102 @@
-Wally: A working Android wallpaper carousel with no ads.
+# Wally
+
+**A simple, ad-free Android wallpaper carousel.**
[](https://github.com/Fector101/wallpaper-carousel/actions/workflows/android-release.yml)
-[APK Download](https://github.com/Fector101/wallpaper-carousel/releases/download/v1.0.6/waller.apk)
+**[Download APK](https://github.com/Fector101/wallpaper-carousel/releases/download/v1.0.6/waller.apk)**
+
+---
+
+## Overview
+
+Wally automatically rotates your wallpapers using images you choose.
-- The wallpaper automatically switches to a new user-selected image every 2 minutes (or at a custom interval set by the user). Users can also choose changes to happen on screen wakes.
+* Change wallpapers every **2 minutes** (or any custom interval)
+* Option to update wallpapers on **screen wake**
+* Fully **offline** and **ad-free**
+---
+## Features
-## More Features
+### Wallpaper Carousel
-### Home screen widget
-- To view Lock-screen image from home screen
-- Can Tap to Enter App
+* Automatically cycles through selected images
+* Custom time intervals
+* Optional change on screen wake
-### Notification
-- Large Icon to Preview Next image
-- Skip Button to change upcoming Image
-- Stop Button to End carousel
+### Home Screen Widget
-### Permissions
-- Access to Only Images.
-- Notifications for more user controls and preview
+* View current lock-screen wallpaper from your home screen
+* Tap the widget to quickly open the app
-### Help
-- Saving added images in app path only
-- Added export button to get images if uninstalling app
-- A Button to Share Images to other Apps
-- App is set to Auto detect android Theme (Dark Theme Recommended 😅)
+### Notification Controls
-## Few Screen Samples
+* Preview the next wallpaper (large icon)
+* Skip to the next image
+* Stop the carousel anytime
-All screenshot samples are in low Res (for fast loading)
-| **View Screen Design** | **Home Screen Widget** |
-|-------------------------------------|--------------------------------------------------------|
-| |  |
+### Image Import & Sharing
+* Share images directly to Wally from other apps
+* Easily share images from Wally to other apps
---
+## Permissions
-| **Gallery Screen Design** | **Preview Screen Design** |
-|-------------------------------------|--------------------------------------------------------|
-| |  |
+* Images only — no unnecessary access
+* Notifications — for controls and previews
+---
+
+## Help & Notes
+
+* Images are stored inside the app’s internal directory
+* Use the Export feature before uninstalling to keep your images
+* Automatically adapts to system theme (dark mode recommended)
---
+## Screenshots
+*All screenshots are low resolution for faster loading*
-| **Request Screen Design** | **Request Screen Dark Mode** |
-|-------------------------------------|--------------------------------------------------------|
-| |  |
+### Main Screens
+| View Screen | Home Screen Widget |
+| ---------------------------------------------------------------- | -------------------------------------------------------- |
+|  |  |
+
+---
-**Share Image to other App**
+### Gallery & Preview
+| Gallery Screen | Preview Screen |
+| ------------------------------------------------------ | --------------------------------------------------- |
+|  |  |
+
+---
+
+### Request Screens
+
+| Light Mode | Dark Mode |
+| ------------------------------------------------------ | ---------------------------------------------------------- |
+|  |  |
+
+---
+
+### Share Feature
+
+
+
+---
-
+## Why Wally?
+* No ads
+* No tracking
+* Clean, focused wallpaper experience
---
diff --git a/app_src/android/DEV.md b/app_src/android/DEV.md
index 5034f5a..59c7487 100644
--- a/app_src/android/DEV.md
+++ b/app_src/android/DEV.md
@@ -205,4 +205,13 @@ jobs:
name: Release v${{ steps.version.outputs.VERSION }}
files: bin/waller.apk
-```
\ No newline at end of file
+```
+### To see Only App Logs
+```shell
+adb logcat | grep -E "python"
+```
+
+
+## P4a.hook
+Peek at AndroidManifest.xml File before editing with [hook.py](p4a/hook.py)
+[AndroidManifest.xml Location](../../.buildozer/android/platform/build-arm64-v8a_armeabi-v7a/dists/waller/src/main/AndroidManifest.xml)
diff --git a/app_src/android/p4a/hook.py b/app_src/android/p4a/hook.py
index 34c4ced..56b7495 100644
--- a/app_src/android/p4a/hook.py
+++ b/app_src/android/p4a/hook.py
@@ -114,6 +114,11 @@ def after_apk_build(toolchain: ToolchainCL):
"""
manifest_file_content = manifest_file_content.replace("", f"{image_share_from_other_apps_to_app_activity}")
+ manifest_file_content = insert_to_end_of_xml(f"""
+ """,manifest_file_content)
android_manifest_file_path.write_text(manifest_file_content, encoding="utf-8")
print("Successfully: Updated Manifest!\n",manifest_file_content)
diff --git a/app_src/android/src/CameraActivity.java b/app_src/android/src/CameraActivity.java
new file mode 100644
index 0000000..5ef2a15
--- /dev/null
+++ b/app_src/android/src/CameraActivity.java
@@ -0,0 +1,327 @@
+package org.wally.waller;
+
+import android.Manifest;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.RippleDrawable;
+import android.content.res.ColorStateList;
+import android.os.Bundle;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.activity.ComponentActivity;
+import androidx.camera.core.Camera;
+import androidx.camera.core.CameraControl;
+import androidx.camera.core.CameraInfo;
+import androidx.camera.core.CameraSelector;
+import androidx.camera.core.ImageCapture;
+import androidx.camera.core.ImageCaptureException;
+import androidx.camera.core.Preview;
+import androidx.camera.lifecycle.ProcessCameraProvider;
+import androidx.camera.view.PreviewView;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.io.File;
+import java.text.SimpleDateFormat;
+import java.util.Locale;
+import java.util.concurrent.Executor;
+
+public class CameraActivity extends ComponentActivity {
+
+ private static final String TAG = "CAMERA_DBG";
+
+ public static final String EXTRA_PHOTO_PATH = "photo_path";
+ private static final int REQ_CAMERA = 10;
+
+ private PreviewView previewView;
+ private ImageCapture imageCapture;
+ private ProcessCameraProvider cameraProvider;
+ private Executor mainExecutor;
+
+ private Camera camera;
+ private CameraSelector cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA;
+
+ private boolean torchOn = false;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Log.d(TAG, "onCreate");
+
+ mainExecutor = ContextCompat.getMainExecutor(this);
+
+ // ===== ROOT =====
+ FrameLayout root = new FrameLayout(this);
+
+ // ===== PREVIEW =====
+ previewView = new PreviewView(this);
+ previewView.setLayoutParams(new FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.MATCH_PARENT,
+ FrameLayout.LayoutParams.MATCH_PARENT
+ ));
+ previewView.setScaleType(PreviewView.ScaleType.FILL_CENTER);
+ root.addView(previewView);
+
+ // ===== BOTTOM BAR =====
+ LinearLayout bar = new LinearLayout(this);
+ bar.setOrientation(LinearLayout.HORIZONTAL);
+ bar.setGravity(Gravity.CENTER);
+ bar.setPadding(dp(18), dp(12), dp(18), dp(24));
+
+ FrameLayout.LayoutParams barParams = new FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ );
+ barParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
+ bar.setLayoutParams(barParams);
+
+ GradientDrawable barBg = new GradientDrawable();
+ barBg.setColor(Color.parseColor("#66000000"));
+ barBg.setCornerRadius(dp(28));
+ bar.setBackground(barBg);
+
+ ImageButton btnFlip = makeRoundIconButton(android.R.drawable.ic_menu_rotate, dp(44));
+ ImageButton btnShoot = makeRoundIconButton(android.R.drawable.ic_menu_camera, dp(72));
+ ImageButton btnTorch = makeRoundIconButton(android.R.drawable.btn_star_big_off, dp(44));
+
+ LinearLayout.LayoutParams smallLp = new LinearLayout.LayoutParams(dp(48), dp(48));
+ smallLp.leftMargin = dp(10);
+ smallLp.rightMargin = dp(10);
+
+ LinearLayout.LayoutParams bigLp = new LinearLayout.LayoutParams(dp(76), dp(76));
+ bigLp.leftMargin = dp(14);
+ bigLp.rightMargin = dp(14);
+
+ bar.addView(btnFlip, smallLp);
+ bar.addView(btnShoot, bigLp);
+ bar.addView(btnTorch, smallLp);
+
+ root.addView(bar);
+
+ // ===== CLOSE =====
+ ImageButton btnClose = makeRoundIconButton(android.R.drawable.ic_menu_close_clear_cancel, dp(40));
+ FrameLayout.LayoutParams closeLp = new FrameLayout.LayoutParams(dp(44), dp(44));
+ closeLp.gravity = Gravity.TOP | Gravity.START;
+ closeLp.leftMargin = dp(12);
+ closeLp.topMargin = dp(12);
+ root.addView(btnClose, closeLp);
+
+ setContentView(root);
+
+ // ===== BUTTONS =====
+ btnClose.setOnClickListener(v -> {
+ Log.d(TAG, "Close clicked");
+ setResult(RESULT_CANCELED);
+ finish();
+ });
+
+ btnShoot.setOnClickListener(v -> {
+ Log.d(TAG, "Shoot clicked");
+ takePhoto();
+ });
+
+ btnFlip.setOnClickListener(v -> {
+ Log.d(TAG, "Flip clicked");
+ cameraSelector = (cameraSelector == CameraSelector.DEFAULT_BACK_CAMERA)
+ ? CameraSelector.DEFAULT_FRONT_CAMERA
+ : CameraSelector.DEFAULT_BACK_CAMERA;
+ Log.d(TAG, "Camera switched");
+ bindCameraUseCases();
+ });
+
+ btnTorch.setOnClickListener(v -> {
+ Log.d(TAG, "Torch clicked");
+ toggleTorch(btnTorch);
+ });
+
+ // ===== PERMISSION =====
+ Log.d(TAG, "Checking permission");
+
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
+ == PackageManager.PERMISSION_GRANTED) {
+ Log.d(TAG, "Permission granted");
+ startCamera();
+ } else {
+ Log.d(TAG, "Requesting permission");
+ ActivityCompat.requestPermissions(this,
+ new String[]{Manifest.permission.CAMERA},
+ REQ_CAMERA);
+ }
+ }
+
+ // ---------- Camera ----------
+ private void startCamera() {
+ Log.d(TAG, "startCamera");
+
+ ListenableFuture future =
+ ProcessCameraProvider.getInstance(this);
+
+ future.addListener(() -> {
+ try {
+ Log.d(TAG, "CameraProvider ready");
+ cameraProvider = future.get();
+
+ previewView.post(() -> {
+ Log.d(TAG, "Binding camera");
+ bindCameraUseCases();
+ });
+
+ } catch (Exception e) {
+ Log.e(TAG, "CameraProvider error", e);
+ finish();
+ }
+ }, mainExecutor);
+ }
+
+ private void bindCameraUseCases() {
+ Log.d(TAG, "bindCameraUseCases");
+
+ if (cameraProvider == null) {
+ Log.w(TAG, "cameraProvider NULL");
+ return;
+ }
+
+ cameraProvider.unbindAll();
+
+ Preview preview = new Preview.Builder().build();
+ preview.setSurfaceProvider(previewView.getSurfaceProvider());
+
+ imageCapture = new ImageCapture.Builder().build();
+
+ try {
+ camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture);
+ Log.d(TAG, "Camera bound OK");
+ } catch (Exception e) {
+ Log.e(TAG, "Bind failed", e);
+ }
+ }
+
+ private void takePhoto() {
+ Log.d(TAG, "takePhoto");
+
+ if (imageCapture == null) {
+ Log.w(TAG, "imageCapture NULL");
+ return;
+ }
+
+ File dir = new File(getCacheDir(), "captures");
+ if (!dir.exists()) dir.mkdirs();
+
+ String ts = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US)
+ .format(System.currentTimeMillis());
+
+ File file = new File(dir, "IMG_" + ts + ".jpg");
+ Log.d(TAG, "Saving: " + file.getAbsolutePath());
+
+ ImageCapture.OutputFileOptions options =
+ new ImageCapture.OutputFileOptions.Builder(file).build();
+
+ imageCapture.takePicture(options, mainExecutor, new ImageCapture.OnImageSavedCallback() {
+ @Override
+ public void onImageSaved(@NonNull ImageCapture.OutputFileResults output) {
+ Log.d(TAG, "Photo saved");
+
+ Intent data = new Intent();
+ data.putExtra(EXTRA_PHOTO_PATH, file.getAbsolutePath());
+ setResult(RESULT_OK, data);
+ finish();
+ }
+
+ @Override
+ public void onError(@NonNull ImageCaptureException exception) {
+ Log.e(TAG, "Photo error", exception);
+ }
+ });
+ }
+
+ // ---------- Torch ----------
+ private void toggleTorch(ImageButton btnTorch) {
+ torchOn = !torchOn;
+ Log.d(TAG, "Torch: " + torchOn);
+ applyTorchState(btnTorch);
+ }
+
+ private void applyTorchState(ImageButton btnTorch) {
+ if (camera == null) {
+ Log.w(TAG, "Camera NULL for torch");
+ return;
+ }
+
+ CameraInfo info = camera.getCameraInfo();
+ CameraControl control = camera.getCameraControl();
+
+ if (!info.hasFlashUnit()) {
+ Log.w(TAG, "No flash");
+ return;
+ }
+
+ control.enableTorch(torchOn);
+ }
+
+ // ---------- Helpers ----------
+ private ImageButton makeRoundIconButton(int iconRes, int sizePx) {
+ ImageButton b = new ImageButton(this);
+ b.setImageResource(iconRes);
+ b.setBackground(makeRoundRippleBackground());
+ b.setColorFilter(Color.WHITE);
+ b.setLayoutParams(new ViewGroup.LayoutParams(sizePx, sizePx));
+ return b;
+ }
+
+ private Drawable makeRoundRippleBackground() {
+ GradientDrawable circle = new GradientDrawable();
+ circle.setShape(GradientDrawable.OVAL);
+ circle.setColor(Color.parseColor("#66000000"));
+
+ return new RippleDrawable(
+ ColorStateList.valueOf(Color.parseColor("#55FFFFFF")),
+ circle,
+ null
+ );
+ }
+
+ private int dp(int dp) {
+ return (int) TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ dp,
+ getResources().getDisplayMetrics()
+ );
+ }
+
+ // ---------- Permission ----------
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
+ @NonNull int[] grantResults) {
+ Log.d(TAG, "Permission result");
+
+ if (requestCode == REQ_CAMERA) {
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ Log.d(TAG, "Permission OK");
+ startCamera();
+ } else {
+ Log.d(TAG, "Permission denied");
+ finish();
+ }
+ }
+ }
+
+ // ---------- Lifecycle ----------
+ @Override protected void onStart() { super.onStart(); Log.d(TAG, "onStart"); }
+ @Override protected void onResume() { super.onResume(); Log.d(TAG, "onResume"); }
+ @Override protected void onPause() { super.onPause(); Log.d(TAG, "onPause"); }
+ @Override protected void onStop() { super.onStop(); Log.d(TAG, "onStop"); }
+ @Override protected void onDestroy() { super.onDestroy(); Log.d(TAG, "onDestroy"); }
+}
\ No newline at end of file
diff --git a/app_src/main.py b/app_src/main.py
index 6b290cc..1a07de6 100644
--- a/app_src/main.py
+++ b/app_src/main.py
@@ -137,7 +137,7 @@ def set_intent_for_file_operation_class(activity_id, some_int, intent):
# some_int is usually -1 when a file is chosen and 0 when no file is chosen
self.file_operation.hide_spinner()
try:
- print("intent must be before chooser callback",activity_id,some_int,intent)
+ print(f"intent must be before chooser callback activity_id: {activity_id}, some_int: {some_int}, intent:{intent}")
if intent:
# Fix for permission Error when choosing from Internal Storage section Android FileExplorer
self.file_operation.intent = intent
diff --git a/app_src/ui/screens/camera_screen.py b/app_src/ui/screens/camera_screen.py
index f61b6b7..f36fb64 100644
--- a/app_src/ui/screens/camera_screen.py
+++ b/app_src/ui/screens/camera_screen.py
@@ -2,345 +2,138 @@
Front-Facing Camera App - with quality selector + live preview rotation (canvas transform).
Enhanced UI design with modern look & feel.
"""
-import traceback
-from android_notify.config import on_android_platform
-from kivy.app import App
-from kivy.uix.boxlayout import BoxLayout
-from kivy.uix.button import Button
-from kivy.uix.label import Label
-from kivy.uix.camera import Camera
-from kivy.graphics import PushMatrix, PopMatrix, Rotate, Scale, Color, RoundedRectangle
-from kivy.clock import Clock
-from kivy.utils import platform
-from kivy.core.text import LabelBase
-
-import os
+import traceback
from datetime import datetime
+from functools import partial
-# Quality presets: (label, camera_resolution, jpeg_quality, active_color)
-QUALITY_PRESETS = {
- 'Low': ((320, 240), 50, (0.2, 0.6, 0.9, 1)), # blue
- 'Medium': ((640, 480), 80, (0.3, 0.8, 0.4, 1)), # green
- 'High': ((1280, 720), 95, (0.9, 0.5, 0.2, 1)), # orange
-}
-INACTIVE_COLOR = (0.3, 0.3, 0.3, 1)
-BG_COLOR = (0.12, 0.12, 0.12, 1) # dark background
-BUTTON_COLOR = (0.2, 0.2, 0.2, 1)
-CAPTURE_COLOR = (0.9, 0.3, 0.2, 1) # reddish capture
-FLIP_COLOR = (0.3, 0.5, 0.8, 1) # blue flip
-
-# Register a default font (optional, just for consistency)
-#LabelBase.register(name='Roboto', fn_regular='Roboto-Regular.ttf') # falls back to DroidSans
+from kivy.clock import Clock, mainthread
+from android_notify.config import on_android_platform, get_package_name
+from android_notify.internal.java_classes import cast, autoclass
+from ui.widgets.layouts import MyMDScreen
+from utils.logger import app_logger
-class RotatedCamera(Camera):
- """Camera widget that rotates and optionally mirrors the live preview."""
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
- self._rotation_angle = 0
- self._mirror = False
- self._transform_applied = False
+if on_android_platform():
+ from android.permissions import request_permissions, Permission # type: ignore
- def set_preview_transform(self, angle, mirror):
- self._rotation_angle = angle
- self._mirror = mirror
- self._apply_transform()
+from utils.model import get_app
- def _apply_transform(self):
- self.canvas.before.clear()
- self.canvas.after.clear()
+C_DEBUG = 1
- if self._rotation_angle == 0 and not self._mirror:
- return
+C_SCAN_OK = 0
+C_SCAN_KO = 1
+C_SCAN_PC = 2
- with self.canvas.before:
- PushMatrix()
- Rotate(angle=self._rotation_angle, origin=self.center)
- if self._mirror:
- Scale(-1, 1, origin=(self.center_x, self.center_y))
+C_SCAN_QR = 69
+C_SCAN_CAMERA = C_SCAN_QR+1
- with self.canvas.after:
- PopMatrix()
- self._transform_applied = True
- def on_size(self, *args):
- if self._transform_applied:
- self._apply_transform()
+class CameraScreen(MyMDScreen):
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ self.app = get_app()
- def on_pos(self, *args):
- if self._transform_applied:
- self._apply_transform()
+ self._cb_end_scan = None
+ self.name="camera"
+ self._PythonActivity = autoclass("org.kivy.android.PythonActivity")
+ self._Intent = autoclass("android.content.Intent")
+ self._ActivityCamera = autoclass(f"{get_package_name()}.CameraActivity")
+ self._Activity = autoclass("android.app.Activity")
+ self.build()
+ def build(self):
+ print('called binding CameraScreen.build')
+ if on_android_platform():
+ from android import activity # type: ignore
+ activity.bind(on_activity_result=self._on_activity_result)
-class RoundedButton(Button):
- """Button with rounded corners and background color."""
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
- self.background_color = (0, 0, 0, 0) # transparent
- self.background_normal = ''
- self.bind(pos=self._update_canvas, size=self._update_canvas)
- self._update_canvas()
+ def do_photo(self, cb_end_scan):
+ """"""
+ self._cb_end_scan = cb_end_scan
+ if on_android_platform():
+ request_permissions(
+ [Permission.CAMERA, Permission.VIBRATE],
+ self._call_lib_photo
+ )
+ else:
+ #handle pc
+ Clock.schedule_once(partial(self._on_activity_result, C_SCAN_CAMERA, None, None), 0)
- def _update_canvas(self, *args):
- self.canvas.before.clear()
- with self.canvas.before:
- Color(*self.background_color)
- RoundedRectangle(pos=self.pos, size=self.size, radius=[10])
+ @mainthread
+ def _on_activity_result(self, request_code, result_code, intent, *args):
+ """Call from java/android if on android, otherwiise diretly from Clock.schedule_interval
+ *args is used only because schedule_interval pass dt. @https://kivy.org/doc/stable/api-kivy.clock.html
+ """
+ print(f"[PY] activity_result → req={request_code}, result={result_code}, intent={intent}, args:{args}")
+ print(f"(C_SCAN_QR, C_SCAN_CAMERA): {C_SCAN_QR}, {C_SCAN_CAMERA}")
+ # PY] activity_result → req=70, result=0, intent=>, args:()
-from kivymd.uix.boxlayout import MDBoxLayout
-from kivymd.app import MDApp
-class CameraLayout(BoxLayout):
- def __init__(self, **kwargs):
- super().__init__(orientation='vertical', **kwargs)
- self._current_index = 1 # 1 = front on Android
- self._quality = 'Medium'
- try:
- self._build_ui()
- except Exception as e:
- print(e)
- traceback.print_exc()
- Clock.schedule_once(self._start_camera, 1.0)
-
-
- # ------------------------------------------------------------------
- # UI Construction
- # ------------------------------------------------------------------
-
- def _build_ui(self):
- # Main background color
- self.canvas.before.add(Color(*BG_COLOR))
- self.canvas.before.add(RoundedRectangle(size=self.size, pos=self.pos))
- self.bind(size=self._update_bg, pos=self._update_bg)
-
- # Status label at top
- self.status_label = Label(
- text='Starting camera...',
- size_hint=(1, 0.07),
- font_size='14sp',
- color=(0.9, 0.9, 0.9, 1),
- bold=True,
- )
- self.add_widget(self.status_label)
-
- # Camera preview container (with rounded corners border)
- self.camera_container = MDBoxLayout(
- size_hint=(1, 1),
- padding=[0, 0],
- md_bg_color=[1,0,0,2]
- )
- with self.camera_container.canvas.before:
- Color(0.2, 1, 0.2, 1)
- self.camera_border = RoundedRectangle(size=self.camera_container.size,
- pos=self.camera_container.pos,
- radius=[15])
- self.camera_container.bind(size=self._update_camera_border)#, pos=self._update_camera_border)
-
- res = QUALITY_PRESETS[self._quality][0]
- self.camera = RotatedCamera(
- index=self._current_index,
- resolution=res,
- play=False,
- size_hint=(1, 1),
- allow_stretch=True,
- keep_ratio=False
- )
- self.camera_container.add_widget(self.camera)
- self.add_widget(self.camera_container)
-
- # Quality selection row (styled)
- quality_row = BoxLayout(
- size_hint=(1, 0.09),
- spacing=8,
- padding=[12, 4, 12, 4],
- )
- quality_label = Label(
- text='Quality:',
- size_hint_x=0.2,
- font_size='14sp',
- color=(0.9, 0.9, 0.9, 1),
- halign='right',
- valign='middle',
- )
- quality_label.bind(size=quality_label.setter('text_size'))
- quality_row.add_widget(quality_label)
-
- self._quality_btns = {}
- for name in ('Low', 'Medium', 'High'):
- color = QUALITY_PRESETS[name][2] if name == self._quality else INACTIVE_COLOR
- btn = RoundedButton(
- text=name,
- font_size='13sp',
- background_color=color,
- size_hint_x=0.27,
- )
- btn.bind(on_release=self._on_quality_btn)
- quality_row.add_widget(btn)
- self._quality_btns[name] = btn
- self.add_widget(quality_row)
-
- # Action buttons row
- btn_row = BoxLayout(
- size_hint=(1, 0.12),
- spacing=12,
- padding=[20, 8, 20, 8],
- )
- self.capture_btn = RoundedButton(
- text='CAPTURE',
- font_size='16sp',
- bold=True,
- background_color=CAPTURE_COLOR,
- )
- self.capture_btn.bind(on_release=self.capture_photo)
-
- self.flip_btn = RoundedButton(
- text='~FLIP',
- font_size='14sp',
- background_color=FLIP_COLOR,
- )
- self.flip_btn.bind(on_release=self.flip_camera)
-
- btn_row.add_widget(self.capture_btn)
- btn_row.add_widget(self.flip_btn)
- self.add_widget(btn_row)
-
- # Path / info label
- self.path_label = Label(
- text='',
- size_hint=(1, 0.07),
- font_size='11sp',
- color=(0.7, 0.9, 0.7, 1),
- halign='center',
- valign='middle',
- )
- self.path_label.bind(size=self.path_label.setter('text_size'))
- self.add_widget(self.path_label)
-
- def _update_bg(self, *args):
- self.canvas.before.clear()
- with self.canvas.before:
- Color(*BG_COLOR)
- RoundedRectangle(size=self.size, pos=self.pos, radius=[0])
-
- def _update_camera_border(self, *args):
- self.camera_container.canvas.before.clear()
- with self.camera_container.canvas.before:
- Color(0.2, 0.2, 0.2, 1)
- RoundedRectangle(size=self.camera_container.size,
- pos=self.camera_container.pos,
- radius=[15])
-
- # ------------------------------------------------------------------
- # Quality selection
- # ------------------------------------------------------------------
-
- def _on_quality_btn(self, btn):
- name = btn.text
- if name == self._quality:
+ if request_code not in (C_SCAN_QR, C_SCAN_CAMERA):
return
- self._quality = name
- self._refresh_quality_btn_colors()
- self.status_label.text = f'Quality set to {name} - restarting...'
- self._set_buttons_enabled(False)
- self._release_camera()
- Clock.schedule_once(self._reopen_camera, 0.6)
-
- def _refresh_quality_btn_colors(self):
- for name, btn in self._quality_btns.items():
- btn.background_color = QUALITY_PRESETS[name][2] if name == self._quality else INACTIVE_COLOR
-
- def _set_buttons_enabled(self, enabled):
- self.capture_btn.disabled = not enabled
- self.flip_btn.disabled = not enabled
- for btn in self._quality_btns.values():
- btn.disabled = not enabled
-
- # ------------------------------------------------------------------
- # Camera lifecycle
- # ------------------------------------------------------------------
-
- def _start_camera(self, dt=None):
- try:
- res = QUALITY_PRESETS[self._quality][0]
- self.camera.resolution = res
- self.camera.index = self._current_index
- self.camera.play = True
- self._update_preview_transform()
- side = 'Front' if self._current_index == 1 else 'Back'
- self.status_label.text = f'{side} • {self._quality} ({res[0]}x{res[1]})'
- except Exception as e:
- self.status_label.text = f'Error: {e}'
- def _release_camera(self):
- try:
- self.camera.play = False
- if self.camera._camera is not None:
- self.camera._camera.stop()
- if hasattr(self.camera._camera, '_android_camera'):
- ac = self.camera._camera._android_camera
- if ac is not None:
- try:
- ac.stopPreview()
- except Exception:
- pass
- try:
- ac.release()
- except Exception:
- pass
- self.camera._camera._android_camera = None
- self.camera._camera = None
- except Exception as e:
- self.status_label.text = f'Release warning: {e}'
-
- def _reopen_camera(self, dt):
- try:
- res = QUALITY_PRESETS[self._quality][0]
- self.camera.resolution = res
- self.camera.index = self._current_index
- self.camera.play = True
- self._update_preview_transform()
- side = 'Front' if self._current_index == 1 else 'Back'
- self.status_label.text = f'{side} • {self._quality} ({res[0]}x{res[1]})'
- except Exception as e:
- self.status_label.text = f'Error: {e}'
- finally:
- self._set_buttons_enabled(True)
-
- # ------------------------------------------------------------------
- # Preview rotation
- # ------------------------------------------------------------------
-
- def _update_preview_transform(self):
- if self._current_index == 0: # Back camera
- angle = -90
- mirror = False
- else: # Front camera
- angle = 90
- mirror = False
if on_android_platform():
- self.camera.set_preview_transform(angle, mirror)
+ if result_code == self._Activity.RESULT_OK and intent:
+ if request_code == C_SCAN_QR:
+ print('bad still thinks QR Code stuff exists')
+ get_str_data = self._ActivityQR.EXTRA_QR_DATA
+ else:
+ get_str_data = self._ActivityCamera.EXTRA_PHOTO_PATH
+ value = intent.getStringExtra(get_str_data)
+ result = (C_SCAN_OK, value)
+ else:
+ msg = "QrWork::Timeout / Users ends"
+ result = (C_SCAN_KO, msg)
+ else:
+ # handle pc
+ result = "Not android"
+ if callable(self._cb_end_scan):
+ Clock.schedule_once(lambda dt: self._cb_end_scan(result), 0)
+ else:
+ app_logger.error(f"Error: {self._cb_end_scan} not collable. Skip")
- # ------------------------------------------------------------------
- # Flip
- # ------------------------------------------------------------------
- def flip_camera(self, *args):
- self.status_label.text = 'Switching camera...'
- self._set_buttons_enabled(False)
- self._release_camera()
- self._current_index = 0 if self._current_index == 1 else 1
- Clock.schedule_once(self._reopen_camera, 0.6)
- # ------------------------------------------------------------------
- # Capture & Save (original logic unchanged)
- # ------------------------------------------------------------------
+ def _call_lib_photo(self, permissions, grants):
+ """"""
+ print(f"permissions:{permissions}")
+ if on_android_platform():
+ if not all(grants):
+ try:
+ print(f"QrWork::Not all permits: %s" % repr(grants))
+ except Exception as e:
+ print(f'unfamiliar syntax e: {e}')
+ return
+ try:
+ activity = cast("android.app.Activity", self._PythonActivity.mActivity)
+ intent = self._Intent(activity, self._ActivityCamera)
+ activity.startActivityForResult(intent, C_SCAN_CAMERA)
+ except Exception as starting_activity_error:
+ print(f"starting_activity_error; {starting_activity_error}")
+
+ @mainthread
+ def callable_test(self, result):
+ print(f"callable_test from App._ret_scan_load_config {result}")
+ # callable_test from App._ret_scan_load_config (1, 'QrWork::Timeout / Users ends')
+ # callable_test from App._ret_scan_load_config (0, '/data/user/0/org.wally.waller/cache/captures/IMG_20260414_225638.jpg')
+ print(f"got: {result}, [0]: {result[0]}")
+ if not result[0]:
+ complete_file_path = result[1]
+ print(f"using file: {complete_file_path}")
+ try:
+ self.app.file_operation.copy_add([complete_file_path])
+ except Exception as error_adding_image:
+ print(f"error_adding_image:{error_adding_image}")
+ traceback.print_exc()
+ self.manager.current = "thumbs"
def capture_photo(self, *args):
+ print(f"args:{args}")
try:
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
quality_name = self._quality.lower()
@@ -357,109 +150,32 @@ def capture_photo(self, *args):
except Exception as e:
self.status_label.text = f'Capture error: {e}'
- def _fix_rotation_and_compress(self, png_path, timestamp, quality_name):
- jpeg_quality = QUALITY_PRESETS[self._quality][1]
- try:
- from PIL import Image as PILImage
- img = PILImage.open(png_path)
- if platform == 'android':
- if self._current_index == 1:
- img = img.rotate(90, expand=True)
- img = img.transpose(PILImage.FLIP_LEFT_RIGHT)
- else:
- img = img.rotate(-90, expand=True)
- jpg_name = f'capture_{quality_name}_{timestamp}.jpg'
- jpg_path = self._save_path(jpg_name)
- img.convert('RGB').save(jpg_path, 'JPEG', quality=jpeg_quality, optimize=True)
- os.remove(png_path)
- return jpg_path
- except ImportError:
- return self._rotate_png_stdlib(png_path, timestamp, quality_name)
-
- def _rotate_png_stdlib(self, png_path, timestamp, quality_name):
- import struct, zlib
- with open(png_path, 'rb') as f:
- data = f.read()
- def read_chunks(data):
- pos = 8
- chunks = {}
- while pos < len(data):
- length = struct.unpack('>I', data[pos:pos+4])[0]
- name = data[pos+4:pos+8].decode('ascii')
- chunk_data = data[pos+8:pos+8+length]
- chunks.setdefault(name, []).append(chunk_data)
- pos += 12 + length
- return chunks
- chunks = read_chunks(data)
- ihdr = chunks['IHDR'][0]
- w, h = struct.unpack('>II', ihdr[:8])
- bit_depth, color_type = ihdr[8], ihdr[9]
- if bit_depth != 8 or color_type not in (2, 6):
- return png_path
- channels = 4 if color_type == 6 else 3
- raw = zlib.decompress(b''.join(chunks['IDAT']))
- rows = []
- stride = 1 + w * channels
- for y in range(h):
- row_bytes = raw[y * stride + 1: (y + 1) * stride]
- rows.append([tuple(row_bytes[x*channels:(x+1)*channels]) for x in range(w)])
- if self._current_index == 1:
- new_w, new_h = h, w
- new_rows = [[rows[new_h - 1 - x][y] for x in range(new_h)] for y in range(new_w)]
- new_rows = [row[::-1] for row in new_rows]
- else:
- new_w, new_h = h, w
- new_rows = [[rows[x][new_w - 1 - y] for x in range(new_h)] for y in range(new_w)]
- def make_png(rows, width, height, channels):
- sig = b'\x89PNG\r\n\x1a\n'
- def chunk(name, data):
- c = name.encode() + data
- return struct.pack('>I', len(data)) + c + struct.pack('>I', zlib.crc32(c) & 0xffffffff)
- ct = 6 if channels == 4 else 2
- ihdr_new = struct.pack('>IIBBBBB', width, height, 8, ct, 0, 0, 0)
- raw_rows = b''
- for row in rows:
- raw_rows += b'\x00'
- for px in row:
- raw_rows += bytes(px)
- idat = zlib.compress(raw_rows, 6)
- return sig + chunk('IHDR', ihdr_new) + chunk('IDAT', idat) + chunk('IEND', b'')
- rotated_name = f'capture_{quality_name}_{timestamp}_r.png'
- rotated_path = self._save_path(rotated_name)
- with open(rotated_path, 'wb') as f:
- f.write(make_png(new_rows, new_w, new_h, channels))
- os.remove(png_path)
- return rotated_path
-
- def _save_path(self, filename):
- if platform == 'android':
- base = '/storage/emulated/0/Pictures/KivyCam'
- else:
- base = os.path.join(os.path.expanduser('~'), 'Pictures', 'KivyCam')
- os.makedirs(base, exist_ok=True)
- return os.path.join(base, filename)
+ def start_camera(self):
+ print("from start camera, start_camera")
+ self.do_photo(self.callable_test)
- def _scan_gallery(self, filepath):
- if platform != 'android':
- return
- try:
- from jnius import autoclass
- PythonActivity = autoclass('org.kivy.android.PythonActivity')
- MediaScannerConnection = autoclass('android.media.MediaScannerConnection')
- MediaScannerConnection.scanFile(PythonActivity.mActivity, [filepath], None, None)
- except Exception:
- pass
+ def go_to_gallery_screen(self,*_):
+ self.release_camera()
+ setattr(self.manager, "current", "thumbs")
-class FrontCameraApp(MDApp):
- def build(self):
- self.title = ' Front Camera'
- return CameraLayout()
+if __name__ == '__main__':
+ from kivymd.app import MDApp
- def on_stop(self):
- if hasattr(self.root, '_release_camera'):
- self.root._release_camera()
+
+ class FrontCameraApp(MDApp):
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ self.c=CameraScreen()
+
+ def build(self):
+ self.title = ' Front Camera'
+ Clock.schedule_once(lambda x: self.on_stop(),5)
+ return self.c
+
+ def on_stop(self):
+ if hasattr(self.c, 'release_camera'):
+ self.c.release_camera()
-if __name__ == '__main__':
FrontCameraApp().run()
\ No newline at end of file
diff --git a/app_src/ui/screens/gallery_screen.kv b/app_src/ui/screens/gallery_screen.kv
index 4c47ff3..2b8c774 100644
--- a/app_src/ui/screens/gallery_screen.kv
+++ b/app_src/ui/screens/gallery_screen.kv
@@ -3,6 +3,7 @@
#:import BUTTON_BG_DARK utils.constants.BUTTON_BG_DARK
#:import THEME_PRIMARY_COLOR utils.constants.THEME_PRIMARY_COLOR
#:import GalleryTabs utils.model.GalleryTabs
+#:import Clock kivy.clock
@@ -248,15 +249,90 @@
height:self.minimum_height
padding:[0, 0, 0, dp(120)]
+ FabButtonLayout:
+ id: fab_btn_widget
+ md_bg_color: [0,0,0,0]
+# md_bg_color: [.8,.8,.8,.4]
- MDFabButton:
- icon:"plus"
- on_release: root.open_file_chooser()
- pos_hint: {"right": .9,"center_y":.15}
- theme_font_size: "Custom"
- font_size: sp(30)
- theme_bg_color: "Custom"
- # md_bg_color: get_color_from_hex("#1A1B1B") if app.device_theme == "light" else "white"
- # text_color: [0.8,0.8, 0.8, 1] if app.device_theme == "light" else [.3,.3,.4,1]
- md_bg_color: get_color_from_hex("#262C3A")
- text_color: get_color_from_hex("#98F1DD")
+# size_hint:[None,None]
+# size: [root.height,root.width]
+# adaptive_size: 1
+# pos_hint: {"right": .9,"center_y":.15}
+ Column:
+ id: actions_container_widget
+# md_bg_color: [0,.4,.3,1]
+ adaptive_size: 1
+ opacity:0
+# pos:[0,0]
+ pos_hint: {"right": .93}
+# pos_hint: {"right": .93,"center_y":.15}
+ spacing: dp(10)
+ MDButton:
+ id: add_with_phone_camera_btn
+ on_release: root.open_camera()
+# on_release: lambda *_: Clock.schedule_once(lambda *_:setattr(root.manager,"current","camera"))
+ theme_bg_color: "Custom"
+ md_bg_color: get_color_from_hex("#262C3A")
+ radius: [15]
+# theme_width:"Custom"
+# theme_height:"Custom"
+# size:[135,50]
+
+ MDButtonText:
+ text: "Camera"
+ theme_text_color: "Custom"
+ text_color: get_color_from_hex("#98F1DD")
+# theme_font_size: "Custom"
+# padding: [10,0,0,0]
+# font_size: sp(18)
+# adaptive_size:1
+ bold:1
+ MDButtonIcon:
+ icon:"camera-plus"
+ text_color: get_color_from_hex("#98F1DD")
+# theme_font_size: "Custom"
+# font_size: sp(30)
+ MDButton:
+ id: add_with_phone_gallery_btn
+ on_release: root.open_file_chooser()
+ theme_bg_color: "Custom"
+ md_bg_color: get_color_from_hex("#262C3A")
+ radius: [15]
+ MDButtonText:
+ text: "Photos"
+ theme_text_color: "Custom"
+ text_color: get_color_from_hex("#98F1DD")
+# theme_font_size: "Custom"
+# font_size: sp(18)
+# adaptive_size:1
+# padding: [10,0,0,0]
+ bold:1
+ MDButtonIcon:
+ icon:"image-plus"
+ text_color: get_color_from_hex("#98F1DD")
+# theme_font_size: "Custom"
+# font_size: sp(30)
+
+ MDFabButton:
+ id: fab_button
+ icon:"plus"
+ on_release: root.toggle_choice_popup(self,fab_btn_widget,add_with_phone_gallery_btn,add_with_phone_camera_btn)
+ theme_font_size: "Custom"
+ font_size: sp(30)
+ theme_bg_color: "Custom"
+ md_bg_color: get_color_from_hex("#262C3A")
+ text_color: get_color_from_hex("#98F1DD")
+# pos_hint: {"right": .9,"center_y":.16}
+ pos_hint: {"right": .9,"center_y":.2}
+
+#MDFabButton:
+# icon:"plus"
+# on_release: root.open_file_chooser()
+# pos_hint: {"right": .9,"center_y":.15}
+# theme_font_size: "Custom"
+# font_size: sp(30)
+# theme_bg_color: "Custom"
+# # md_bg_color: get_color_from_hex("#1A1B1B") if app.device_theme == "light" else "white"
+# # text_color: [0.8,0.8, 0.8, 1] if app.device_theme == "light" else [.3,.3,.4,1]
+# md_bg_color:
+# text_color: get_color_from_hex("#98F1DD")
\ No newline at end of file
diff --git a/app_src/ui/screens/gallery_screen.py b/app_src/ui/screens/gallery_screen.py
index 1592854..38454a5 100644
--- a/app_src/ui/screens/gallery_screen.py
+++ b/app_src/ui/screens/gallery_screen.py
@@ -2,6 +2,8 @@
from datetime import datetime
from pathlib import Path
+from kivymd.uix.floatlayout import MDFloatLayout
+
from android_notify.config import on_android_platform
from kivy.clock import Clock
from kivy.core.window import Window
@@ -15,7 +17,7 @@
from kivymd.app import MDApp
from kivymd.uix.boxlayout import MDBoxLayout
-from kivymd.uix.button import MDIconButton
+from kivymd.uix.button import MDIconButton, MDFabButton
from kivymd.uix.gridlayout import MDGridLayout
from kivymd.uix.label import MDLabel
from kivymd.uix.relativelayout import MDRelativeLayout
@@ -298,9 +300,8 @@ def change_preview_img_size(self,widget,number_of_cols):
class GalleryScreen(MyMDScreen):
current_tab = StringProperty(GalleryTabs.BOTH.value)
wallpapers = ListProperty([])
-
+ showing_action_btns=BooleanProperty(False)
def __init__(self, **kwargs):
-
super().__init__(**kwargs)
self.app = get_app()
# self.app.device_theme = "light"
@@ -354,6 +355,7 @@ def run_widgets_creation( *args):
Clock.schedule_once(run_widgets_creation)
def open_file_chooser(self, *_):
+ self.close_choice_popup()
# file_operation = FileOperation(self.update_thumbnails_method)
# if platform == 'android':
# from android import activity # type: ignore
@@ -373,6 +375,7 @@ def open_file_chooser(self, *_):
#
# activity.bind(on_activity_result=test) # handling image with no permission
self.app.file_operation.show_spinner()
+ self.app.bottom_bar.hide(animation=False)
def show_chooser(dt=None):
@@ -643,6 +646,51 @@ def set_widget_left_and_right_padding(self,left_padding, right_padding,rotation)
self.ids.head_section.size_hint_x=1
self.ids.head_section.padding= [dp(0), dp(self.status_bar_height), dp(10), dp(0)]
+ def toggle_choice_popup(self,button_instance,fab_button_layout,add_with_phone_gallery_btn,add_with_phone_camera_btn):
+ fab_button_layout.btns = [button_instance,add_with_phone_gallery_btn,add_with_phone_camera_btn]
+
+ if self.showing_action_btns:
+ self.close_choice_popup()
+ else:
+ self.open_choice_popup()
+
+ def open_choice_popup(self):
+ fab_button_layout = self.ids.fab_btn_widget
+ button_instance = self.ids.fab_button
+ self.ids.fab_btn_widget.md_bg_color = [.2, .2, .2, .4]
+
+ y=button_instance.pos[1]
+ height=button_instance.height
+ self.ids.actions_container_widget.y = height+y + 10
+ self.ids.actions_container_widget.opacity = 1
+ button_instance.icon = "close"
+ fab_button_layout.showing = True
+ self.showing_action_btns= True
+ if hasattr(self.app, "bottom_bar"):
+ self.app.bottom_bar.hide(animation=False)
+ else:
+ app_logger.warning("No bottom nav bar to hide, You're on Hot Reload ")
+
+ def close_choice_popup(self):
+ fab_button_layout=self.ids.fab_btn_widget
+ button_instance=self.ids.fab_button
+ self.ids.fab_btn_widget.md_bg_color = [0, 0, 0, 0]
+ self.ids.actions_container_widget.y=-100
+ self.ids.actions_container_widget.opacity = 0
+ button_instance.icon = "plus"
+ fab_button_layout.showing = False
+ self.showing_action_btns= False
+ if hasattr(self.app, "bottom_bar"):
+ self.app.bottom_bar.show(animation=False)
+ else:
+ app_logger.warning("No bottom nav bar to show, You're on Hot Reload ")
+
+ def open_camera(self,*_):
+ Clock.schedule_once(lambda *_:setattr(self.manager, "current", "camera"))
+ self.manager.current="camera"
+ self.manager.current_screen.start_camera()
+ # self.app.sm.current_screen._start_camera()
+
def change_amount_of_columns(self,chosen_cols):
for each_tab,data in self.tab_instances.items():
tab_container = data["widget"]
@@ -650,6 +698,54 @@ def change_amount_of_columns(self,chosen_cols):
for each in tab_container.walk():
if isinstance(each, DateGroupLayout):
each.change_preview_img_size(None, chosen_cols)
+class FabButtonLayout(MDFloatLayout):
+ btns=ListProperty()
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ self.showing=False
+ Clock.schedule_once(lambda dt:self.adjust_children(),1)
+ Clock.schedule_once(lambda dt:self.adjust_children(),4)
+
+ def on_touch_up(self, touch):
+ widget_on_floating_screen = False
+ for each_btn in self.btns:
+ widget_on_floating_screen = each_btn.collide_point(*touch.pos)
+ if widget_on_floating_screen:
+ break
+
+ if self.showing and not widget_on_floating_screen:
+ return True
+ return super(FabButtonLayout, self).on_touch_up(touch)
+
+ def on_touch_down(self, touch):
+ widget_on_floating_screen = False
+ for each_btn in self.btns:
+ widget_on_floating_screen = each_btn.collide_point(*touch.pos)
+ if widget_on_floating_screen:
+ break
+ if self.showing and not widget_on_floating_screen:
+ return True
+
+ # if self.collide_point(*touch.pos):
+ # print("Widget clicked!")
+ # # Returning True consumes the touch and stops propagation
+ # return True
+ return super(FabButtonLayout, self).on_touch_down(touch)
+
+ def adjust_children(self):
+ floating_button = None
+ other_btns_case = None
+ for each_widget in self.children:
+ if isinstance(each_widget, MDFabButton):
+ floating_button = each_widget
+ if isinstance(each_widget,Column):
+ other_btns_case = each_widget
+ if not other_btns_case or not floating_button:
+ app_logger.error(f"Didn't find any children for other_btns_case:{other_btns_case}, floating_button:{floating_button}")
+ return
+ y=floating_button.pos[1]
+ height=floating_button.height
+ other_btns_case.y = height+y + 20
if __name__ == "__main__":
diff --git a/app_src/ui/screens/manager.py b/app_src/ui/screens/manager.py
index a238463..fa78901 100644
--- a/app_src/ui/screens/manager.py
+++ b/app_src/ui/screens/manager.py
@@ -18,6 +18,7 @@
from ui.screens.welcome_screen import WelcomeScreen
from ui.screens.logs_screen import LogsScreen
from ui.screens.download_apk_screen import DownloadApkScreen
+from ui.screens.camera_screen import CameraScreen
@@ -29,12 +30,14 @@ def __init__(self, **kwargs):
self.welcome_screen = WelcomeScreen()
self.gallery_screen = GalleryScreen()
self.full_screen = FullscreenScreen()
+ self.camera_screen = CameraScreen()
self.settings_screen = SettingsScreen()
self.log_screen = LogsScreen()
self.download_apk_screen = DownloadApkScreen()
self.add_widget(self.gallery_screen)
self.add_widget(self.full_screen)
+ self.add_widget(self.camera_screen)
self.add_widget(self.settings_screen)
self.add_widget(self.welcome_screen)
self.add_widget(self.log_screen)
@@ -67,7 +70,7 @@ def __register_rotate_listener(self):
def on_current(self,*args):
screen_name = args[1]
- is_fullscreen = screen_name in ["welcome","fullscreen","logs","update_screen"]
+ is_fullscreen = screen_name in ["welcome","fullscreen","logs","update_screen","camera"]
if is_fullscreen and self.app.bottom_bar:
self.app.bottom_bar.hide(animation=False)
elif self.app.bottom_bar:
diff --git a/app_src/ui/widgets/layouts.py b/app_src/ui/widgets/layouts.py
index 57290a0..8749ecb 100644
--- a/app_src/ui/widgets/layouts.py
+++ b/app_src/ui/widgets/layouts.py
@@ -384,3 +384,6 @@ def _update_spinner_pos(self, *args):
def remove(self,dt=None):
if self.parent:
self.parent.remove_widget(self) # Hides the spinner by removing it
+ app_logger.warning("Removing Loading layout from parent")
+ else:
+ app_logger.warning("Loading layout has no parent")
\ No newline at end of file
diff --git a/app_src/utils/image_operations.py b/app_src/utils/image_operations.py
index 72c4c29..51ec819 100644
--- a/app_src/utils/image_operations.py
+++ b/app_src/utils/image_operations.py
@@ -48,7 +48,7 @@ def __copy_add(self, files):
# print('entered',files)
if not files:
- Clock.schedule_once(lambda dt: self.load_saved(has_files=False))# for hiding bottom nav
+ Clock.schedule_once(lambda dt: self.load_saved(has_files=False))# for showing bottom nav
self.hide_spinner()
return
new_images = []
diff --git a/app_src/utils/model.py b/app_src/utils/model.py
index e89fe9f..ce31d5a 100644
--- a/app_src/utils/model.py
+++ b/app_src/utils/model.py
@@ -27,6 +27,10 @@ def _camera_pressed(self, *args) -> None: ...
def _settings_pressed(self, *args) -> None: ...
def color_tab_buttons(self, current: Literal["thumbs", "fullscreen", "settings", "welcome", "logs"]) -> None: ...
+class MyBtmSheetModel:
+ def hide(self,animation=True) -> None: ...
+ def show(self,animation=True) -> None: ...
+
class MyScreenManagerModel:
# screens
gallery_screen: "GalleryScreen"
@@ -61,6 +65,7 @@ class WallpaperCarouselAppModel:
root_layout: object
sm: MyScreenManagerModel
bottom_bar: BottomButtonBarModel
+ btm_sheet: MyBtmSheetModel
def bind(self, **kwargs: Callable[..., Any]) -> None: ...
def monitor_dark_and_light_device_change(self) -> Literal["light","dark"]: ...
diff --git a/buildozer.spec b/buildozer.spec
index c69f704..199f56b 100644
--- a/buildozer.spec
+++ b/buildozer.spec
@@ -31,12 +31,15 @@ osx.kivy_version = 1.9.1
fullscreen = 0
orientation = all
-android.permissions = RECEIVE_BOOT_COMPLETED, INTERNET, VIBRATE, USE_EXACT_ALARM, SCHEDULE_EXACT_ALARM, FOREGROUND_SERVICE, FOREGROUND_SERVICE_SPECIAL_USE, POST_NOTIFICATIONS, SET_WALLPAPER, READ_MEDIA_IMAGES, (name=android.permission.READ_EXTERNAL_STORAGE;maxSdkVersion=32), (name=android.permission.WRITE_EXTERNAL_STORAGE;maxSdkVersion=28), REQUEST_INSTALL_PACKAGES
-#android.permissions = RECEIVE_BOOT_COMPLETED, INTERNET, VIBRATE, USE_EXACT_ALARM, SCHEDULE_EXACT_ALARM, FOREGROUND_SERVICE, FOREGROUND_SERVICE_DATA_SYNC, POST_NOTIFICATIONS, SET_WALLPAPER, READ_MEDIA_IMAGES, (name=android.permission.READ_EXTERNAL_STORAGE;maxSdkVersion=32), (name=android.permission.WRITE_EXTERNAL_STORAGE;maxSdkVersion=28)
+android.permissions = RECEIVE_BOOT_COMPLETED, INTERNET, VIBRATE, USE_EXACT_ALARM, SCHEDULE_EXACT_ALARM, FOREGROUND_SERVICE, FOREGROUND_SERVICE_SPECIAL_USE, POST_NOTIFICATIONS, SET_WALLPAPER, READ_MEDIA_IMAGES, (name=android.permission.READ_EXTERNAL_STORAGE;maxSdkVersion=32), (name=android.permission.WRITE_EXTERNAL_STORAGE;maxSdkVersion=28), REQUEST_INSTALL_PACKAGES, FOREGROUND_SERVICE_DATA_SYNC, CAMERA
+#android.permissions = RECEIVE_BOOT_COMPLETED, INTERNET, VIBRATE, USE_EXACT_ALARM, SCHEDULE_EXACT_ALARM, FOREGROUND_SERVICE, FOREGROUND_SERVICE_SPECIAL_USE, POST_NOTIFICATIONS, SET_WALLPAPER, READ_MEDIA_IMAGES, (name=android.permission.READ_EXTERNAL_STORAGE;maxSdkVersion=32), (name=android.permission.WRITE_EXTERNAL_STORAGE;maxSdkVersion=28), REQUEST_INSTALL_PACKAGES
+
android.add_src = %(source.dir)s/android/src
android.add_resources = %(source.dir)s/android/res
-android.gradle_dependencies = androidx.core:core-ktx:1.12.0, com.google.android.material:material:1.12.0
-#android.gradle_dependencies = com.google.android.material:material:1.6.0, androidx.core:core-ktx:1.15.0, androidx.core:core:1.6.0, androidx.work:work-runtime:2.9.0
+android.gradle_dependencies = androidx.core:core-ktx:1.12.0, com.google.android.material:material:1.12.0,androidx.camera:camera-core:1.3.4,androidx.camera:camera-camera2:1.3.4,androidx.camera:camera-lifecycle:1.3.4,androidx.camera:camera-view:1.3.4,androidx.appcompat:appcompat:1.6.1,androidx.activity:activity:1.8.2
+
+# android.gradle_dependencies = androidx.core:core-ktx:1.12.0, com.google.android.material:material:1.12.0,androidx.camera:camera-core:1.3.4,androidx.camera:camera-camera2:1.3.4,androidx.camera:camera-lifecycle:1.3.4,androidx.camera:camera-view:1.3.4,androidx.core:core-ktx:1.12.0,androidx.appcompat:appcompat:1.6.1,androidx.activity:activity:1.8.2,com.google.guava:guava:31.1-android
+#android.gradle_dependencies = androidx.core:core-ktx:1.12.0, com.google.android.material:material:1.12.0
android.enable_androidx = True
p4a.hook = %(source.dir)s/android/p4a/hook.py
@@ -47,7 +50,8 @@ android.api = 35
p4a.branch = develop
-android.archs = arm64-v8a, armeabi-v7a
+android.archs = arm64-v8a
+# android.archs = arm64-v8a, armeabi-v7a
android.allow_backup = True