Shoot from the volume keys, while the camera is up
This commit is contained in:
@@ -31,6 +31,7 @@ import { LITE_FAVORITE_LIMIT, LITE_MARK, LITE_RECIPE_LIMIT, proLookInUse } from
|
||||
import { initProUnlock, useProUnlock } from './src/utils/proUnlock';
|
||||
import UltraWide from './modules/recipescam-ultrawide';
|
||||
import RecipescamExport from './modules/recipescam-export';
|
||||
import VolumeShutter from './modules/recipescam-volume';
|
||||
|
||||
// @ts-ignore
|
||||
import './global.css';
|
||||
@@ -1532,6 +1533,24 @@ export default function App() {
|
||||
}
|
||||
};
|
||||
|
||||
// Volume keys as a shutter release. `handleCapture` is rebuilt every render,
|
||||
// so the listener is bound once and reaches the current one through a ref.
|
||||
const captureFromKeyRef = useRef(handleCapture);
|
||||
captureFromKeyRef.current = handleCapture;
|
||||
useEffect(() => {
|
||||
const sub = VolumeShutter.addListener('onShutter', () => {
|
||||
void captureFromKeyRef.current();
|
||||
});
|
||||
return () => sub.remove();
|
||||
}, []);
|
||||
// Only the camera screen claims the keys; everywhere else (the library)
|
||||
// they stay volume keys.
|
||||
// ponytail: keyed off the mode alone, so a press behind a modal still
|
||||
// takes a frame - add the overlay flags here if that ever bites.
|
||||
useEffect(() => {
|
||||
VolumeShutter.setEnabled(mode === 'camera');
|
||||
}, [mode]);
|
||||
|
||||
// Held-shutter burst: press and hold the shutter, frames keep coming until
|
||||
// the finger lifts. Every frame is a normal capture (GPS resolve, RAW rules,
|
||||
// background render enqueue), so a burst photo is indistinguishable from a
|
||||
|
||||
@@ -3,12 +3,15 @@ package com.locphamtran.recipescamera
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
|
||||
import com.facebook.react.ReactActivity
|
||||
import com.facebook.react.ReactActivityDelegate
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
|
||||
import com.facebook.react.defaults.DefaultReactActivityDelegate
|
||||
|
||||
import com.locphamtran.recipescamera.volume.VolumeShutterModule
|
||||
|
||||
import expo.modules.ReactActivityDelegateWrapper
|
||||
|
||||
class MainActivity : ReactActivity() {
|
||||
@@ -31,6 +34,17 @@ class MainActivity : ReactActivity() {
|
||||
super.onNewIntent(intent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Volume keys double as a shutter release while the camera screen is up.
|
||||
* This is the only place that runs before the window turns them into a
|
||||
* stream-volume change, so the module gets first refusal and the keys it
|
||||
* does not claim (everything, outside the camera) fall through to the
|
||||
* default handling unchanged.
|
||||
*/
|
||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||
return VolumeShutterModule.onKeyEvent(event) || super.dispatchKeyEvent(event)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the main component registered from JavaScript. This is used to schedule
|
||||
* rendering of the component.
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
plugins {
|
||||
id 'com.android.library'
|
||||
id 'expo-module-gradle-plugin'
|
||||
}
|
||||
|
||||
group = 'com.locphamtran.recipescamera'
|
||||
version = '0.1.0'
|
||||
|
||||
android {
|
||||
namespace "com.locphamtran.recipescamera.volume"
|
||||
defaultConfig {
|
||||
versionCode 1
|
||||
versionName "0.1.0"
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package com.locphamtran.recipescamera.volume
|
||||
|
||||
import android.view.KeyEvent
|
||||
import expo.modules.kotlin.modules.Module
|
||||
import expo.modules.kotlin.modules.ModuleDefinition
|
||||
|
||||
/**
|
||||
* Volume keys as a shutter release.
|
||||
*
|
||||
* Android exposes no key stream to JS, and the volume keys never reach a view -
|
||||
* the window (PhoneWindow.OnKeyListener) turns them into a stream-volume change
|
||||
* before any view sees them. The one place that runs first is the activity's
|
||||
* dispatchKeyEvent, so MainActivity hands every key here and only passes on
|
||||
* what comes back false.
|
||||
*
|
||||
* Enabled from JS only while the camera screen is up: with the flag off the keys
|
||||
* go straight back to the window, so volume still works in the library.
|
||||
*/
|
||||
class VolumeShutterModule : Module() {
|
||||
override fun definition() = ModuleDefinition {
|
||||
Name("RecipesCamVolumeShutter")
|
||||
|
||||
Events("onShutter")
|
||||
|
||||
// The activity is the one dispatching, but it does not own the module -
|
||||
// publish the single live instance so it can be reached from there.
|
||||
OnCreate { instance = this@VolumeShutterModule }
|
||||
OnDestroy { if (instance === this@VolumeShutterModule) instance = null }
|
||||
|
||||
Function("setEnabled") { enabled: Boolean -> VolumeShutterModule.enabled = enabled }
|
||||
}
|
||||
|
||||
companion object {
|
||||
@Volatile private var instance: VolumeShutterModule? = null
|
||||
|
||||
@Volatile private var enabled = false
|
||||
|
||||
/**
|
||||
* Every key event of the activity goes through here first. Returns true when
|
||||
* the key is ours and has been consumed - that consumption is what keeps the
|
||||
* press from also changing the media volume, the way a stock camera app
|
||||
* behaves.
|
||||
*
|
||||
* Only ACTION_DOWN with repeatCount 0 fires: the UP and the auto-repeat of a
|
||||
* held button are still swallowed (so holding it does not leak a volume
|
||||
* change on release) but produce no second frame.
|
||||
*/
|
||||
fun onKeyEvent(event: KeyEvent): Boolean {
|
||||
if (!enabled) return false
|
||||
if (event.keyCode != KeyEvent.KEYCODE_VOLUME_UP && event.keyCode != KeyEvent.KEYCODE_VOLUME_DOWN) {
|
||||
return false
|
||||
}
|
||||
if (event.action == KeyEvent.ACTION_DOWN && event.repeatCount == 0) {
|
||||
instance?.sendEvent("onShutter")
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"platforms": ["android"],
|
||||
"android": {
|
||||
"modules": [
|
||||
"com.locphamtran.recipescamera.volume.VolumeShutterModule"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { requireNativeModule } from 'expo-modules-core';
|
||||
|
||||
export interface VolumeShutterModule {
|
||||
/**
|
||||
* Claims the volume keys while true; while false every key is handed back to
|
||||
* the window, so they stay volume keys everywhere but the camera screen.
|
||||
*/
|
||||
setEnabled(enabled: boolean): void;
|
||||
/** Fired once per press (a held key does not auto-repeat). */
|
||||
addListener(event: 'onShutter', listener: () => void): { remove(): void };
|
||||
}
|
||||
|
||||
export default requireNativeModule<VolumeShutterModule>('RecipesCamVolumeShutter');
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "recipescam-volume",
|
||||
"version": "0.1.0",
|
||||
"description": "Volume keys as a shutter release for Recipescam.",
|
||||
"main": "index.ts",
|
||||
"license": "MIT",
|
||||
"private": true
|
||||
}
|
||||
Reference in New Issue
Block a user