From df4a44404176106f4525f8c68f15b085fc33a30f Mon Sep 17 00:00:00 2001 From: osakila Date: Thu, 6 Jun 2024 09:21:47 +0900 Subject: [PATCH 01/13] Add burst shooting & unknown to CaptureStatusEnum --- .github/workflows/test-kmp.yaml | 2 +- demos/demo-react-native/src/ListPhotos.tsx | 2 +- demos/demo-react-native/src/MainMenu.tsx | 2 +- demos/demo-react-native/src/TakePhoto.tsx | 2 +- .../src/modules/theta-client.ts | 1 + demos/demo-react-native/tsconfig.json | 1 + flutter/lib/state/capture_status.dart | 48 ++++ flutter/lib/state/importer.dart | 6 + flutter/lib/state/theta_state.dart | 199 +++++++++++++- flutter/lib/theta_client_flutter.dart | 243 +----------------- .../theta_client_flutter_method_channel.dart | 1 - ...eta_client_flutter_platform_interface.dart | 1 - flutter/lib/utils/convert_utils.dart | 3 - flutter/test/enum_name_test.dart | 20 -- flutter/test/state/capture_status_test.dart | 30 +++ flutter/test/theta_client_flutter_test.dart | 2 - .../ricoh360/thetaclient/ThetaRepository.kt | 16 +- .../capture/CaptureStatusMonitor.kt | 4 +- .../thetaclient/transferred/stateApi.kt | 60 +++-- .../repository/GetThetaStateTest.kt | 10 + .../thetaclient/transferred/StateApiTest.kt | 63 +++++ react-native/package.json | 2 +- .../theta-state/capture-status.test.ts | 29 +++ .../theta-state/capture-status.ts | 27 ++ .../src/theta-repository/theta-state/index.ts | 1 + .../theta-state/theta-state.ts | 25 +- react-native/verification-tool/src/App.tsx | 2 +- .../capture-common-options.tsx | 2 +- .../list-files-view/list-files-view.tsx | 2 +- .../auto-bracket/auto-bracket-edit.tsx | 2 +- .../burst-option/burst-option-edit.tsx | 2 +- .../options/gps-info/gps-info-edit.tsx | 2 +- .../src/components/options/index.ts | 2 +- .../options/number-edit/number-edit.tsx | 2 +- .../options/string-edit/string-edit.tsx | 2 +- .../options/time-shift/time-shift-edit.tsx | 5 +- .../top-bottom-correction-rotation-edit.tsx | 2 +- .../src/modules/theta-client.ts | 1 + .../burst-capture-screen.tsx | 2 +- .../commands-screen/commands-screen.tsx | 2 +- .../composite-interval-capture-screen.tsx | 2 +- .../continuous-capture-screen.tsx | 2 +- .../delete-files-screen.tsx | 2 +- .../get-info-screen/get-info-screen.tsx | 2 +- .../get-metadata-screen.tsx | 6 +- .../limitless-interval-capture-screen.tsx | 2 +- .../list-files-screen/list-files-screen.tsx | 2 +- .../live-preview-screen.tsx | 2 +- .../src/screen/menu-screen/menu-screen.tsx | 2 +- .../multi-bracket-capture-screen.tsx | 2 +- .../screen/options-screen/options-screen.tsx | 2 +- .../photo-capture-screen.tsx | 2 +- ...ount-specified-interval-capture-screen.tsx | 2 +- .../time-shift-capture-screen.tsx | 2 +- .../video-capture-screen.tsx | 2 +- .../video-convert-screen.tsx | 2 +- 56 files changed, 518 insertions(+), 348 deletions(-) create mode 100644 demos/demo-react-native/src/modules/theta-client.ts create mode 100644 flutter/lib/state/capture_status.dart create mode 100644 flutter/lib/state/importer.dart create mode 100644 flutter/test/state/capture_status_test.dart create mode 100644 kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/transferred/StateApiTest.kt create mode 100644 react-native/src/__tests__/theta-repository/theta-state/capture-status.test.ts create mode 100644 react-native/src/theta-repository/theta-state/capture-status.ts create mode 100644 react-native/verification-tool/src/modules/theta-client.ts diff --git a/.github/workflows/test-kmp.yaml b/.github/workflows/test-kmp.yaml index 7c677cf1ff..210530d597 100644 --- a/.github/workflows/test-kmp.yaml +++ b/.github/workflows/test-kmp.yaml @@ -20,7 +20,7 @@ jobs: - name: Build and Test with Gradle uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1 with: - arguments: testReleaseUnitTest + arguments: testReleaseUnitTest --info - name: Archive code coverage results if: always() uses: actions/upload-artifact@v1 diff --git a/demos/demo-react-native/src/ListPhotos.tsx b/demos/demo-react-native/src/ListPhotos.tsx index 515d3bc814..0777e90f3a 100644 --- a/demos/demo-react-native/src/ListPhotos.tsx +++ b/demos/demo-react-native/src/ListPhotos.tsx @@ -16,7 +16,7 @@ import { getThetaInfo, FileTypeEnum, FileInfo, -} from 'theta-client-react-native'; +} from './modules/theta-client'; const listPhotos = async () => { const {fileList} = await listFiles(FileTypeEnum.IMAGE, 0, 1000); diff --git a/demos/demo-react-native/src/MainMenu.tsx b/demos/demo-react-native/src/MainMenu.tsx index 00ea3015a1..e2528b96a1 100644 --- a/demos/demo-react-native/src/MainMenu.tsx +++ b/demos/demo-react-native/src/MainMenu.tsx @@ -9,7 +9,7 @@ import { } from 'react-native'; import {SafeAreaView} from 'react-native-safe-area-context'; import styles from './Styles'; -import {initialize} from 'theta-client-react-native'; +import {initialize} from './modules/theta-client'; const MainMenu = ({navigation}) => { const goTake = () => { diff --git a/demos/demo-react-native/src/TakePhoto.tsx b/demos/demo-react-native/src/TakePhoto.tsx index df02888ae1..3227e04d17 100644 --- a/demos/demo-react-native/src/TakePhoto.tsx +++ b/demos/demo-react-native/src/TakePhoto.tsx @@ -14,7 +14,7 @@ import { getPhotoCaptureBuilder, THETA_EVENT_NAME, isInitialized, -} from 'theta-client-react-native'; +} from './modules/theta-client'; import {useIsFocused} from '@react-navigation/native'; import WebView from 'react-native-webview'; diff --git a/demos/demo-react-native/src/modules/theta-client.ts b/demos/demo-react-native/src/modules/theta-client.ts new file mode 100644 index 0000000000..1769d38e03 --- /dev/null +++ b/demos/demo-react-native/src/modules/theta-client.ts @@ -0,0 +1 @@ +export * from 'theta-client-react-native'; diff --git a/demos/demo-react-native/tsconfig.json b/demos/demo-react-native/tsconfig.json index 0cc9cdd69a..fdc5fcde14 100644 --- a/demos/demo-react-native/tsconfig.json +++ b/demos/demo-react-native/tsconfig.json @@ -2,6 +2,7 @@ { "extends": "@tsconfig/react-native/tsconfig.json", /* Recommended React Native TSConfig base */ "compilerOptions": { + "jsx": "react", /* Visit https://aka.ms/tsconfig.json to read more about this file */ /* Completeness */ diff --git a/flutter/lib/state/capture_status.dart b/flutter/lib/state/capture_status.dart new file mode 100644 index 0000000000..635b077de7 --- /dev/null +++ b/flutter/lib/state/capture_status.dart @@ -0,0 +1,48 @@ +/// Capture Status +enum CaptureStatusEnum { + /// Undefined value + unknown('UNKNOWN'), + + /// Capture status. Performing continuously shoot + shooting('SHOOTING'), + + /// Capture status. In standby + idle('IDLE'), + + /// Capture status. Self-timer is operating + selfTimerCountdown('SELF_TIMER_COUNTDOWN'), + + /// Capture status. Performing multi bracket shooting + bracketShooting('BRACKET_SHOOTING'), + + /// Capture status. Converting post file... + converting('CONVERTING'), + + /// Capture status. Performing timeShift shooting + timeShiftShooting('TIME_SHIFT_SHOOTING'), + + /// Capture status. Performing continuous shooting + continuousShooting('CONTINUOUS_SHOOTING'), + + /// Capture status. Waiting for retrospective video... + retrospectiveImageRecording('RETROSPECTIVE_IMAGE_RECORDING'), + + /// Capture status. Performing burst shooting + burstShooting('BURST_SHOOTING'), + ; + + final String rawValue; + + const CaptureStatusEnum(this.rawValue); + + @override + String toString() { + return rawValue; + } + + static CaptureStatusEnum? getValue(String rawValue) { + return CaptureStatusEnum.values.cast().firstWhere( + (element) => element?.rawValue == rawValue, + orElse: () => null); + } +} diff --git a/flutter/lib/state/importer.dart b/flutter/lib/state/importer.dart new file mode 100644 index 0000000000..d92f2f32e6 --- /dev/null +++ b/flutter/lib/state/importer.dart @@ -0,0 +1,6 @@ +/// THETA state +library; + +export 'capture_status.dart'; +export 'state_gps_info.dart'; +export 'theta_state.dart'; diff --git a/flutter/lib/state/theta_state.dart b/flutter/lib/state/theta_state.dart index 86923dbdfc..e61b8bd11b 100644 --- a/flutter/lib/state/theta_state.dart +++ b/flutter/lib/state/theta_state.dart @@ -1,5 +1,4 @@ import '../theta_client_flutter.dart'; -import 'state_gps_info.dart'; /// Mutable values representing Theta status. class ThetaState { @@ -100,3 +99,201 @@ class ThetaState { this.boardTemp, this.batteryTemp); } + +/// Battery charging state +enum ChargingStateEnum { + /// Battery charging state. Charging + charging('CHARGING'), + + /// Battery charging state. Charging completed + completed('COMPLETED'), + + /// Battery charging state. Not charging + notCharging('NOT_CHARGING'); + + final String rawValue; + + const ChargingStateEnum(this.rawValue); + + @override + String toString() { + return rawValue; + } + + static ChargingStateEnum? getValue(String rawValue) { + return ChargingStateEnum.values.cast().firstWhere( + (element) => element?.rawValue == rawValue, + orElse: () => null); + } +} + +/// Shooting function. +/// Shooting settings are retained separately for both the Still image shooting mode and Video shooting mode. +/// Setting them at the same time as exposureDelay will result in an error. +/// +/// For +/// - RICOH THETA X +/// - RICOH THETA Z1 +enum ShootingFunctionEnum { + /// Normal shooting function + normal('NORMAL'), + + /// Self-timer shooting function(RICOH THETA X is not supported.) + selfTimer('SELF_TIMER'), + + /// My setting shooting function + mySetting('MY_SETTING'); + + final String rawValue; + + const ShootingFunctionEnum(this.rawValue); + + @override + String toString() { + return rawValue; + } + + static ShootingFunctionEnum? getValue(String rawValue) { + return ShootingFunctionEnum.values.cast().firstWhere( + (element) => element?.rawValue == rawValue, + orElse: () => null); + } +} + +/// Microphone option +enum MicrophoneOptionEnum { + /// Microphone option. auto + auto('AUTO'), + + /// Microphone option. built-in microphone + internal('INTERNAL'), + + /// Microphone option. external microphone + external('EXTERNAL'); + + final String rawValue; + + const MicrophoneOptionEnum(this.rawValue); + + @override + String toString() { + return rawValue; + } + + static MicrophoneOptionEnum? getValue(String rawValue) { + return MicrophoneOptionEnum.values.cast().firstWhere( + (element) => element?.rawValue == rawValue, + orElse: () => null); + } +} + +/// Camera error +enum CameraErrorEnum { + /// Camera error + /// Undefined value + unknown('UNKNOWN'), + + /// Camera error + /// Insufficient memory + noMemory('NO_MEMORY'), + + /// Camera error + /// Maximum file number exceeded + fileNumberOver('FILE_NUMBER_OVER'), + + /// Camera error + /// Camera clock not set + noDateSetting('NO_DATE_SETTING'), + + /// Camera error + /// Includes when the card is removed + readError('READ_ERROR'), + + /// Camera error + /// Unsupported media (SDHC, etc.) + notSupportedMediaType('NOT_SUPPORTED_MEDIA_TYPE'), + + /// Camera error + /// FAT32, etc. + notSupportedFileSystem('NOT_SUPPORTED_FILE_SYSTEM'), + + /// Camera error + /// Error warning while mounting + mediaNotReady('MEDIA_NOT_READY'), + + /// Camera error + /// Battery level warning (firmware update) + notEnoughBattery('NOT_ENOUGH_BATTERY'), + + /// Camera error + /// Firmware file mismatch warning + invalidFile('INVALID_FILE'), + + /// Camera error + /// Plugin start warning (IoT technical standards compliance) + pluginBootError('PLUGIN_BOOT_ERROR'), + + /// Camera error + /// When performing continuous shooting by operating the camera while executing , + /// , or with the WebAPI or MTP. + inProgressError('IN_PROGRESS_ERROR'), + + /// Camera error + /// Battery inserted + WLAN ON + Video mode + 4K 60fps / 5.7K 10fps / 5.7K 15fps / 5.7K 30fps / 8K 10fps + cannotRecording('CANNOT_RECORDING'), + + /// Camera error + /// Battery inserted AND Specified battery level or lower + WLAN ON + Video mode + 4K 30fps + cannotRecordLowbat('CANNOT_RECORD_LOWBAT'), + + /// Camera error + /// Shooting hardware failure + captureHwFailed('CAPTURE_HW_FAILED'), + + /// Camera error + /// Software error + captureSwFailed('CAPTURE_SW_FAILED'), + + /// Camera error + /// Internal memory access error + internalMemAccessFail('INTERNAL_MEM_ACCESS_FAIL'), + + /// Camera error + /// Undefined error + unexpectedError('UNEXPECTED_ERROR'), + + /// Camera error + /// Charging error + batteryChargeFail('BATTERY_CHARGE_FAIL'), + + /// Camera error + /// (Board) temperature warning + highTemperatureWarning('HIGH_TEMPERATURE_WARNING'), + + /// Camera error + /// (Board) temperature error + highTemperature('HIGH_TEMPERATURE'), + + /// Camera error + /// Battery temperature error + batteryHighTemperature('BATTERY_HIGH_TEMPERATURE'), + + /// Camera error + /// Electronic compass error + compassCalibration('COMPASS_CALIBRATION'); + + final String rawValue; + + const CameraErrorEnum(this.rawValue); + + @override + String toString() { + return rawValue; + } + + static CameraErrorEnum? getValue(String rawValue) { + return CameraErrorEnum.values.cast().firstWhere( + (element) => element?.rawValue == rawValue, + orElse: () => null); + } +} diff --git a/flutter/lib/theta_client_flutter.dart b/flutter/lib/theta_client_flutter.dart index c539f84d54..54a6352ea5 100644 --- a/flutter/lib/theta_client_flutter.dart +++ b/flutter/lib/theta_client_flutter.dart @@ -5,13 +5,14 @@ import 'package:theta_client_flutter/digest_auth.dart'; import 'capture/capture_builder.dart'; import 'options/importer.dart'; -import 'state/theta_state.dart'; +import 'state/importer.dart'; import 'theta_client_flutter_platform_interface.dart'; export 'capture/capture.dart'; export 'capture/capture_builder.dart'; export 'capture/capturing.dart'; export 'options/importer.dart'; +export 'state/importer.dart'; /// Handle Theta web APIs. class ThetaClientFlutter { @@ -1086,246 +1087,6 @@ enum BurstOrderEnum { } } -/// Capture Status -enum CaptureStatusEnum { - /// Capture status. Performing continuously shoot - shooting('SHOOTING'), - - /// Capture status. In standby - idle('IDLE'), - - /// Capture status. Self-timer is operating - selfTimerCountdown('SELF_TIMER_COUNTDOWN'), - - /// Capture status. Performing multi bracket shooting - bracketShooting('BRACKET_SHOOTING'), - - /// Capture status. Converting post file... - converting('CONVERTING'), - - /// Capture status. Performing timeShift shooting - timeShiftShooting('TIME_SHIFT_SHOOTING'), - - /// Capture status. Performing continuous shooting - continuousShooting('CONTINUOUS_SHOOTING'), - - /// Capture status. Waiting for retrospective video... - retrospectiveImageRecording('RETROSPECTIVE_IMAGE_RECORDING'); - - final String rawValue; - - const CaptureStatusEnum(this.rawValue); - - @override - String toString() { - return rawValue; - } - - static CaptureStatusEnum? getValue(String rawValue) { - return CaptureStatusEnum.values.cast().firstWhere( - (element) => element?.rawValue == rawValue, - orElse: () => null); - } -} - -/// Battery charging state -enum ChargingStateEnum { - /// Battery charging state. Charging - charging('CHARGING'), - - /// Battery charging state. Charging completed - completed('COMPLETED'), - - /// Battery charging state. Not charging - notCharging('NOT_CHARGING'); - - final String rawValue; - - const ChargingStateEnum(this.rawValue); - - @override - String toString() { - return rawValue; - } - - static ChargingStateEnum? getValue(String rawValue) { - return ChargingStateEnum.values.cast().firstWhere( - (element) => element?.rawValue == rawValue, - orElse: () => null); - } -} - -/// Shooting function. -/// Shooting settings are retained separately for both the Still image shooting mode and Video shooting mode. -/// Setting them at the same time as exposureDelay will result in an error. -/// -/// For -/// - RICOH THETA X -/// - RICOH THETA Z1 -enum ShootingFunctionEnum { - /// Normal shooting function - normal('NORMAL'), - - /// Self-timer shooting function(RICOH THETA X is not supported.) - selfTimer('SELF_TIMER'), - - /// My setting shooting function - mySetting('MY_SETTING'); - - final String rawValue; - - const ShootingFunctionEnum(this.rawValue); - - @override - String toString() { - return rawValue; - } - - static ShootingFunctionEnum? getValue(String rawValue) { - return ShootingFunctionEnum.values.cast().firstWhere( - (element) => element?.rawValue == rawValue, - orElse: () => null); - } -} - -/// Microphone option -enum MicrophoneOptionEnum { - /// Microphone option. auto - auto('AUTO'), - - /// Microphone option. built-in microphone - internal('INTERNAL'), - - /// Microphone option. external microphone - external('EXTERNAL'); - - final String rawValue; - - const MicrophoneOptionEnum(this.rawValue); - - @override - String toString() { - return rawValue; - } - - static MicrophoneOptionEnum? getValue(String rawValue) { - return MicrophoneOptionEnum.values.cast().firstWhere( - (element) => element?.rawValue == rawValue, - orElse: () => null); - } -} - -/// Camera error -enum CameraErrorEnum { - /// Camera error - /// Undefined value - unknown('UNKNOWN'), - - /// Camera error - /// Insufficient memory - noMemory('NO_MEMORY'), - - /// Camera error - /// Maximum file number exceeded - fileNumberOver('FILE_NUMBER_OVER'), - - /// Camera error - /// Camera clock not set - noDateSetting('NO_DATE_SETTING'), - - /// Camera error - /// Includes when the card is removed - readError('READ_ERROR'), - - /// Camera error - /// Unsupported media (SDHC, etc.) - notSupportedMediaType('NOT_SUPPORTED_MEDIA_TYPE'), - - /// Camera error - /// FAT32, etc. - notSupportedFileSystem('NOT_SUPPORTED_FILE_SYSTEM'), - - /// Camera error - /// Error warning while mounting - mediaNotReady('MEDIA_NOT_READY'), - - /// Camera error - /// Battery level warning (firmware update) - notEnoughBattery('NOT_ENOUGH_BATTERY'), - - /// Camera error - /// Firmware file mismatch warning - invalidFile('INVALID_FILE'), - - /// Camera error - /// Plugin start warning (IoT technical standards compliance) - pluginBootError('PLUGIN_BOOT_ERROR'), - - /// Camera error - /// When performing continuous shooting by operating the camera while executing , - /// , or with the WebAPI or MTP. - inProgressError('IN_PROGRESS_ERROR'), - - /// Camera error - /// Battery inserted + WLAN ON + Video mode + 4K 60fps / 5.7K 10fps / 5.7K 15fps / 5.7K 30fps / 8K 10fps - cannotRecording('CANNOT_RECORDING'), - - /// Camera error - /// Battery inserted AND Specified battery level or lower + WLAN ON + Video mode + 4K 30fps - cannotRecordLowbat('CANNOT_RECORD_LOWBAT'), - - /// Camera error - /// Shooting hardware failure - captureHwFailed('CAPTURE_HW_FAILED'), - - /// Camera error - /// Software error - captureSwFailed('CAPTURE_SW_FAILED'), - - /// Camera error - /// Internal memory access error - internalMemAccessFail('INTERNAL_MEM_ACCESS_FAIL'), - - /// Camera error - /// Undefined error - unexpectedError('UNEXPECTED_ERROR'), - - /// Camera error - /// Charging error - batteryChargeFail('BATTERY_CHARGE_FAIL'), - - /// Camera error - /// (Board) temperature warning - highTemperatureWarning('HIGH_TEMPERATURE_WARNING'), - - /// Camera error - /// (Board) temperature error - highTemperature('HIGH_TEMPERATURE'), - - /// Camera error - /// Battery temperature error - batteryHighTemperature('BATTERY_HIGH_TEMPERATURE'), - - /// Camera error - /// Electronic compass error - compassCalibration('COMPASS_CALIBRATION'); - - final String rawValue; - - const CameraErrorEnum(this.rawValue); - - @override - String toString() { - return rawValue; - } - - static CameraErrorEnum? getValue(String rawValue) { - return CameraErrorEnum.values.cast().firstWhere( - (element) => element?.rawValue == rawValue, - orElse: () => null); - } -} - /// Exif metadata of a still image. class Exif { /// EXIF Support version diff --git a/flutter/lib/theta_client_flutter_method_channel.dart b/flutter/lib/theta_client_flutter_method_channel.dart index e288d238fb..ed9cf3ad75 100644 --- a/flutter/lib/theta_client_flutter_method_channel.dart +++ b/flutter/lib/theta_client_flutter_method_channel.dart @@ -5,7 +5,6 @@ import 'package:flutter/services.dart'; import 'package:theta_client_flutter/theta_client_flutter.dart'; import 'package:theta_client_flutter/utils/convert_utils.dart'; -import 'state/theta_state.dart'; import 'theta_client_flutter_platform_interface.dart'; const notifyIdLivePreview = 10001; diff --git a/flutter/lib/theta_client_flutter_platform_interface.dart b/flutter/lib/theta_client_flutter_platform_interface.dart index a652b02eb4..808345c9fc 100644 --- a/flutter/lib/theta_client_flutter_platform_interface.dart +++ b/flutter/lib/theta_client_flutter_platform_interface.dart @@ -2,7 +2,6 @@ import 'package:flutter/foundation.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'package:theta_client_flutter/theta_client_flutter.dart'; -import 'state/theta_state.dart'; import 'theta_client_flutter_method_channel.dart'; abstract class ThetaClientFlutterPlatform extends PlatformInterface { diff --git a/flutter/lib/utils/convert_utils.dart b/flutter/lib/utils/convert_utils.dart index 25dd59749c..728255f0f8 100644 --- a/flutter/lib/utils/convert_utils.dart +++ b/flutter/lib/utils/convert_utils.dart @@ -1,9 +1,6 @@ import 'package:theta_client_flutter/digest_auth.dart'; import 'package:theta_client_flutter/theta_client_flutter.dart'; -import '../state/state_gps_info.dart'; -import '../state/theta_state.dart'; - class ConvertUtils { static List? convertAutoBracketOption(List? data) { if (data == null) { diff --git a/flutter/test/enum_name_test.dart b/flutter/test/enum_name_test.dart index 57f30fda3c..4275ecd493 100644 --- a/flutter/test/enum_name_test.dart +++ b/flutter/test/enum_name_test.dart @@ -255,26 +255,6 @@ void main() { } }); - test('CaptureStatusEnum', () async { - List> data = [ - [CaptureStatusEnum.shooting, 'SHOOTING'], - [CaptureStatusEnum.idle, 'IDLE'], - [CaptureStatusEnum.selfTimerCountdown, 'SELF_TIMER_COUNTDOWN'], - [CaptureStatusEnum.bracketShooting, 'BRACKET_SHOOTING'], - [CaptureStatusEnum.converting, 'CONVERTING'], - [CaptureStatusEnum.timeShiftShooting, 'TIME_SHIFT_SHOOTING'], - [CaptureStatusEnum.continuousShooting, 'CONTINUOUS_SHOOTING'], - [ - CaptureStatusEnum.retrospectiveImageRecording, - 'RETROSPECTIVE_IMAGE_RECORDING' - ], - ]; - expect(data.length, CaptureStatusEnum.values.length, reason: 'enum count'); - for (int i = 0; i < data.length; i++) { - expect(data[i][0].toString(), data[i][1], reason: data[i][1]); - } - }); - test('ChargingStateEnum', () async { List> data = [ [ChargingStateEnum.charging, 'CHARGING'], diff --git a/flutter/test/state/capture_status_test.dart b/flutter/test/state/capture_status_test.dart new file mode 100644 index 0000000000..dac0b15063 --- /dev/null +++ b/flutter/test/state/capture_status_test.dart @@ -0,0 +1,30 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:theta_client_flutter/theta_client_flutter.dart'; + +void main() { + setUp(() {}); + + tearDown(() {}); + + test('CaptureStatusEnum', () async { + List> data = [ + [CaptureStatusEnum.unknown, 'UNKNOWN'], + [CaptureStatusEnum.shooting, 'SHOOTING'], + [CaptureStatusEnum.idle, 'IDLE'], + [CaptureStatusEnum.selfTimerCountdown, 'SELF_TIMER_COUNTDOWN'], + [CaptureStatusEnum.bracketShooting, 'BRACKET_SHOOTING'], + [CaptureStatusEnum.converting, 'CONVERTING'], + [CaptureStatusEnum.timeShiftShooting, 'TIME_SHIFT_SHOOTING'], + [CaptureStatusEnum.continuousShooting, 'CONTINUOUS_SHOOTING'], + [ + CaptureStatusEnum.retrospectiveImageRecording, + 'RETROSPECTIVE_IMAGE_RECORDING' + ], + [CaptureStatusEnum.burstShooting, 'BURST_SHOOTING'], + ]; + expect(data.length, CaptureStatusEnum.values.length, reason: 'enum count'); + for (int i = 0; i < data.length; i++) { + expect(data[i][0].toString(), data[i][1], reason: data[i][1]); + } + }); +} diff --git a/flutter/test/theta_client_flutter_test.dart b/flutter/test/theta_client_flutter_test.dart index 727761e287..1f56d465ee 100644 --- a/flutter/test/theta_client_flutter_test.dart +++ b/flutter/test/theta_client_flutter_test.dart @@ -2,8 +2,6 @@ import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'package:theta_client_flutter/state/state_gps_info.dart'; -import 'package:theta_client_flutter/state/theta_state.dart'; import 'package:theta_client_flutter/theta_client_flutter.dart'; import 'package:theta_client_flutter/theta_client_flutter_method_channel.dart'; import 'package:theta_client_flutter/theta_client_flutter_platform_interface.dart'; diff --git a/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/ThetaRepository.kt b/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/ThetaRepository.kt index 697a8e9003..1fa75aaf89 100644 --- a/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/ThetaRepository.kt +++ b/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/ThetaRepository.kt @@ -6344,6 +6344,11 @@ class ThetaRepository internal constructor(val endpoint: String, config: Config? * Capture Status */ enum class CaptureStatusEnum { + /** + * Undefined value + */ + UNKNOWN, + /** * Capture status * Performing continuously shoot @@ -6390,7 +6395,14 @@ class ThetaRepository internal constructor(val endpoint: String, config: Config? * Capture status * Waiting for retrospective video... */ - RETROSPECTIVE_IMAGE_RECORDING; + RETROSPECTIVE_IMAGE_RECORDING, + + /** + * Capture status + * Performing burst shooting + */ + BURST_SHOOTING, + ; companion object { /** @@ -6401,6 +6413,7 @@ class ThetaRepository internal constructor(val endpoint: String, config: Config? */ internal fun get(captureStatus: CaptureStatus): CaptureStatusEnum { return when (captureStatus) { + CaptureStatus.UNKNOWN -> UNKNOWN CaptureStatus.SHOOTING -> SHOOTING CaptureStatus.IDLE -> IDLE CaptureStatus.SELF_TIMER_COUNTDOWN -> SELF_TIMER_COUNTDOWN @@ -6409,6 +6422,7 @@ class ThetaRepository internal constructor(val endpoint: String, config: Config? CaptureStatus.TIME_SHIFT_SHOOTING -> TIME_SHIFT_SHOOTING CaptureStatus.CONTINUOUS_SHOOTING -> CONTINUOUS_SHOOTING CaptureStatus.RETROSPECTIVE_IMAGE_RECORDING -> RETROSPECTIVE_IMAGE_RECORDING + CaptureStatus.BURST_SHOOTING -> BURST_SHOOTING } } } diff --git a/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/capture/CaptureStatusMonitor.kt b/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/capture/CaptureStatusMonitor.kt index 722f110db5..261adfc115 100644 --- a/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/capture/CaptureStatusMonitor.kt +++ b/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/capture/CaptureStatusMonitor.kt @@ -47,7 +47,9 @@ internal class CaptureStatusMonitor( updateStatus(status) } } - delay(checkStateInterval) + if (isStartMonitor) { + delay(checkStateInterval) + } } } } diff --git a/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/transferred/stateApi.kt b/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/transferred/stateApi.kt index 7fd40c101f..750dfc2995 100644 --- a/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/transferred/stateApi.kt +++ b/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/transferred/stateApi.kt @@ -6,7 +6,6 @@ package com.ricoh360.thetaclient.transferred import io.ktor.http.* import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonNames /** * /osc/state api request @@ -222,58 +221,81 @@ internal data class CameraState( val _batteryTemp: Int? = null, ) +internal object CaptureStatusSerializer : + SerialNameEnumIgnoreUnknownSerializer(CaptureStatus.entries, CaptureStatus.UNKNOWN) + /** * Capture status */ -@Serializable -internal enum class CaptureStatus { +@Serializable(with = CaptureStatusSerializer::class) +internal enum class CaptureStatus : SerialNameEnum { + /** + * Undefined value + */ + UNKNOWN, + /** * shooting: Performing continuously shoots, */ - @SerialName("shooting") - SHOOTING, + SHOOTING { + override val serialName: String = "shooting" + }, /** * idle: In standby, */ - @SerialName("idle") - IDLE, + IDLE { + override val serialName: String = "idle" + }, /** * self-timer countdown: Self-timer is operating, */ - @SerialName("self-timer countdown") - SELF_TIMER_COUNTDOWN, + SELF_TIMER_COUNTDOWN { + override val serialName: String = "self-timer countdown" + }, /** * bracket shooting: Performing multi bracket shooting, */ - @SerialName("bracket shooting") - BRACKET_SHOOTING, + BRACKET_SHOOTING { + override val serialName: String = "bracket shooting" + }, /** * converting: Converting post file…, */ - @SerialName("converting") - CONVERTING, + CONVERTING { + override val serialName: String = "converting" + }, /** * timeShift shooting: Performing timeShift shooting, */ - @SerialName("timeShift shooting") - TIME_SHIFT_SHOOTING, + TIME_SHIFT_SHOOTING { + override val serialName: String = "timeShift shooting" + }, /** * continuous shooting: Performing continuous shooting, */ - @SerialName("continuous shooting") - CONTINUOUS_SHOOTING, + CONTINUOUS_SHOOTING { + override val serialName: String = "continuous shooting" + }, /** * retrospective image recording: Waiting for retrospective video… */ - @SerialName("retrospective image recording") - RETROSPECTIVE_IMAGE_RECORDING, + RETROSPECTIVE_IMAGE_RECORDING { + override val serialName: String = "retrospective image recording" + }, + + /** + * burst shooting + */ + BURST_SHOOTING { + override val serialName: String = "burst shooting" + }, } /** diff --git a/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/repository/GetThetaStateTest.kt b/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/repository/GetThetaStateTest.kt index 8366240d91..b41202ba59 100644 --- a/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/repository/GetThetaStateTest.kt +++ b/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/repository/GetThetaStateTest.kt @@ -520,6 +520,11 @@ class GetThetaStateTest { } // check CaptureStatusEnum + assertEquals( + ThetaRepository.CaptureStatusEnum.get(CaptureStatus.UNKNOWN), + ThetaRepository.CaptureStatusEnum.UNKNOWN, + "CaptureStatusEnum" + ) assertEquals( ThetaRepository.CaptureStatusEnum.get(CaptureStatus.SHOOTING), ThetaRepository.CaptureStatusEnum.SHOOTING, @@ -560,6 +565,11 @@ class GetThetaStateTest { ThetaRepository.CaptureStatusEnum.RETROSPECTIVE_IMAGE_RECORDING, "CaptureStatusEnum" ) + assertEquals( + ThetaRepository.CaptureStatusEnum.get(CaptureStatus.BURST_SHOOTING), + ThetaRepository.CaptureStatusEnum.BURST_SHOOTING, + "CaptureStatusEnum" + ) // check ChargingStateEnum assertEquals( diff --git a/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/transferred/StateApiTest.kt b/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/transferred/StateApiTest.kt new file mode 100644 index 0000000000..bb39e34f34 --- /dev/null +++ b/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/transferred/StateApiTest.kt @@ -0,0 +1,63 @@ +package com.ricoh360.thetaclient.transferred + +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals + +@OptIn(ExperimentalSerializationApi::class) +class StateApiTest { + @BeforeTest + fun setup() { + } + + @AfterTest + fun teardown() { + } + + val js = Json { + encodeDefaults = true // Encode properties with default value. + explicitNulls = false // Don't encode properties with null value. + ignoreUnknownKeys = true // Ignore unknown keys on decode. + } + + @Test + fun serializeCaptureStatus() = runTest { + val data = listOf( + Pair(CaptureStatus.UNKNOWN, "unknown value"), + Pair(CaptureStatus.SHOOTING, "shooting"), + Pair(CaptureStatus.IDLE, "idle"), + Pair(CaptureStatus.SELF_TIMER_COUNTDOWN, "self-timer countdown"), + Pair(CaptureStatus.BRACKET_SHOOTING, "bracket shooting"), + Pair(CaptureStatus.CONVERTING, "converting"), + Pair(CaptureStatus.TIME_SHIFT_SHOOTING, "timeShift shooting"), + Pair(CaptureStatus.CONTINUOUS_SHOOTING, "continuous shooting"), + Pair(CaptureStatus.RETROSPECTIVE_IMAGE_RECORDING, "retrospective image recording"), + Pair(CaptureStatus.BURST_SHOOTING, "burst shooting"), + ) + + @Serializable + data class Dummy( + val value: CaptureStatus, + ) + + data.forEach { + val jsonString = """ + { + "value": "${it.second}" + } + """.trimIndent() + val dummy = js.decodeFromString(jsonString) + assertEquals(dummy.value, it.first, "CaptureStatus: ${it.first.name}") + + val encoded = js.encodeToString(dummy) + val dist = js.decodeFromString(encoded) + assertEquals(dist.value, it.first, "CaptureStatus: ${it.first.name}") + } + } +} \ No newline at end of file diff --git a/react-native/package.json b/react-native/package.json index 4ff863e1cb..ff20d67131 100644 --- a/react-native/package.json +++ b/react-native/package.json @@ -83,7 +83,7 @@ "engines": { "node": ">= 16.0.0" }, - "packageManager": "^yarn@1.22.15", + "packageManager": "yarn@1.22.22", "jest": { "preset": "react-native", "modulePathIgnorePatterns": [ diff --git a/react-native/src/__tests__/theta-repository/theta-state/capture-status.test.ts b/react-native/src/__tests__/theta-repository/theta-state/capture-status.test.ts new file mode 100644 index 0000000000..bbaa6f890f --- /dev/null +++ b/react-native/src/__tests__/theta-repository/theta-state/capture-status.test.ts @@ -0,0 +1,29 @@ +import { CaptureStatusEnum } from '../../../theta-repository/theta-state'; + +describe('CaptureStatusEnum', () => { + const data: [CaptureStatusEnum, string][] = [ + [CaptureStatusEnum.UNKNOWN, 'UNKNOWN'], + [CaptureStatusEnum.SHOOTING, 'SHOOTING'], + [CaptureStatusEnum.IDLE, 'IDLE'], + [CaptureStatusEnum.SELF_TIMER_COUNTDOWN, 'SELF_TIMER_COUNTDOWN'], + [CaptureStatusEnum.BRACKET_SHOOTING, 'BRACKET_SHOOTING'], + [CaptureStatusEnum.CONVERTING, 'CONVERTING'], + [CaptureStatusEnum.TIME_SHIFT_SHOOTING, 'TIME_SHIFT_SHOOTING'], + [CaptureStatusEnum.CONTINUOUS_SHOOTING, 'CONTINUOUS_SHOOTING'], + [ + CaptureStatusEnum.RETROSPECTIVE_IMAGE_RECORDING, + 'RETROSPECTIVE_IMAGE_RECORDING', + ], + [CaptureStatusEnum.BURST_SHOOTING, 'BURST_SHOOTING'], + ]; + + test('length', () => { + expect(data.length).toBe(Object.keys(CaptureStatusEnum).length); + }); + + test('data', () => { + data.forEach((item) => { + expect(item[0]).toBe(item[1]); + }); + }); +}); diff --git a/react-native/src/theta-repository/theta-state/capture-status.ts b/react-native/src/theta-repository/theta-state/capture-status.ts new file mode 100644 index 0000000000..4fa92d8963 --- /dev/null +++ b/react-native/src/theta-repository/theta-state/capture-status.ts @@ -0,0 +1,27 @@ +/** Capture Status constants */ +export const CaptureStatusEnum = { + /** Undefined value */ + UNKNOWN: 'UNKNOWN', + /** Performing continuously shoot */ + SHOOTING: 'SHOOTING', + /** In standby */ + IDLE: 'IDLE', + /** Self-timer is operating */ + SELF_TIMER_COUNTDOWN: 'SELF_TIMER_COUNTDOWN', + /** Performing multi bracket shooting */ + BRACKET_SHOOTING: 'BRACKET_SHOOTING', + /** Converting post file... */ + CONVERTING: 'CONVERTING', + /** Performing timeShift shooting */ + TIME_SHIFT_SHOOTING: 'TIME_SHIFT_SHOOTING', + /** Performing continuous shooting */ + CONTINUOUS_SHOOTING: 'CONTINUOUS_SHOOTING', + /** Waiting for retrospective video... */ + RETROSPECTIVE_IMAGE_RECORDING: 'RETROSPECTIVE_IMAGE_RECORDING', + /** Performing burst shooting */ + BURST_SHOOTING: 'BURST_SHOOTING', +} as const; + +/** type definition of CaptureStatusEnum */ +export type CaptureStatusEnum = + (typeof CaptureStatusEnum)[keyof typeof CaptureStatusEnum]; diff --git a/react-native/src/theta-repository/theta-state/index.ts b/react-native/src/theta-repository/theta-state/index.ts index 51eefdaf70..37d9b616a3 100644 --- a/react-native/src/theta-repository/theta-state/index.ts +++ b/react-native/src/theta-repository/theta-state/index.ts @@ -1,3 +1,4 @@ export * from './camera-error'; +export * from './capture-status'; export * from './state-gps-info'; export * from './theta-state'; diff --git a/react-native/src/theta-repository/theta-state/theta-state.ts b/react-native/src/theta-repository/theta-state/theta-state.ts index ed459627d5..80340ab704 100644 --- a/react-native/src/theta-repository/theta-state/theta-state.ts +++ b/react-native/src/theta-repository/theta-state/theta-state.ts @@ -1,5 +1,6 @@ import type { ShootingFunctionEnum } from '../options/option-function'; import type { CameraErrorEnum } from './camera-error'; +import type { CaptureStatusEnum } from './capture-status'; import type { StateGpsInfo } from './state-gps-info'; /** Battery charging state constants */ @@ -16,30 +17,6 @@ export const ChargingStateEnum = { export type ChargingStateEnum = (typeof ChargingStateEnum)[keyof typeof ChargingStateEnum]; -/** Capture Status constants */ -export const CaptureStatusEnum = { - /** Performing continuously shoot */ - SHOOTING: 'SHOOTING', - /** In standby */ - IDLE: 'IDLE', - /** Self-timer is operating */ - SELF_TIMER_COUNTDOWN: 'SELF_TIMER_COUNTDOWN', - /** Performing multi bracket shooting */ - BRACKET_SHOOTING: 'BRACKET_SHOOTING', - /** Converting post file... */ - CONVERTING: 'CONVERTING', - /** Performing timeShift shooting */ - TIME_SHIFT_SHOOTING: 'TIME_SHIFT_SHOOTING', - /** Performing continuous shooting */ - CONTINUOUS_SHOOTING: 'CONTINUOUS_SHOOTING', - /** Waiting for retrospective video... */ - RETROSPECTIVE_IMAGE_RECORDING: 'RETROSPECTIVE_IMAGE_RECORDING', -} as const; - -/** type definition of CaptureStatusEnum */ -export type CaptureStatusEnum = - (typeof CaptureStatusEnum)[keyof typeof CaptureStatusEnum]; - /** Microphone option constants */ export const MicrophoneOptionEnum = { /** auto */ diff --git a/react-native/verification-tool/src/App.tsx b/react-native/verification-tool/src/App.tsx index e2b1b4e824..e1a0e48c46 100644 --- a/react-native/verification-tool/src/App.tsx +++ b/react-native/verification-tool/src/App.tsx @@ -23,7 +23,7 @@ import CompositeIntervalCaptureScreen from './screen/composite-interval-capture- import BurstCaptureScreen from './screen/burst-capture-screen/burst-capture-screen'; import ContinuousCaptureScreen from './screen/continuous-capture-screen/continuous-capture-screen'; import MultiBracketCaptureScreen from './screen/multi-bracket-capture-screen/multi-bracket-capture-screen'; -import type { FileInfo } from 'theta-client-react-native'; +import type { FileInfo } from './modules/theta-client'; export type RootStackParamList = { menu: undefined; diff --git a/react-native/verification-tool/src/components/capture/capture-common-options/capture-common-options.tsx b/react-native/verification-tool/src/components/capture/capture-common-options/capture-common-options.tsx index 81c70f6d76..65551ec194 100644 --- a/react-native/verification-tool/src/components/capture/capture-common-options/capture-common-options.tsx +++ b/react-native/verification-tool/src/components/capture/capture-common-options/capture-common-options.tsx @@ -13,7 +13,7 @@ import { IsoAutoHighLimitEnum, IsoEnum, WhiteBalanceEnum, -} from 'theta-client-react-native'; +} from '../../../modules/theta-client'; export const CaptureCommonOptionsEdit: React.FC = ({ onChange, diff --git a/react-native/verification-tool/src/components/list-files-view/list-files-view.tsx b/react-native/verification-tool/src/components/list-files-view/list-files-view.tsx index 614d37cb32..147bb62c40 100644 --- a/react-native/verification-tool/src/components/list-files-view/list-files-view.tsx +++ b/react-native/verification-tool/src/components/list-files-view/list-files-view.tsx @@ -16,7 +16,7 @@ import { StorageEnum, ThetaFiles, listFiles, -} from 'theta-client-react-native'; +} from '../../modules/theta-client'; interface Props extends Pick { style?: StyleProp; diff --git a/react-native/verification-tool/src/components/options/auto-bracket/auto-bracket-edit.tsx b/react-native/verification-tool/src/components/options/auto-bracket/auto-bracket-edit.tsx index 436afc6538..32e5fbe543 100644 --- a/react-native/verification-tool/src/components/options/auto-bracket/auto-bracket-edit.tsx +++ b/react-native/verification-tool/src/components/options/auto-bracket/auto-bracket-edit.tsx @@ -12,7 +12,7 @@ import { IsoEnum, ShutterSpeedEnum, WhiteBalanceEnum, -} from 'theta-client-react-native'; +} from '../../../modules/theta-client'; import { EnumEdit } from '../enum-edit'; const EditItem = ({ diff --git a/react-native/verification-tool/src/components/options/burst-option/burst-option-edit.tsx b/react-native/verification-tool/src/components/options/burst-option/burst-option-edit.tsx index 1cc2293482..098a285795 100644 --- a/react-native/verification-tool/src/components/options/burst-option/burst-option-edit.tsx +++ b/react-native/verification-tool/src/components/options/burst-option/burst-option-edit.tsx @@ -9,7 +9,7 @@ import { BurstEnableIsoControlEnum, BurstMaxExposureTimeEnum, BurstOrderEnum, -} from 'theta-client-react-native'; +} from '../../../modules/theta-client'; export const BurstOptionsEdit: React.FC = ({ onChange, diff --git a/react-native/verification-tool/src/components/options/gps-info/gps-info-edit.tsx b/react-native/verification-tool/src/components/options/gps-info/gps-info-edit.tsx index 90ed7f9eb0..1a23407a58 100644 --- a/react-native/verification-tool/src/components/options/gps-info/gps-info-edit.tsx +++ b/react-native/verification-tool/src/components/options/gps-info/gps-info-edit.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import type { OptionEditProps } from '..'; import { View, Text, Switch } from 'react-native'; -import type { GpsInfo } from 'theta-client-react-native'; +import type { GpsInfo } from '../../../modules/theta-client'; import { InputNumber } from '../../ui/input-number'; import { InputString } from '../../ui/input-string'; import styles from './styles'; diff --git a/react-native/verification-tool/src/components/options/index.ts b/react-native/verification-tool/src/components/options/index.ts index 52091153b9..067b276298 100644 --- a/react-native/verification-tool/src/components/options/index.ts +++ b/react-native/verification-tool/src/components/options/index.ts @@ -1,4 +1,4 @@ -import type { Options } from 'theta-client-react-native'; +import type { Options } from '../../modules/theta-client'; export interface OptionEditProps { onChange: (options: Options) => void; diff --git a/react-native/verification-tool/src/components/options/number-edit/number-edit.tsx b/react-native/verification-tool/src/components/options/number-edit/number-edit.tsx index 74d8b41a7e..61f7354a44 100644 --- a/react-native/verification-tool/src/components/options/number-edit/number-edit.tsx +++ b/react-native/verification-tool/src/components/options/number-edit/number-edit.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import type { OptionEditProps } from '..'; import { InputNumber } from '../../ui/input-number'; -import type { Options } from 'theta-client-react-native'; +import type { Options } from '../../../modules/theta-client'; interface Props extends OptionEditProps { propName: string; diff --git a/react-native/verification-tool/src/components/options/string-edit/string-edit.tsx b/react-native/verification-tool/src/components/options/string-edit/string-edit.tsx index 056f985c8a..684d12d17e 100644 --- a/react-native/verification-tool/src/components/options/string-edit/string-edit.tsx +++ b/react-native/verification-tool/src/components/options/string-edit/string-edit.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import type { OptionEditProps } from '..'; import { InputString } from '../../ui/input-string'; -import type { Options } from 'theta-client-react-native'; +import type { Options } from '../../../modules/theta-client'; interface Props extends OptionEditProps { propName: string; diff --git a/react-native/verification-tool/src/components/options/time-shift/time-shift-edit.tsx b/react-native/verification-tool/src/components/options/time-shift/time-shift-edit.tsx index 3edeebe02f..d5fb401af4 100644 --- a/react-native/verification-tool/src/components/options/time-shift/time-shift-edit.tsx +++ b/react-native/verification-tool/src/components/options/time-shift/time-shift-edit.tsx @@ -1,5 +1,8 @@ import * as React from 'react'; -import { TimeShift, TimeShiftIntervalEnum } from 'theta-client-react-native'; +import { + TimeShift, + TimeShiftIntervalEnum, +} from '../../../modules/theta-client'; import { EnumEdit, type OptionEditProps } from '..'; import { View } from 'react-native'; import { TitledSwitch } from '../../ui/titled-switch'; diff --git a/react-native/verification-tool/src/components/options/top-bottom-correction-rotation/top-bottom-correction-rotation-edit.tsx b/react-native/verification-tool/src/components/options/top-bottom-correction-rotation/top-bottom-correction-rotation-edit.tsx index e7ca07dc99..d91e9c2f70 100644 --- a/react-native/verification-tool/src/components/options/top-bottom-correction-rotation/top-bottom-correction-rotation-edit.tsx +++ b/react-native/verification-tool/src/components/options/top-bottom-correction-rotation/top-bottom-correction-rotation-edit.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import type { OptionEditProps } from '..'; import { View } from 'react-native'; -import type { TopBottomCorrectionRotation } from 'theta-client-react-native'; +import type { TopBottomCorrectionRotation } from '../../../modules/theta-client'; import { InputNumber } from '../../ui/input-number'; export const TopBottomCorrectionRotationEdit: React.FC = ({ diff --git a/react-native/verification-tool/src/modules/theta-client.ts b/react-native/verification-tool/src/modules/theta-client.ts new file mode 100644 index 0000000000..1769d38e03 --- /dev/null +++ b/react-native/verification-tool/src/modules/theta-client.ts @@ -0,0 +1 @@ +export * from 'theta-client-react-native'; diff --git a/react-native/verification-tool/src/screen/burst-capture-screen/burst-capture-screen.tsx b/react-native/verification-tool/src/screen/burst-capture-screen/burst-capture-screen.tsx index c6250d0aca..ad913dbaf5 100644 --- a/react-native/verification-tool/src/screen/burst-capture-screen/burst-capture-screen.tsx +++ b/react-native/verification-tool/src/screen/burst-capture-screen/burst-capture-screen.tsx @@ -15,7 +15,7 @@ import { Options, getBurstCaptureBuilder, stopSelfTimer, -} from 'theta-client-react-native'; +} from '../../modules/theta-client'; import { CaptureCommonOptionsEdit } from '../../components/capture/capture-common-options'; import { InputNumber } from '../../components/ui/input-number'; import type { NativeStackScreenProps } from '@react-navigation/native-stack'; diff --git a/react-native/verification-tool/src/screen/commands-screen/commands-screen.tsx b/react-native/verification-tool/src/screen/commands-screen/commands-screen.tsx index c1833a649a..299e2ffb90 100644 --- a/react-native/verification-tool/src/screen/commands-screen/commands-screen.tsx +++ b/react-native/verification-tool/src/screen/commands-screen/commands-screen.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import { ScrollView, Text, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import styles from './styles'; -import { reset, restoreSettings, finishWlan } from 'theta-client-react-native'; +import { reset, restoreSettings, finishWlan } from '../../modules/theta-client'; import Button from '../../components/ui/button'; import { ItemListView, type Item } from '../../components/ui/item-list'; import type { NativeStackScreenProps } from '@react-navigation/native-stack'; diff --git a/react-native/verification-tool/src/screen/composite-interval-capture-screen/composite-interval-capture-screen.tsx b/react-native/verification-tool/src/screen/composite-interval-capture-screen/composite-interval-capture-screen.tsx index e06049446d..58349a0b5d 100644 --- a/react-native/verification-tool/src/screen/composite-interval-capture-screen/composite-interval-capture-screen.tsx +++ b/react-native/verification-tool/src/screen/composite-interval-capture-screen/composite-interval-capture-screen.tsx @@ -8,7 +8,7 @@ import { Options, getCompositeIntervalCaptureBuilder, stopSelfTimer, -} from 'theta-client-react-native'; +} from '../../modules/theta-client'; import { CaptureCommonOptionsEdit } from '../../components/capture/capture-common-options'; import { InputNumber } from '../../components/ui/input-number'; import { NumberEdit } from 'verification-tool/src/components/options/number-edit'; diff --git a/react-native/verification-tool/src/screen/continuous-capture-screen/continuous-capture-screen.tsx b/react-native/verification-tool/src/screen/continuous-capture-screen/continuous-capture-screen.tsx index 71dd65c4ff..94034129a8 100644 --- a/react-native/verification-tool/src/screen/continuous-capture-screen/continuous-capture-screen.tsx +++ b/react-native/verification-tool/src/screen/continuous-capture-screen/continuous-capture-screen.tsx @@ -10,7 +10,7 @@ import { PhotoFileFormatEnum, getContinuousCaptureBuilder, stopSelfTimer, -} from 'theta-client-react-native'; +} from '../../modules/theta-client'; import { CaptureCommonOptionsEdit } from '../../components/capture/capture-common-options'; import { InputNumber } from '../../components/ui/input-number'; import type { NativeStackScreenProps } from '@react-navigation/native-stack'; diff --git a/react-native/verification-tool/src/screen/delete-files-screen/delete-files-screen.tsx b/react-native/verification-tool/src/screen/delete-files-screen/delete-files-screen.tsx index f273cb82fa..aec8bd35e6 100644 --- a/react-native/verification-tool/src/screen/delete-files-screen/delete-files-screen.tsx +++ b/react-native/verification-tool/src/screen/delete-files-screen/delete-files-screen.tsx @@ -10,7 +10,7 @@ import { deleteAllImageFiles, deleteAllVideoFiles, deleteFiles, -} from 'theta-client-react-native'; +} from '../../modules/theta-client'; import Button from '../../components/ui/button'; import { ListFilesView } from '../../components/list-files-view'; import { Item, ItemSelectorView } from '../../components/ui/item-list'; diff --git a/react-native/verification-tool/src/screen/get-info-screen/get-info-screen.tsx b/react-native/verification-tool/src/screen/get-info-screen/get-info-screen.tsx index 0176daf828..0958d1c22d 100644 --- a/react-native/verification-tool/src/screen/get-info-screen/get-info-screen.tsx +++ b/react-native/verification-tool/src/screen/get-info-screen/get-info-screen.tsx @@ -9,7 +9,7 @@ import { getThetaState, listAccessPoints, listPlugins, -} from 'theta-client-react-native'; +} from '../../modules/theta-client'; import Button from '../../components/ui/button'; import { ItemListView, type Item } from '../../components/ui/item-list'; import type { NativeStackScreenProps } from '@react-navigation/native-stack'; diff --git a/react-native/verification-tool/src/screen/get-metadata-screen/get-metadata-screen.tsx b/react-native/verification-tool/src/screen/get-metadata-screen/get-metadata-screen.tsx index 4e426dfad0..d17406a156 100644 --- a/react-native/verification-tool/src/screen/get-metadata-screen/get-metadata-screen.tsx +++ b/react-native/verification-tool/src/screen/get-metadata-screen/get-metadata-screen.tsx @@ -2,7 +2,11 @@ import React, { useState } from 'react'; import { StatusBar, Text, View, ScrollView, Alert } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import styles from './styles'; -import { FileTypeEnum, FileInfo, getMetadata } from 'theta-client-react-native'; +import { + FileTypeEnum, + FileInfo, + getMetadata, +} from '../../modules/theta-client'; import Button from '../../components/ui/button'; import { ListFilesView } from '../../components/list-files-view'; import type { NativeStackScreenProps } from '@react-navigation/native-stack'; diff --git a/react-native/verification-tool/src/screen/limitless-interval-capture-screen/limitless-interval-capture-screen.tsx b/react-native/verification-tool/src/screen/limitless-interval-capture-screen/limitless-interval-capture-screen.tsx index a85fa58f6c..4338cb7d7a 100644 --- a/react-native/verification-tool/src/screen/limitless-interval-capture-screen/limitless-interval-capture-screen.tsx +++ b/react-native/verification-tool/src/screen/limitless-interval-capture-screen/limitless-interval-capture-screen.tsx @@ -8,7 +8,7 @@ import { Options, getLimitlessIntervalCaptureBuilder, stopSelfTimer, -} from 'theta-client-react-native'; +} from '../../modules/theta-client'; import { CaptureCommonOptionsEdit } from '../../components/capture/capture-common-options'; import { NumberEdit } from 'verification-tool/src/components/options/number-edit'; import type { NativeStackScreenProps } from '@react-navigation/native-stack'; diff --git a/react-native/verification-tool/src/screen/list-files-screen/list-files-screen.tsx b/react-native/verification-tool/src/screen/list-files-screen/list-files-screen.tsx index d2f3e4fb50..c771b4a5c9 100644 --- a/react-native/verification-tool/src/screen/list-files-screen/list-files-screen.tsx +++ b/react-native/verification-tool/src/screen/list-files-screen/list-files-screen.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'; import { StatusBar, Text, View, ScrollView, Alert } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import styles from './styles'; -import { FileTypeEnum, StorageEnum } from 'theta-client-react-native'; +import { FileTypeEnum, StorageEnum } from '../../modules/theta-client'; import Button from '../../components/ui/button'; import { ListFilesView } from '../../components/list-files-view'; import { ItemSelectorView } from '../../components/ui/item-list'; diff --git a/react-native/verification-tool/src/screen/live-preview-screen/live-preview-screen.tsx b/react-native/verification-tool/src/screen/live-preview-screen/live-preview-screen.tsx index 025a5fb115..b030ec94ed 100644 --- a/react-native/verification-tool/src/screen/live-preview-screen/live-preview-screen.tsx +++ b/react-native/verification-tool/src/screen/live-preview-screen/live-preview-screen.tsx @@ -13,7 +13,7 @@ import { isInitialized, stopLivePreview, THETA_EVENT_NAME, -} from 'theta-client-react-native'; +} from '../../modules/theta-client'; import { useIsFocused } from '@react-navigation/native'; import Button from '../../components/ui/button'; import WebView from 'react-native-webview'; diff --git a/react-native/verification-tool/src/screen/menu-screen/menu-screen.tsx b/react-native/verification-tool/src/screen/menu-screen/menu-screen.tsx index 00839c8b5d..3ec38f21c8 100644 --- a/react-native/verification-tool/src/screen/menu-screen/menu-screen.tsx +++ b/react-native/verification-tool/src/screen/menu-screen/menu-screen.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { StatusBar, View, Alert, ScrollView, Text } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; -import { getThetaModel, initialize } from 'theta-client-react-native'; +import { getThetaModel, initialize } from '../../modules/theta-client'; import styles from './styles'; import Button from '../../components/ui/button'; import { ItemListPopupView } from '../../components/ui/item-list/item-list-popup-view'; diff --git a/react-native/verification-tool/src/screen/multi-bracket-capture-screen/multi-bracket-capture-screen.tsx b/react-native/verification-tool/src/screen/multi-bracket-capture-screen/multi-bracket-capture-screen.tsx index f5d167f561..3926a9ff0e 100644 --- a/react-native/verification-tool/src/screen/multi-bracket-capture-screen/multi-bracket-capture-screen.tsx +++ b/react-native/verification-tool/src/screen/multi-bracket-capture-screen/multi-bracket-capture-screen.tsx @@ -13,7 +13,7 @@ import { getOptions, setOptions, stopSelfTimer, -} from 'theta-client-react-native'; +} from '../../modules/theta-client'; import { AutoBracketEdit } from '../../components/options/auto-bracket'; import { InputNumber } from '../../components/ui/input-number'; import type { NativeStackScreenProps } from '@react-navigation/native-stack'; diff --git a/react-native/verification-tool/src/screen/options-screen/options-screen.tsx b/react-native/verification-tool/src/screen/options-screen/options-screen.tsx index 12504f4988..b56d58eb69 100644 --- a/react-native/verification-tool/src/screen/options-screen/options-screen.tsx +++ b/react-native/verification-tool/src/screen/options-screen/options-screen.tsx @@ -29,7 +29,7 @@ import { offDelayToSeconds, setOptions, sleepDelayToSeconds, -} from 'theta-client-react-native'; +} from '../../modules/theta-client'; import { AutoBracketEdit, GpsInfoEdit, diff --git a/react-native/verification-tool/src/screen/photo-capture-screen/photo-capture-screen.tsx b/react-native/verification-tool/src/screen/photo-capture-screen/photo-capture-screen.tsx index ecbffd3205..eb61b1b5e8 100644 --- a/react-native/verification-tool/src/screen/photo-capture-screen/photo-capture-screen.tsx +++ b/react-native/verification-tool/src/screen/photo-capture-screen/photo-capture-screen.tsx @@ -12,7 +12,7 @@ import { getPhotoCaptureBuilder, setOptions, stopSelfTimer, -} from 'theta-client-react-native'; +} from '../../modules/theta-client'; import { CaptureCommonOptionsEdit } from '../../components/capture/capture-common-options'; import type { NativeStackScreenProps } from '@react-navigation/native-stack'; import type { RootStackParamList } from '../../App'; diff --git a/react-native/verification-tool/src/screen/shot-count-specified-interval-capture-screen/shot-count-specified-interval-capture-screen.tsx b/react-native/verification-tool/src/screen/shot-count-specified-interval-capture-screen/shot-count-specified-interval-capture-screen.tsx index ab2ff7321f..7d5205050b 100644 --- a/react-native/verification-tool/src/screen/shot-count-specified-interval-capture-screen/shot-count-specified-interval-capture-screen.tsx +++ b/react-native/verification-tool/src/screen/shot-count-specified-interval-capture-screen/shot-count-specified-interval-capture-screen.tsx @@ -8,7 +8,7 @@ import { ShotCountSpecifiedIntervalCapture, getShotCountSpecifiedIntervalCaptureBuilder, stopSelfTimer, -} from 'theta-client-react-native'; +} from '../../modules/theta-client'; import { CaptureCommonOptionsEdit } from '../../components/capture/capture-common-options'; import { InputNumber } from '../../components/ui/input-number'; import { NumberEdit } from 'verification-tool/src/components/options/number-edit'; diff --git a/react-native/verification-tool/src/screen/time-shift-capture-screen/time-shift-capture-screen.tsx b/react-native/verification-tool/src/screen/time-shift-capture-screen/time-shift-capture-screen.tsx index 25d985e11a..269578f2bd 100644 --- a/react-native/verification-tool/src/screen/time-shift-capture-screen/time-shift-capture-screen.tsx +++ b/react-native/verification-tool/src/screen/time-shift-capture-screen/time-shift-capture-screen.tsx @@ -10,7 +10,7 @@ import { getTimeShiftCaptureBuilder, setOptions, stopSelfTimer, -} from 'theta-client-react-native'; +} from '../../modules/theta-client'; import { CaptureCommonOptionsEdit } from '../../components/capture/capture-common-options'; import { TimeShiftEdit } from '../../components/options/time-shift'; import { InputNumber } from '../../components/ui/input-number'; diff --git a/react-native/verification-tool/src/screen/video-capture-screen/video-capture-screen.tsx b/react-native/verification-tool/src/screen/video-capture-screen/video-capture-screen.tsx index 351e3ad1a0..665bd07d7f 100644 --- a/react-native/verification-tool/src/screen/video-capture-screen/video-capture-screen.tsx +++ b/react-native/verification-tool/src/screen/video-capture-screen/video-capture-screen.tsx @@ -12,7 +12,7 @@ import { getVideoCaptureBuilder, setOptions, stopSelfTimer, -} from 'theta-client-react-native'; +} from '../../modules/theta-client'; import { CaptureCommonOptionsEdit } from '../../components/capture/capture-common-options'; import type { NativeStackScreenProps } from '@react-navigation/native-stack'; import type { RootStackParamList } from '../../App'; diff --git a/react-native/verification-tool/src/screen/video-convert-screen/video-convert-screen.tsx b/react-native/verification-tool/src/screen/video-convert-screen/video-convert-screen.tsx index 634ee377e0..515c3b183e 100644 --- a/react-native/verification-tool/src/screen/video-convert-screen/video-convert-screen.tsx +++ b/react-native/verification-tool/src/screen/video-convert-screen/video-convert-screen.tsx @@ -8,7 +8,7 @@ import { FileInfo, convertVideoFormats, cancelVideoConvert, -} from 'theta-client-react-native'; +} from '../../modules/theta-client'; import Button from '../../components/ui/button'; import { TitledSwitch } from '../../components/ui/titled-switch'; import { ListFilesView } from '../../components/list-files-view'; From b44b29903897092d0c1c996a06a4a9e624a8c7bb Mon Sep 17 00:00:00 2001 From: osakila Date: Tue, 18 Jun 2024 15:29:11 +0900 Subject: [PATCH 02/13] Improve self-timer in PhotoCapture --- .../thetaClientDemo/PreviewScreen.kt | 5 + .../demo-flutter/lib/take_picture_screen.dart | 2 + demos/demo-ios/SdkSample/ThetaSdk.swift | 4 +- .../theta_client_flutter/ConvertUtil.kt | 12 + .../ThetaClientFlutterPlugin.kt | 8 + flutter/ios/Classes/ConvertUtil.swift | 12 + .../SwiftThetaClientFlutterPlugin.swift | 13 +- flutter/lib/capture/capture.dart | 38 ++- flutter/lib/capture/capture_builder.dart | 11 +- .../theta_client_flutter_method_channel.dart | 34 ++- ...eta_client_flutter_platform_interface.dart | 5 +- .../photo_capture_method_channel_test.dart | 36 ++- flutter/test/capture/photo_capture_test.dart | 57 +++- flutter/test/enum_name_test.dart | 12 + flutter/test/theta_client_flutter_test.dart | 17 +- .../ricoh360/thetaclient/capture/Capture.kt | 17 ++ .../capture/CaptureStatusMonitor.kt | 18 +- .../thetaclient/capture/PhotoCapture.kt | 62 +++- .../thetaclient/capture/PhotoCaptureTest.kt | 269 +++++++++++++++--- .../resources/PhotoCapture/state_idle.json | 22 ++ .../PhotoCapture/state_self_timer.json | 1 + .../thetaclientreactnative/Converter.kt | 14 + .../ThetaClientSdkModule.kt | 8 + react-native/ios/ConvertUtil.swift | 12 + react-native/ios/ThetaClientReactNative.swift | 25 +- .../__tests__/capture/photo-capture.test.ts | 68 ++++- react-native/src/capture/capture.ts | 12 + .../src/capture/multi-bracket-capture.ts | 2 +- react-native/src/capture/photo-capture.ts | 57 +++- .../photo-capture-screen.tsx | 22 +- .../screen/photo-capture-screen/styles.tsx | 6 + 31 files changed, 790 insertions(+), 91 deletions(-) create mode 100644 kotlin-multiplatform/src/commonTest/resources/PhotoCapture/state_idle.json create mode 100644 kotlin-multiplatform/src/commonTest/resources/PhotoCapture/state_self_timer.json diff --git a/demos/demo-android/app/src/main/java/com/ricoh360/thetaclient/thetaClientDemo/PreviewScreen.kt b/demos/demo-android/app/src/main/java/com/ricoh360/thetaclient/thetaClientDemo/PreviewScreen.kt index aa3721ea73..729bb5e498 100755 --- a/demos/demo-android/app/src/main/java/com/ricoh360/thetaclient/thetaClientDemo/PreviewScreen.kt +++ b/demos/demo-android/app/src/main/java/com/ricoh360/thetaclient/thetaClientDemo/PreviewScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalLifecycleOwner import com.ricoh360.thetaclient.ThetaRepository +import com.ricoh360.thetaclient.capture.CapturingStatusEnum import com.ricoh360.thetaclient.capture.PhotoCapture import com.ricoh360.thetaclient.thetaClientDemo.ui.theme.ThetaSimpleAndroidAppTheme import kotlinx.coroutines.CoroutineScope @@ -51,6 +52,10 @@ fun PreviewScreen(toPhoto: (photoUrl: String) -> Unit, viewModel: ThetaViewModel } } + override fun onCapturing(status: CapturingStatusEnum) { + Timber.i("takePicture onCapturing: ${status.name}") + } + override fun onError(exception: ThetaRepository.ThetaRepositoryException) { Timber.e(exception) } diff --git a/demos/demo-flutter/lib/take_picture_screen.dart b/demos/demo-flutter/lib/take_picture_screen.dart index 5db74978b8..339cbe7a01 100644 --- a/demos/demo-flutter/lib/take_picture_screen.dart +++ b/demos/demo-flutter/lib/take_picture_screen.dart @@ -212,6 +212,8 @@ class _TakePictureScreen extends State shooting = false; }); debugPrint(exception.toString()); + }, onCapturing: (status) { + debugPrint("onCapturing: $status"); }); } } diff --git a/demos/demo-ios/SdkSample/ThetaSdk.swift b/demos/demo-ios/SdkSample/ThetaSdk.swift index cf0c171e40..8c0203fc2d 100644 --- a/demos/demo-ios/SdkSample/ThetaSdk.swift +++ b/demos/demo-ios/SdkSample/ThetaSdk.swift @@ -151,7 +151,9 @@ class Theta { callback(fileUrl, nil) } - func onProgress(completion _: Float) {} + func onCapturing(status: CapturingStatusEnum) { + print("takePicture onCapturing: " + status.name) + } func onError(exception: ThetaException) { callback(nil, exception.asError()) diff --git a/flutter/android/src/main/kotlin/com/ricoh360/thetaclient/theta_client_flutter/ConvertUtil.kt b/flutter/android/src/main/kotlin/com/ricoh360/thetaclient/theta_client_flutter/ConvertUtil.kt index bf5f0abe94..dd3702d9a7 100644 --- a/flutter/android/src/main/kotlin/com/ricoh360/thetaclient/theta_client_flutter/ConvertUtil.kt +++ b/flutter/android/src/main/kotlin/com/ricoh360/thetaclient/theta_client_flutter/ConvertUtil.kt @@ -11,6 +11,7 @@ const val KEY_NOTIFY_PARAMS = "params" const val KEY_NOTIFY_PARAM_COMPLETION = "completion" const val KEY_NOTIFY_PARAM_IMAGE = "image" const val KEY_NOTIFY_PARAM_MESSAGE = "message" +const val KEY_NOTIFY_PARAM_STATUS = "status" const val KEY_GPS_INFO = "gpsInfo" const val KEY_STATE_EXTERNAL_GPS_INFO = "externalGpsInfo" const val KEY_STATE_INTERNAL_GPS_INFO = "internalGpsInfo" @@ -344,6 +345,11 @@ fun setCaptureBuilderParams(call: MethodCall, builder: Capture.Builder) { } fun setPhotoCaptureBuilderParams(call: MethodCall, builder: PhotoCapture.Builder) { + call.argument("_capture_interval")?.let { + if (it >= 0) { + builder.setCheckStatusCommandInterval(it.toLong()) + } + } call.argument(OptionNameEnum.Filter.name)?.let { enumName -> FilterEnum.values().find { it.name == enumName }?.let { builder.setFilter(it) @@ -906,3 +912,9 @@ fun toMessageNotifyParam(message: String): Map { KEY_NOTIFY_PARAM_MESSAGE to message ) } + +fun toCapturingNotifyParam(status: CapturingStatusEnum): Map { + return mapOf( + KEY_NOTIFY_PARAM_STATUS to status.name + ) +} diff --git a/flutter/android/src/main/kotlin/com/ricoh360/thetaclient/theta_client_flutter/ThetaClientFlutterPlugin.kt b/flutter/android/src/main/kotlin/com/ricoh360/thetaclient/theta_client_flutter/ThetaClientFlutterPlugin.kt index 6aec8756ed..4ba7ac9026 100644 --- a/flutter/android/src/main/kotlin/com/ricoh360/thetaclient/theta_client_flutter/ThetaClientFlutterPlugin.kt +++ b/flutter/android/src/main/kotlin/com/ricoh360/thetaclient/theta_client_flutter/ThetaClientFlutterPlugin.kt @@ -76,6 +76,7 @@ class ThetaClientFlutterPlugin : FlutterPlugin, MethodCallHandler { const val notifyIdBurstCaptureProgress = 10051; const val notifyIdBurstCaptureStopError = 10052; const val notifyIdContinuousCaptureProgress = 10061; + const val notifyIdCapturingStatus = 10071 } fun sendNotifyEvent(id: Int, params: Map) { @@ -666,6 +667,13 @@ class ThetaClientFlutterPlugin : FlutterPlugin, MethodCallHandler { result.success(fileUrl) } + override fun onCapturing(status: CapturingStatusEnum) { + sendNotifyEvent( + notifyIdCapturingStatus, + toCapturingNotifyParam(status) + ) + } + override fun onError(exception: ThetaRepository.ThetaRepositoryException) { result.error(exception.javaClass.simpleName, exception.message, null) } diff --git a/flutter/ios/Classes/ConvertUtil.swift b/flutter/ios/Classes/ConvertUtil.swift index 407732d99d..028a76e822 100644 --- a/flutter/ios/Classes/ConvertUtil.swift +++ b/flutter/ios/Classes/ConvertUtil.swift @@ -8,6 +8,7 @@ let KEY_NOTIFY_PARAMS = "params" let KEY_NOTIFY_PARAM_COMPLETION = "completion" let KEY_NOTIFY_PARAM_IMAGE = "image" let KEY_NOTIFY_PARAM_MESSAGE = "message" +let KEY_NOTIFY_PARAM_STATUS = "status" let KEY_GPS_INFO = "gpsInfo" let KEY_STATE_EXTERNAL_GPS_INFO = "externalGpsInfo" let KEY_STATE_INTERNAL_GPS_INFO = "internalGpsInfo" @@ -254,6 +255,11 @@ func setCaptureBuilderParams(params: [String: Any], builder: CaptureBuilder= 0 + { + builder.setCheckStatusCommandInterval(timeMillis: Int64(interval)) + } if let value = params[ThetaRepository.OptionNameEnum.filter.name] as? String { if let enumValue = getEnumValue(values: ThetaRepository.FilterEnum.values(), name: value) { builder.setFilter(filter: enumValue) @@ -1104,3 +1110,9 @@ func toMessageNotifyParam(message: String) -> [String: Any] { KEY_NOTIFY_PARAM_MESSAGE: message, ] } + +func toCapturingNotifyParam(value: CapturingStatusEnum) -> [String: Any] { + return [ + KEY_NOTIFY_PARAM_STATUS: value.name, + ] +} diff --git a/flutter/ios/Classes/SwiftThetaClientFlutterPlugin.swift b/flutter/ios/Classes/SwiftThetaClientFlutterPlugin.swift index 2130d26edb..2a09843160 100644 --- a/flutter/ios/Classes/SwiftThetaClientFlutterPlugin.swift +++ b/flutter/ios/Classes/SwiftThetaClientFlutterPlugin.swift @@ -17,6 +17,7 @@ let NOTIFY_MULTI_BRACKET_INTERVAL_STOP_ERROR = 10042 let NOTIFY_BURST_PROGRESS = 10051 let NOTIFY_BURST_STOP_ERROR = 10052 let NOTIFY_CONTINUOUS_PROGRESS = 10061 +let NOTIFY_CAPTURING_STATUS = 10071 public class SwiftThetaClientFlutterPlugin: NSObject, FlutterPlugin, FlutterStreamHandler { public func onListen(withArguments _: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { @@ -547,29 +548,33 @@ public class SwiftThetaClientFlutterPlugin: NSObject, FlutterPlugin, FlutterStre class Callback: PhotoCaptureTakePictureCallback { let callback: (_ url: String?, _ error: Error?) -> Void - init(_ callback: @escaping (_ url: String?, _ error: Error?) -> Void) { + weak var plugin: SwiftThetaClientFlutterPlugin? + init(_ callback: @escaping (_ url: String?, _ error: Error?) -> Void, plugin: SwiftThetaClientFlutterPlugin) { self.callback = callback + self.plugin = plugin } func onSuccess(fileUrl: String?) { callback(fileUrl, nil) } - func onProgress(completion _: Float) {} + func onCapturing(status: CapturingStatusEnum) { + plugin?.sendNotifyEvent(id: NOTIFY_CAPTURING_STATUS, params: toCapturingNotifyParam(value: status)) + } func onError(exception: ThetaRepository.ThetaRepositoryException) { callback(nil, exception.asError()) } } photoCapture!.takePicture( - callback: Callback { fileUrl, error in + callback: Callback({ fileUrl, error in if let thetaError = error { let flutterError = FlutterError(code: SwiftThetaClientFlutterPlugin.errorCode, message: thetaError.localizedDescription, details: nil) result(flutterError) } else { result(fileUrl) } - } + }, plugin: self) ) } diff --git a/flutter/lib/capture/capture.dart b/flutter/lib/capture/capture.dart index 0c32a2f97e..5ebf80ecea 100644 --- a/flutter/lib/capture/capture.dart +++ b/flutter/lib/capture/capture.dart @@ -52,9 +52,40 @@ class Capture { Capture(this._options); } +/// Capturing status +enum CapturingStatusEnum { + /// Capture in progress + capturing('CAPTURING'), + + /// Self-timer in progress + selfTimerCountdown('SELF_TIMER_COUNTDOWN'), + ; + + final String rawValue; + + const CapturingStatusEnum(this.rawValue); + + @override + String toString() { + return rawValue; + } + + static CapturingStatusEnum? getValue(String rawValue) { + return CapturingStatusEnum.values.cast().firstWhere( + (element) => element?.rawValue == rawValue, + orElse: () => null); + } +} + /// Capture of Photo class PhotoCapture extends Capture { - PhotoCapture(super.options); + final int _interval; + + PhotoCapture(super.options, this._interval); + + int getCheckStatusCommandInterval() { + return _interval; + } /// Get image processing filter. FilterEnum? getFilter() { @@ -73,9 +104,10 @@ class PhotoCapture extends Capture { /// Take a picture. void takePicture(void Function(String? fileUrl) onSuccess, - void Function(Exception exception) onError) { + void Function(Exception exception) onError, + {void Function(CapturingStatusEnum status)? onCapturing}) { ThetaClientFlutterPlatform.instance - .takePicture() + .takePicture(onCapturing) .then((value) => onSuccess(value!)) .onError((error, stackTrace) => onError(error as Exception)); } diff --git a/flutter/lib/capture/capture_builder.dart b/flutter/lib/capture/capture_builder.dart index 6c03528da3..d6f83e420b 100644 --- a/flutter/lib/capture/capture_builder.dart +++ b/flutter/lib/capture/capture_builder.dart @@ -84,6 +84,13 @@ class CaptureBuilder { /// Builder of [PhotoCapture] class PhotoCaptureBuilder extends CaptureBuilder { + int _interval = -1; + + PhotoCaptureBuilder setCheckStatusCommandInterval(int timeMillis) { + _interval = timeMillis; + return this; + } + /// Set photo file format. PhotoCaptureBuilder setFileFormat(PhotoFileFormatEnum fileFormat) { _options[TagNameEnum.photoFileFormat.rawValue] = fileFormat; @@ -106,8 +113,8 @@ class PhotoCaptureBuilder extends CaptureBuilder { Future build() async { var completer = Completer(); try { - await ThetaClientFlutterPlatform.instance.buildPhotoCapture(_options); - completer.complete(PhotoCapture(_options)); + await ThetaClientFlutterPlatform.instance.buildPhotoCapture(_options, _interval); + completer.complete(PhotoCapture(_options, _interval)); } catch (e) { completer.completeError(e); } diff --git a/flutter/lib/theta_client_flutter_method_channel.dart b/flutter/lib/theta_client_flutter_method_channel.dart index ed9cf3ad75..ecebcb3c73 100644 --- a/flutter/lib/theta_client_flutter_method_channel.dart +++ b/flutter/lib/theta_client_flutter_method_channel.dart @@ -21,6 +21,7 @@ const notifyIdMultiBracketCaptureStopError = 10042; const notifyIdBurstCaptureProgress = 10051; const notifyIdBurstCaptureStopError = 10052; const notifyIdContinuousCaptureProgress = 10061; +const notifyIdCapturingStatus = 10071; /// An implementation of [ThetaClientFlutterPlatform] that uses method channels. class MethodChannelThetaClientFlutter extends ThetaClientFlutterPlatform { @@ -234,14 +235,39 @@ class MethodChannelThetaClientFlutter extends ThetaClientFlutterPlatform { } @override - Future buildPhotoCapture(Map options) async { + Future buildPhotoCapture(Map options, int interval) async { + final params = ConvertUtils.convertCaptureParams(options); + params['_capture_interval'] = interval; return methodChannel.invokeMethod( - 'buildPhotoCapture', ConvertUtils.convertCaptureParams(options)); + 'buildPhotoCapture', params); } @override - Future takePicture() async { - return methodChannel.invokeMethod('takePicture'); + Future takePicture( + void Function(CapturingStatusEnum status)? onCapturing) async { + var completer = Completer(); + try { + enableNotifyEventReceiver(); + if (onCapturing != null) { + addNotify(notifyIdCapturingStatus, (params) { + final strStatus = params?['status'] as String?; + if (strStatus != null) { + final status = CapturingStatusEnum.getValue(strStatus); + if (status != null) { + onCapturing(status); + } + } + }); + } + final fileUrl = + await methodChannel.invokeMethod('takePicture'); + removeNotify(notifyIdCapturingStatus); + completer.complete(fileUrl); + } catch (e) { + removeNotify(notifyIdCapturingStatus); + completer.completeError(e); + } + return completer.future; } @override diff --git a/flutter/lib/theta_client_flutter_platform_interface.dart b/flutter/lib/theta_client_flutter_platform_interface.dart index 808345c9fc..dc96dc4bb3 100644 --- a/flutter/lib/theta_client_flutter_platform_interface.dart +++ b/flutter/lib/theta_client_flutter_platform_interface.dart @@ -92,11 +92,12 @@ abstract class ThetaClientFlutterPlatform extends PlatformInterface { 'getPhotoCaptureBuilder() has not been implemented.'); } - Future buildPhotoCapture(Map options) { + Future buildPhotoCapture(Map options, int interval) { throw UnimplementedError('buildPhotoCapture() has not been implemented.'); } - Future takePicture() { + Future takePicture( + void Function(CapturingStatusEnum status)? onCapturing) { throw UnimplementedError('takePicture() has not been implemented.'); } diff --git a/flutter/test/capture/photo_capture_method_channel_test.dart b/flutter/test/capture/photo_capture_method_channel_test.dart index a93d5aac81..0cc4926e7c 100644 --- a/flutter/test/capture/photo_capture_method_channel_test.dart +++ b/flutter/test/capture/photo_capture_method_channel_test.dart @@ -64,7 +64,7 @@ void main() { return Future.value(); }); - await platform.buildPhotoCapture(options); + await platform.buildPhotoCapture(options, 1); }); test('takePicture', () async { @@ -74,6 +74,38 @@ void main() { .setMockMethodCallHandler(channel, (MethodCall methodCall) async { return fileUrl; }); - expect(await platform.takePicture(), fileUrl); + expect(await platform.takePicture(null), fileUrl); + }); + + test('call onCapturing', () async { + const fileUrl = + 'http://192.168.1.1/files/150100524436344d4201375fda9dc400/100RICOH/R0013336.JPG'; + + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + expect(platform.notifyList.containsKey(10071), true, + reason: 'add notify capturing status'); + + await Future.delayed(const Duration(milliseconds: 5)); + // native event + platform.onNotify({ + 'id': 10071, + 'params': { + 'status': 'SELF_TIMER_COUNTDOWN', + }, + }); + await Future.delayed(const Duration(milliseconds: 5)); + + return fileUrl; + }); + + CapturingStatusEnum? lastStatus; + expect( + await platform.takePicture((status) { + expect(status, CapturingStatusEnum.selfTimerCountdown); + lastStatus = status; + }), + fileUrl); + expect(lastStatus, CapturingStatusEnum.selfTimerCountdown); }); } diff --git a/flutter/test/capture/photo_capture_test.dart b/flutter/test/capture/photo_capture_test.dart index 703ea98b12..4ec00877a3 100644 --- a/flutter/test/capture/photo_capture_test.dart +++ b/flutter/test/capture/photo_capture_test.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter_test/flutter_test.dart'; import 'package:theta_client_flutter/theta_client_flutter.dart'; import 'package:theta_client_flutter/theta_client_flutter_platform_interface.dart'; @@ -5,7 +7,9 @@ import 'package:theta_client_flutter/theta_client_flutter_platform_interface.dar import '../theta_client_flutter_test.dart'; void main() { - setUp(() {}); + setUp(() { + onCallBuildPhotoCapture = (options, interval) => Future.value(); + }); tearDown(() {}); @@ -52,7 +56,7 @@ void main() { const preset = [PresetEnum.face, 'Preset']; const whiteBalance = [WhiteBalanceEnum.auto, 'WhiteBalance']; - onCallBuildPhotoCapture = (options) { + onCallBuildPhotoCapture = (options, interval) { expect(options[aperture[1]], aperture[0]); expect(options[colorTemperature[1]], colorTemperature[0]); expect(options[exposureCompensation[1]], exposureCompensation[0]); @@ -84,6 +88,7 @@ void main() { builder.setIsoAutoHighLimit(isoAutoHighLimit[0] as IsoAutoHighLimitEnum); builder.setPreset(preset[0] as PresetEnum); builder.setWhiteBalance(whiteBalance[0] as WhiteBalanceEnum); + builder.setCheckStatusCommandInterval(100); var capture = await builder.build(); expect(capture, isNotNull); @@ -100,6 +105,7 @@ void main() { expect(capture.getIsoAutoHighLimit(), isoAutoHighLimit[0]); expect(capture.getPreset(), preset[0]); expect(capture.getWhiteBalance(), whiteBalance[0]); + expect(capture.getCheckStatusCommandInterval(), 100); }); test('takePicture', () async { @@ -111,8 +117,8 @@ void main() { const imageUrl = 'http://test.jpg'; onCallGetPhotoCaptureBuilder = Future.value; - onCallBuildPhotoCapture = Future.value; - onCallTakePicture = () { + onCallBuildPhotoCapture = (options, interval) => Future.value(); + onCallTakePicture = (onCapturing) { return Future.value(imageUrl); }; @@ -137,8 +143,8 @@ void main() { ThetaClientFlutterPlatform.instance = fakePlatform; onCallGetPhotoCaptureBuilder = Future.value; - onCallBuildPhotoCapture = Future.value; - onCallTakePicture = () { + onCallBuildPhotoCapture = (options, interval) => Future.value(); + onCallTakePicture = (onCapturing) { return Future.error(Exception('Error. takePicture')); }; @@ -157,4 +163,43 @@ void main() { expect(fileUrl, isNull); expect(error, isNotNull); }); + + test('call onCapturing', () async { + ThetaClientFlutter thetaClientPlugin = ThetaClientFlutter(); + MockThetaClientFlutterPlatform fakePlatform = + MockThetaClientFlutterPlatform(); + ThetaClientFlutterPlatform.instance = fakePlatform; + + var completer = Completer(); + void Function(CapturingStatusEnum status)? paramOnCapturing; + + const imageUrl = 'http://test.jpg'; + + onCallGetPhotoCaptureBuilder = Future.value; + onCallBuildPhotoCapture = (options, interval) => Future.value(); + onCallTakePicture = (onCapturing) { + paramOnCapturing = onCapturing; + return Future.value(imageUrl); + }; + + var builder = thetaClientPlugin.getPhotoCaptureBuilder(); + var isOnCapturing = false; + var capture = await builder.build(); + String? fileUrl; + capture.takePicture((value) { + expect(value, imageUrl); + fileUrl = value; + completer.complete(null); + }, (exception) { + expect(false, isTrue, reason: 'Error. takePicture'); + }, onCapturing: (status) { + isOnCapturing = true; + expect(status, CapturingStatusEnum.capturing); + }); + paramOnCapturing?.call(CapturingStatusEnum.capturing); + + await Future.delayed(const Duration(milliseconds: 10), () {}); + expect(fileUrl, imageUrl); + expect(isOnCapturing, isTrue); + }); } diff --git a/flutter/test/enum_name_test.dart b/flutter/test/enum_name_test.dart index 4275ecd493..f61ac8abef 100644 --- a/flutter/test/enum_name_test.dart +++ b/flutter/test/enum_name_test.dart @@ -898,4 +898,16 @@ void main() { expect(data[i][0].toString(), data[i][1], reason: data[i][1]); } }); + + test('CapturingStatusEnum', () async { + List> data = [ + [CapturingStatusEnum.capturing, 'CAPTURING'], + [CapturingStatusEnum.selfTimerCountdown, 'SELF_TIMER_COUNTDOWN'], + ]; + expect(data.length, CapturingStatusEnum.values.length, + reason: 'enum count'); + for (int i = 0; i < data.length; i++) { + expect(data[i][0].toString(), data[i][1], reason: data[i][1]); + } + }); } diff --git a/flutter/test/theta_client_flutter_test.dart b/flutter/test/theta_client_flutter_test.dart index 1f56d465ee..e0c710c191 100644 --- a/flutter/test/theta_client_flutter_test.dart +++ b/flutter/test/theta_client_flutter_test.dart @@ -60,13 +60,14 @@ class MockThetaClientFlutterPlatform } @override - Future buildPhotoCapture(Map options) { - return onCallBuildPhotoCapture(options); + Future buildPhotoCapture(Map options, int interval) { + return onCallBuildPhotoCapture(options, interval); } @override - Future takePicture() { - return onCallTakePicture(); + Future takePicture( + void Function(CapturingStatusEnum status)? onCapturing) { + return onCallTakePicture(onCapturing); } @override @@ -417,10 +418,12 @@ Future Function() onGetThetaLicense = Future.value; Future Function() onGetThetaState = Future.value; Future Function() onCallGetLivePreview = Future.value; Future Function() onCallListFiles = Future.value; + Future Function() onCallGetPhotoCaptureBuilder = Future.value; -Future Function(Map options) onCallBuildPhotoCapture = - Future.value; -Future Function() onCallTakePicture = Future.value; +Future Function(Map options, int interval) onCallBuildPhotoCapture = + (options, interval) => Future.value(); +Future Function(void Function(CapturingStatusEnum)? onCapturing) + onCallTakePicture = (onCapturing) => Future.value(); Future Function() onCallGetTimeShiftCaptureBuilder = Future.value; Future Function(Map options, int interval) diff --git a/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/capture/Capture.kt b/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/capture/Capture.kt index 0d345d751b..437bb80273 100644 --- a/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/capture/Capture.kt +++ b/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/capture/Capture.kt @@ -6,6 +6,23 @@ import com.ricoh360.thetaclient.transferred.UnknownResponse import io.ktor.client.call.body import io.ktor.client.statement.HttpResponse +/** + * Capturing status + * + * Identify the self-timer during capture + */ +enum class CapturingStatusEnum { + /** + * Capture in progress + */ + CAPTURING, + + /** + * Self-timer in progress + */ + SELF_TIMER_COUNTDOWN, +} + /* * Capture * diff --git a/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/capture/CaptureStatusMonitor.kt b/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/capture/CaptureStatusMonitor.kt index 261adfc115..9bcebee243 100644 --- a/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/capture/CaptureStatusMonitor.kt +++ b/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/capture/CaptureStatusMonitor.kt @@ -4,6 +4,7 @@ import com.ricoh360.thetaclient.ThetaApi import com.ricoh360.thetaclient.transferred.CaptureStatus import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -12,11 +13,13 @@ internal class CaptureStatusMonitor( var onChangeStatus: ((newStatus: CaptureStatus, oldStatus: CaptureStatus?) -> Unit), var onError: ((error: Throwable) -> Unit), val checkStateInterval: Long = CHECK_STATE_INTERVAL, + val checkShootingIdleCount: Int = CHECK_SHOOTING_IDLE_COUNT, ) { private var isStartMonitor = false private val scope = CoroutineScope(Dispatchers.Default) var currentStatus: CaptureStatus? = null var lastException: Throwable? = null + var job: Job? = null companion object { private const val CHECK_STATE_INTERVAL = 1000L @@ -25,9 +28,12 @@ internal class CaptureStatusMonitor( } fun start() { + if (isStartMonitor) { + return + } isStartMonitor = true - scope.launch { - var idleCount = CHECK_SHOOTING_IDLE_COUNT + job = scope.launch { + var idleCount = checkShootingIdleCount while (isStartMonitor) { when (val status = getCaptureStatus()) { CaptureStatus.IDLE -> { @@ -43,7 +49,7 @@ internal class CaptureStatusMonitor( } else -> { - idleCount = CHECK_SHOOTING_IDLE_COUNT + idleCount = checkShootingIdleCount updateStatus(status) } } @@ -56,10 +62,14 @@ internal class CaptureStatusMonitor( fun stop() { isStartMonitor = false + job?.let { + it.cancel() + job = null + } } private fun updateStatus(status: CaptureStatus) { - if (currentStatus == status) { + if (!isStartMonitor || currentStatus == status) { return } val oldStatus = currentStatus diff --git a/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/capture/PhotoCapture.kt b/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/capture/PhotoCapture.kt index 2bd608332f..93985092ad 100644 --- a/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/capture/PhotoCapture.kt +++ b/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/capture/PhotoCapture.kt @@ -14,10 +14,18 @@ import kotlinx.coroutines.* * @property endpoint URL of Theta web API endpoint * @property options option of take a picture */ -class PhotoCapture private constructor(private val endpoint: String, options: Options) : Capture(options) { +class PhotoCapture private constructor( + private val endpoint: String, + options: Options, + private val checkStatusCommandInterval: Long + ) : Capture(options) { private val scope = CoroutineScope(Dispatchers.Default) + fun getCheckStatusCommandInterval(): Long { + return checkStatusCommandInterval + } + /** * Get image processing filter. * @@ -30,8 +38,8 @@ class PhotoCapture private constructor(private val endpoint: String, options: Op * * @return Photo file format */ - fun getFileFormat() = options.fileFormat?.let { it -> - ThetaRepository.FileFormatEnum.get(it)?.let { + fun getFileFormat() = options.fileFormat?.let { fileFormat -> + ThetaRepository.FileFormatEnum.get(fileFormat).let { ThetaRepository.PhotoFileFormatEnum.get(it) } } @@ -59,11 +67,11 @@ class PhotoCapture private constructor(private val endpoint: String, options: Op fun onSuccess(fileUrl: String?) /** - * Called when state "inProgress". + * Called when change capture status. * - * @param completion Progress rate of command executed + * @param status Capturing status */ - fun onProgress(completion: Float) {} + fun onCapturing(status: CapturingStatusEnum) {} /** * Called when error occurs. @@ -81,21 +89,41 @@ class PhotoCapture private constructor(private val endpoint: String, options: Op fun takePicture(callback: TakePictureCallback) { scope.launch { lateinit var takePictureResponse: TakePictureResponse + val monitor = CaptureStatusMonitor( + endpoint, + { newStatus, _ -> + when (newStatus) { + CaptureStatus.SELF_TIMER_COUNTDOWN -> callback.onCapturing( + CapturingStatusEnum.SELF_TIMER_COUNTDOWN + ) + + else -> callback.onCapturing(CapturingStatusEnum.CAPTURING) + } + }, + { error -> + println("CaptureStatusMonitor error: ${error.message}") + }, + checkStatusCommandInterval, + 1 + ) try { takePictureResponse = ThetaApi.callTakePictureCommand(endpoint = endpoint) + monitor.start() val id = takePictureResponse.id while (takePictureResponse.state == CommandState.IN_PROGRESS) { - delay(timeMillis = CHECK_COMMAND_STATUS_INTERVAL) + delay(timeMillis = checkStatusCommandInterval) takePictureResponse = ThetaApi.callStatusApi( endpoint = endpoint, params = StatusApiParams(id = id) ) as TakePictureResponse - callback.onProgress(completion = takePictureResponse.progress?.completion ?: 0f) } + monitor.stop() } catch (e: JsonConvertException) { + monitor.stop() callback.onError(exception = ThetaRepository.ThetaWebApiException(message = e.message ?: e.toString())) return@launch } catch (e: ResponseException) { + monitor.stop() if (isCanceledShootingResponse(e.response)) { callback.onSuccess(fileUrl = null) // canceled } else { @@ -103,6 +131,7 @@ class PhotoCapture private constructor(private val endpoint: String, options: Op } return@launch } catch (e: Exception) { + monitor.stop() callback.onError(exception = ThetaRepository.NotConnectedException(message = e.message ?: e.toString())) return@launch } @@ -127,7 +156,11 @@ class PhotoCapture private constructor(private val endpoint: String, options: Op * @property endpoint URL of Theta web API endpoint * @property cameraModel Camera model info. */ - class Builder internal constructor(private val endpoint: String, private val cameraModel: ThetaRepository.ThetaModel? = null) : Capture.Builder() { + class Builder internal constructor( + private val endpoint: String, + private val cameraModel: ThetaRepository.ThetaModel? = null + ) : Capture.Builder() { + private var interval: Long? = null internal fun isPreset(): Boolean { return options._preset != null && (cameraModel == ThetaRepository.ThetaModel.THETA_SC2 || cameraModel == ThetaRepository.ThetaModel.THETA_SC2_B) @@ -172,7 +205,16 @@ class PhotoCapture private constructor(private val endpoint: String, options: Op } catch (e: Exception) { throw ThetaRepository.NotConnectedException(e.message ?: e.toString()) } - return PhotoCapture(endpoint, options) + return PhotoCapture( + endpoint = endpoint, + options = options, + checkStatusCommandInterval = interval ?: CHECK_COMMAND_STATUS_INTERVAL, + ) + } + + fun setCheckStatusCommandInterval(timeMillis: Long): Builder { + this.interval = timeMillis + return this } /** diff --git a/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/capture/PhotoCaptureTest.kt b/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/capture/PhotoCaptureTest.kt index e60b9daa68..538ed92a06 100644 --- a/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/capture/PhotoCaptureTest.kt +++ b/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/capture/PhotoCaptureTest.kt @@ -3,21 +3,18 @@ package com.ricoh360.thetaclient.capture import com.goncalossilva.resources.Resource import com.ricoh360.thetaclient.CheckRequest import com.ricoh360.thetaclient.MockApiClient +import com.ricoh360.thetaclient.ThetaApi import com.ricoh360.thetaclient.ThetaRepository import com.ricoh360.thetaclient.transferred.CaptureMode import io.ktor.client.network.sockets.* -import io.ktor.client.request.* import io.ktor.http.* -import io.ktor.http.content.* import io.ktor.utils.io.* import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import kotlinx.coroutines.withTimeout import kotlin.test.* -@OptIn(ExperimentalCoroutinesApi::class) class PhotoCaptureTest { private val endpoint = "http://192.168.1.1:80/" @@ -42,6 +39,8 @@ class PhotoCaptureTest { Resource("src/commonTest/resources/PhotoCapture/takepicture_progress.json").readText(), Resource("src/commonTest/resources/PhotoCapture/takepicture_done.json").readText() ) + val stateIdleResponse = + Resource("src/commonTest/resources/PhotoCapture/state_idle.json").readText() val requestPathArray = arrayOf( "/osc/commands/execute", "/osc/commands/execute", @@ -49,28 +48,53 @@ class PhotoCaptureTest { ) var counter = 0 MockApiClient.onRequest = { request -> - val index = counter++ - // check request - assertEquals(request.url.encodedPath, requestPathArray[index], "take picture request") - when (index) { + val response = when (val index = counter++) { 0 -> { + assertEquals( + request.url.encodedPath, + requestPathArray[index], + "take picture request" + ) CheckRequest.checkSetOptions(request = request, captureMode = CaptureMode.IMAGE) + responseArray[index] } 1 -> { + assertEquals( + request.url.encodedPath, + requestPathArray[index], + "take picture request" + ) CheckRequest.checkCommandName(request, "camera.takePicture") + responseArray[index] + } + + else -> { + when (request.url.encodedPath) { + "/osc/commands/status" -> { + assertEquals( + request.url.encodedPath, + requestPathArray[2], + "take picture request" + ) + responseArray[2] + } + else -> stateIdleResponse + } } } - ByteReadChannel(responseArray[index]) + ByteReadChannel(response) } val deferred = CompletableDeferred() // execute val thetaRepository = ThetaRepository(endpoint) val photoCapture = thetaRepository.getPhotoCaptureBuilder() + .setCheckStatusCommandInterval(100) .build() + ThetaApi.lastSetTimeConsumingOptionTime = 0 assertNull(photoCapture.getFilter(), "set option filter") assertNull(photoCapture.getFileFormat(), "set option fileFormat") @@ -82,6 +106,10 @@ class PhotoCaptureTest { deferred.complete(Unit) } + override fun onCapturing(status: CapturingStatusEnum) { + assertEquals(status, CapturingStatusEnum.CAPTURING) + } + override fun onError(exception: ThetaRepository.ThetaRepositoryException) { assertTrue(false, "error take picture") deferred.complete(Unit) @@ -108,17 +136,32 @@ class PhotoCaptureTest { Resource("src/commonTest/resources/PhotoCapture/takepicture_progress.json").readText(), Resource("src/commonTest/resources/PhotoCapture/takepicture_cancel.json").readText() ) + val stateIdleResponse = + Resource("src/commonTest/resources/PhotoCapture/state_idle.json").readText() var counter = 0 - MockApiClient.onRequest = { _ -> - val index = counter++ - ByteReadChannel(responseArray[index]) + MockApiClient.onRequest = { request -> + val response = when (val index = counter++) { + 0, 1 -> { + responseArray[index] + } + + else -> { + when (request.url.encodedPath) { + "/osc/commands/status" -> responseArray[2] + else -> stateIdleResponse + } + } + } + ByteReadChannel(response) } val deferred = CompletableDeferred() // execute val thetaRepository = ThetaRepository(endpoint) val photoCapture = thetaRepository.getPhotoCaptureBuilder() + .setCheckStatusCommandInterval(100) .build() + ThetaApi.lastSetTimeConsumingOptionTime = 0 var file: String? = "" photoCapture.takePicture(object : PhotoCapture.TakePictureCallback { @@ -153,19 +196,35 @@ class PhotoCaptureTest { Resource("src/commonTest/resources/PhotoCapture/takepicture_progress.json").readText(), Resource("src/commonTest/resources/PhotoCapture/takepicture_cancel.json").readText() ) + val stateIdleResponse = + Resource("src/commonTest/resources/PhotoCapture/state_idle.json").readText() var counter = 0 - MockApiClient.onRequest = { _ -> + MockApiClient.onRequest = { request -> val index = counter++ + val response = when (index) { + 0, 1 -> { + responseArray[index] + } + + else -> { + when (request.url.encodedPath) { + "/osc/commands/status" -> responseArray[2] + else -> stateIdleResponse + } + } + } MockApiClient.status = if (index == 2) HttpStatusCode.Forbidden else HttpStatusCode.OK - ByteReadChannel(responseArray[index]) + ByteReadChannel(response) } val deferred = CompletableDeferred() // execute val thetaRepository = ThetaRepository(endpoint) val photoCapture = thetaRepository.getPhotoCaptureBuilder() + .setCheckStatusCommandInterval(100) .build() + ThetaApi.lastSetTimeConsumingOptionTime = 0 var file: String? = "" photoCapture.takePicture(object : PhotoCapture.TakePictureCallback { @@ -210,40 +269,74 @@ class PhotoCaptureTest { "/osc/commands/execute", "/osc/commands/status" ) + val stateIdleResponse = + Resource("src/commonTest/resources/PhotoCapture/state_idle.json").readText() var counter = 0 MockApiClient.onRequest = { request -> - val index = counter++ - // check request - assertEquals(request.url.encodedPath, requestPathArray[index], "take picture request") - when (index) { + val response = when (val index = counter++) { 0 -> { + assertEquals( + request.url.encodedPath, + requestPathArray[index], + "take picture request" + ) CheckRequest.checkSetOptions(request = request, captureMode = CaptureMode.IMAGE) + responseArray[index] } 1 -> { + assertEquals( + request.url.encodedPath, + requestPathArray[index], + "take picture request" + ) CheckRequest.checkSetOptions( request = request, filter = filter.filter, fileFormat = fileFormat.fileFormat.toMediaFileFormat() ) + responseArray[index] } 2 -> { + assertEquals( + request.url.encodedPath, + requestPathArray[index], + "take picture request" + ) CheckRequest.checkCommandName(request, "camera.takePicture") + responseArray[index] + } + + else -> { + when (request.url.encodedPath) { + "/osc/commands/status" -> { + assertEquals( + request.url.encodedPath, + requestPathArray[3], + "take picture request" + ) + responseArray[3] + } + + else -> stateIdleResponse + } } } - ByteReadChannel(responseArray[index]) + ByteReadChannel(response) } val deferred = CompletableDeferred() // execute val thetaRepository = ThetaRepository(endpoint) val photoCapture = thetaRepository.getPhotoCaptureBuilder() + .setCheckStatusCommandInterval(100) .setFilter(filter) .setFileFormat(fileFormat) .build() + ThetaApi.lastSetTimeConsumingOptionTime = 0 assertEquals(photoCapture.getFilter(), filter, "set option filter") assertEquals(photoCapture.getFileFormat(), fileFormat, "set option filter") @@ -277,7 +370,7 @@ class PhotoCaptureTest { @Test fun settingFilterTest() = runTest { // setup - val filterList = ThetaRepository.FilterEnum.values() + val filterList = ThetaRepository.FilterEnum.entries val responseArray = arrayOf( Resource("src/commonTest/resources/setOptions/set_options_done.json").readText(), @@ -332,7 +425,7 @@ class PhotoCaptureTest { @Test fun settingFileFormatTest() = runTest { // setup - val fileFormatList = ThetaRepository.PhotoFileFormatEnum.values() + val fileFormatList = ThetaRepository.PhotoFileFormatEnum.entries val responseArray = arrayOf( Resource("src/commonTest/resources/setOptions/set_options_done.json").readText(), @@ -387,7 +480,7 @@ class PhotoCaptureTest { @Test fun settingApertureTest() = runTest { // setup - val valueList = ThetaRepository.ApertureEnum.values() + val valueList = ThetaRepository.ApertureEnum.entries val responseArray = arrayOf( Resource("src/commonTest/resources/setOptions/set_options_done.json").readText(), @@ -497,7 +590,7 @@ class PhotoCaptureTest { @Test fun settingExposureCompensationTest() = runTest { // setup - val valueList = ThetaRepository.ExposureCompensationEnum.values() + val valueList = ThetaRepository.ExposureCompensationEnum.entries val responseArray = arrayOf( Resource("src/commonTest/resources/setOptions/set_options_done.json").readText(), @@ -533,6 +626,7 @@ class PhotoCaptureTest { val photoCapture = thetaRepository.getPhotoCaptureBuilder() .setExposureCompensation(it) .build() + ThetaApi.lastSetTimeConsumingOptionTime = 0 // check result assertEquals( @@ -552,7 +646,7 @@ class PhotoCaptureTest { @Test fun settingExposureDelayTest() = runTest { // setup - val valueList = ThetaRepository.ExposureDelayEnum.values() + val valueList = ThetaRepository.ExposureDelayEnum.entries val responseArray = arrayOf( Resource("src/commonTest/resources/setOptions/set_options_done.json").readText(), @@ -607,7 +701,7 @@ class PhotoCaptureTest { @Test fun settingExposureProgramTest() = runTest { // setup - val valueList = ThetaRepository.ExposureProgramEnum.values() + val valueList = ThetaRepository.ExposureProgramEnum.entries val responseArray = arrayOf( Resource("src/commonTest/resources/setOptions/set_options_done.json").readText(), @@ -721,7 +815,7 @@ class PhotoCaptureTest { @Test fun settingGpsTagRecordingTest() = runTest { // setup - val valueList = ThetaRepository.GpsTagRecordingEnum.values() + val valueList = ThetaRepository.GpsTagRecordingEnum.entries val responseArray = arrayOf( Resource("src/commonTest/resources/setOptions/set_options_done.json").readText(), @@ -776,7 +870,7 @@ class PhotoCaptureTest { @Test fun settingIsoTest() = runTest { // setup - val valueList = ThetaRepository.IsoEnum.values() + val valueList = ThetaRepository.IsoEnum.entries val responseArray = arrayOf( Resource("src/commonTest/resources/setOptions/set_options_done.json").readText(), @@ -831,7 +925,7 @@ class PhotoCaptureTest { @Test fun settingIsoAutoHighLimitTest() = runTest { // setup - val valueList = ThetaRepository.IsoAutoHighLimitEnum.values() + val valueList = ThetaRepository.IsoAutoHighLimitEnum.entries val responseArray = arrayOf( Resource("src/commonTest/resources/setOptions/set_options_done.json").readText(), @@ -886,7 +980,7 @@ class PhotoCaptureTest { @Test fun settingWhiteBalanceTest() = runTest { // setup - val valueList = ThetaRepository.WhiteBalanceEnum.values() + val valueList = ThetaRepository.WhiteBalanceEnum.entries val responseArray = arrayOf( Resource("src/commonTest/resources/setOptions/set_options_done.json").readText(), @@ -941,7 +1035,7 @@ class PhotoCaptureTest { @Test fun settingPresetTest() = runTest { // setup - val valueList = ThetaRepository.PresetEnum.values() + val valueList = ThetaRepository.PresetEnum.entries val responseArray = arrayOf( Resource("src/commonTest/resources/setOptions/set_options_done.json").readText(), @@ -1142,17 +1236,29 @@ class PhotoCaptureTest { Resource("src/commonTest/resources/PhotoCapture/takepicture_error.json").readText(), // takePicture status error "Not json" // json error ) + val stateIdleResponse = + Resource("src/commonTest/resources/PhotoCapture/state_idle.json").readText() var counter = 0 - MockApiClient.onRequest = { _ -> - val index = counter++ - ByteReadChannel(responseArray[index]) + MockApiClient.onRequest = { request -> + val response = when (request.url.encodedPath) { + "/osc/state" -> stateIdleResponse + + else -> { + val index = counter++ + responseArray[index] + } + } + + ByteReadChannel(response) } // execute val thetaRepository = ThetaRepository(endpoint) val photoCapture = thetaRepository.getPhotoCaptureBuilder() + .setCheckStatusCommandInterval(100) .build() + ThetaApi.lastSetTimeConsumingOptionTime = 0 // execute takePicture error response var deferred = CompletableDeferred() @@ -1246,7 +1352,9 @@ class PhotoCaptureTest { val thetaRepository = ThetaRepository(endpoint) val photoCapture = thetaRepository.getPhotoCaptureBuilder() + .setCheckStatusCommandInterval(100) .build() + ThetaApi.lastSetTimeConsumingOptionTime = 0 // execute status error and json response var deferred = CompletableDeferred() @@ -1308,4 +1416,99 @@ class PhotoCaptureTest { } } } + + /** + * call takePicture. + */ + @Test + fun capturingStatusTest() = runTest { + // setup + val optionResponse = + Resource("src/commonTest/resources/setOptions/set_options_done.json").readText() + val progressResponse = + Resource("src/commonTest/resources/PhotoCapture/takepicture_progress.json").readText() + val doneResponse = + Resource("src/commonTest/resources/PhotoCapture/takepicture_done.json").readText() + val stateIdleResponse = + Resource("src/commonTest/resources/PhotoCapture/state_idle.json").readText() + val stateSelfTimerResponse = + Resource("src/commonTest/resources/PhotoCapture/state_self_timer.json").readText() + var counter = 0 + var statusCount = 0 + MockApiClient.onRequest = { request -> + val response = when (counter++) { + 0 -> { + optionResponse + } + + 1 -> { + progressResponse + } + + else -> { + when (request.url.encodedPath) { + "/osc/state" -> { + statusCount += 1 + when (statusCount) { + 1 -> stateSelfTimerResponse + else -> stateIdleResponse + } + } + + else -> { + if (statusCount > 1) { + doneResponse + } else { + progressResponse + } + } + } + } + } + + ByteReadChannel(response) + } + val deferred = CompletableDeferred() + + // execute + val thetaRepository = ThetaRepository(endpoint) + val photoCapture = thetaRepository.getPhotoCaptureBuilder() + .setCheckStatusCommandInterval(100) + .build() + ThetaApi.lastSetTimeConsumingOptionTime = 0 + + assertNull(photoCapture.getFilter(), "set option filter") + assertNull(photoCapture.getFileFormat(), "set option fileFormat") + + var file: String? = null + var onCapturingCount = 0 + photoCapture.takePicture(object : PhotoCapture.TakePictureCallback { + override fun onSuccess(fileUrl: String?) { + file = fileUrl + deferred.complete(Unit) + } + + override fun onCapturing(status: CapturingStatusEnum) { + when (onCapturingCount) { + 0 -> assertEquals(status, CapturingStatusEnum.SELF_TIMER_COUNTDOWN) + else -> assertEquals(status, CapturingStatusEnum.CAPTURING) + } + onCapturingCount += 1 + } + + override fun onError(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue(false, "error take picture") + deferred.complete(Unit) + } + }) + runBlocking { + withTimeout(10000) { + deferred.await() + } + } + + // check result + assertTrue(file?.startsWith("http://") ?: false, "take picture") + assertEquals(onCapturingCount, 2) + } } diff --git a/kotlin-multiplatform/src/commonTest/resources/PhotoCapture/state_idle.json b/kotlin-multiplatform/src/commonTest/resources/PhotoCapture/state_idle.json new file mode 100644 index 0000000000..e9c43eff77 --- /dev/null +++ b/kotlin-multiplatform/src/commonTest/resources/PhotoCapture/state_idle.json @@ -0,0 +1,22 @@ +{ + "fingerprint": "FIG_0008", + "state": { + "_apiVersion": 2, + "batteryLevel": 0.81, + "_batteryState": "disconnect", + "_cameraError": [ + "COMPASS_CALIBRATION" + ], + "_captureStatus": "idle", + "_capturedPictures": 0, + "_compositeShootingElapsedTime": 0, + "_function": "selfTimer", + "_latestFileUrl": "http://192.168.1.1/files/150100524436344d4201375fda9dc400/100RICOH/R0013331.JPG", + "_mySettingChanged": false, + "_pluginRunning": false, + "_pluginWebServer": true, + "_recordableTime": 0, + "_recordedTime": 0, + "storageUri": "http://192.168.1.1/files/150100524436344d4201375fda9dc400/" + } +} diff --git a/kotlin-multiplatform/src/commonTest/resources/PhotoCapture/state_self_timer.json b/kotlin-multiplatform/src/commonTest/resources/PhotoCapture/state_self_timer.json new file mode 100644 index 0000000000..1bdc92b1d7 --- /dev/null +++ b/kotlin-multiplatform/src/commonTest/resources/PhotoCapture/state_self_timer.json @@ -0,0 +1 @@ +{"fingerprint":"FIG_0003","state":{"batteryLevel":0.8,"storageUri":"http://192.168.1.1/files/thetasc26c21a247daf35838792bad9e","_apiVersion":2,"_batteryState":"charging","_cameraError":[],"_captureStatus":"self-timer countdown","_capturedPictures":0,"_latestFileUrl":"http://192.168.1.1/files/thetasc26c21a247daf35838792bad9e/100RICOH/R0012313.JPG","_recordableTime":0,"_recordedTime":0,"_function":"selfTimer"}} diff --git a/react-native/android/src/main/java/com/ricoh360/thetaclientreactnative/Converter.kt b/react-native/android/src/main/java/com/ricoh360/thetaclientreactnative/Converter.kt index 9206a4e32c..00f926f952 100644 --- a/react-native/android/src/main/java/com/ricoh360/thetaclientreactnative/Converter.kt +++ b/react-native/android/src/main/java/com/ricoh360/thetaclientreactnative/Converter.kt @@ -16,6 +16,7 @@ const val KEY_NOTIFY_PARAMS = "params" const val KEY_NOTIFY_PARAM_COMPLETION = "completion" const val KEY_NOTIFY_PARAM_EVENT = "event" const val KEY_NOTIFY_PARAM_MESSAGE = "message" +const val KEY_NOTIFY_PARAM_STATUS = "status" const val KEY_GPS_INFO = "gpsInfo" const val KEY_STATE_EXTERNAL_GPS_INFO = "externalGpsInfo" const val KEY_STATE_INTERNAL_GPS_INFO = "internalGpsInfo" @@ -114,6 +115,12 @@ fun toMessageNotifyParam(value: String): WritableMap { return result } +fun toCapturingNotifyParam(status: CapturingStatusEnum): WritableMap { + val result = Arguments.createMap() + result.putString(KEY_NOTIFY_PARAM_STATUS, status.name) + return result +} + fun toGpsInfo(map: ReadableMap): GpsInfo { return GpsInfo( latitude = map.getDouble("latitude").toFloat(), @@ -157,6 +164,13 @@ fun setCaptureBuilderParams(optionMap: ReadableMap, builder: Capture.Builder } fun setPhotoCaptureBuilderParams(optionMap: ReadableMap, builder: PhotoCapture.Builder) { + val interval = if (optionMap.hasKey("_capture_interval")) optionMap.getInt("_capture_interval") else null + interval?.let { + if (it >= 0) { + builder.setCheckStatusCommandInterval(it.toLong()) + } + } + optionMap.getString("filter")?.let { builder.setFilter(FilterEnum.valueOf(it)) } diff --git a/react-native/android/src/main/java/com/ricoh360/thetaclientreactnative/ThetaClientSdkModule.kt b/react-native/android/src/main/java/com/ricoh360/thetaclientreactnative/ThetaClientSdkModule.kt index 351b165a8c..9f527bfa91 100644 --- a/react-native/android/src/main/java/com/ricoh360/thetaclientreactnative/ThetaClientSdkModule.kt +++ b/react-native/android/src/main/java/com/ricoh360/thetaclientreactnative/ThetaClientSdkModule.kt @@ -537,6 +537,13 @@ class ThetaClientReactNativeModule( photoCapture = null } + override fun onCapturing(status: CapturingStatusEnum) { + super.onCapturing(status) + sendNotifyEvent( + toNotify(NOTIFY_CAPTURING, toCapturingNotifyParam(status = status)) + ) + } + override fun onError(exception: ThetaRepository.ThetaRepositoryException) { promise.reject(exception) photoCapture = null @@ -2099,5 +2106,6 @@ class ThetaClientReactNativeModule( const val NOTIFY_CONTINUOUS_PROGRESS = "CONTINUOUS-PROGRESS" const val NOTIFY_EVENT_WEBSOCKET_EVENT = "EVENT-WEBSOCKET-EVENT" const val NOTIFY_EVENT_WEBSOCKET_CLOSE = "EVENT-WEBSOCKET-CLOSE" + const val NOTIFY_CAPTURING = "NOTIFY-CAPTURING" } } diff --git a/react-native/ios/ConvertUtil.swift b/react-native/ios/ConvertUtil.swift index 3bb6e09fae..3f5252e769 100644 --- a/react-native/ios/ConvertUtil.swift +++ b/react-native/ios/ConvertUtil.swift @@ -8,6 +8,7 @@ let KEY_NOTIFY_PARAM_COMPLETION = "completion" let KEY_NOTIFY_PARAM_EVENT = "event" let KEY_NOTIFY_PARAM_IMAGE = "image" let KEY_NOTIFY_PARAM_MESSAGE = "message" +let KEY_NOTIFY_PARAM_STATUS = "status" let KEY_DATETIME = "dateTime" let KEY_LANGUAGE = "language" let KEY_OFF_DELAY = "offDelay" @@ -457,6 +458,12 @@ func toMessageNotifyParam(value: String) -> [String: Any] { ] } +func toCapturingNotifyParam(value: CapturingStatusEnum) -> [String: Any] { + return [ + KEY_NOTIFY_PARAM_STATUS: value.name, + ] +} + // MARK: - Capture builder func setCaptureBuilderParams(params: [String: Any], builder: CaptureBuilder) { @@ -524,6 +531,11 @@ func setCaptureBuilderParams(params: [String: Any], builder: CaptureBuilder= 0 + { + builder.setCheckStatusCommandInterval(timeMillis: Int64(interval)) + } if let value = params[KEY_FILTER] as? String { if let enumValue = getEnumValue(values: ThetaRepository.FilterEnum.values(), name: value) { builder.setFilter(filter: enumValue) diff --git a/react-native/ios/ThetaClientReactNative.swift b/react-native/ios/ThetaClientReactNative.swift index d5e4d036a3..d64365948e 100644 --- a/react-native/ios/ThetaClientReactNative.swift +++ b/react-native/ios/ThetaClientReactNative.swift @@ -80,7 +80,8 @@ class ThetaClientReactNative: RCTEventEmitter { static let NOTIFY_CONTINUOUS_PROGRESS = "CONTINUOUS-PROGRESS" static let NOTIFY_EVENT_WEBSOCKET_EVENT = "EVENT-WEBSOCKET-EVENT" static let NOTIFY_EVENT_WEBSOCKET_CLOSE = "EVENT-WEBSOCKET-CLOSE" - + static let NOTIFY_CAPTURING = "NOTIFY-CAPTURING" + @objc override func supportedEvents() -> [String]! { return [ThetaClientReactNative.EVENT_FRAME, ThetaClientReactNative.EVENT_NOTIFY] @@ -556,15 +557,28 @@ class ThetaClientReactNative: RCTEventEmitter { class Callback: PhotoCaptureTakePictureCallback { let callback: (_ url: String?, _ error: Error?) -> Void - init(_ callback: @escaping (_ url: String?, _ error: Error?) -> Void) { + weak var client: ThetaClientReactNative? + init( + _ callback: @escaping (_ url: String?, _ error: Error?) -> Void, + client: ThetaClientReactNative + ) { self.callback = callback + self.client = client } func onSuccess(fileUrl: String?) { callback(fileUrl, nil) } - func onProgress(completion _: Float) {} + func onCapturing(status: CapturingStatusEnum) { + client?.sendEvent( + withName: ThetaClientReactNative.EVENT_NOTIFY, + body: toNotify( + name: ThetaClientReactNative.NOTIFY_CAPTURING, + params: toCapturingNotifyParam(value: status) + ) + ) + } func onError(exception: ThetaRepository.ThetaRepositoryException) { callback(nil, exception.asError()) @@ -572,14 +586,15 @@ class ThetaClientReactNative: RCTEventEmitter { } photoCapture.takePicture( - callback: Callback { url, error in + callback: Callback({ url, error in if let error { reject(ERROR_CODE_ERROR, error.localizedDescription, error) } else { self.photoCapture = nil resolve(url) } - }) + }, client: self) + ) } @objc(getTimeShiftCaptureBuilder:withRejecter:) diff --git a/react-native/src/__tests__/capture/photo-capture.test.ts b/react-native/src/__tests__/capture/photo-capture.test.ts index ee24131f64..739885ce3e 100644 --- a/react-native/src/__tests__/capture/photo-capture.test.ts +++ b/react-native/src/__tests__/capture/photo-capture.test.ts @@ -1,22 +1,30 @@ import { NativeModules } from 'react-native'; -import { getPhotoCaptureBuilder } from '../../theta-repository'; +import { getPhotoCaptureBuilder, initialize } from '../../theta-repository'; import { FilterEnum, PhotoFileFormatEnum, PresetEnum, } from '../../theta-repository/options'; +import { NativeEventEmitter_addListener } from '../../__mocks__/react-native'; +import { + BaseNotify, + NotifyController, +} from '../../theta-repository/notify-controller'; +import { CapturingStatusEnum } from '../../capture'; describe('photo capture', () => { const thetaClient = NativeModules.ThetaClientReactNative; beforeEach(() => { jest.clearAllMocks(); + NotifyController.instance.release(); }); afterEach(() => { thetaClient.initialize = jest.fn(); thetaClient.buildPhotoCapture = jest.fn(); thetaClient.takePicture = jest.fn(); + NotifyController.instance.release(); }); test('getPhotoCaptureBuilder', async () => { @@ -65,6 +73,64 @@ describe('photo capture', () => { expect(fileUrl).toBe(testUrl); }); + test('takePictureWithCapturing', async () => { + let notifyCallback: (notify: BaseNotify) => void = () => { + expect(true).toBeFalsy(); + }; + jest.mocked(NativeEventEmitter_addListener).mockImplementation( + jest.fn((_, callback) => { + notifyCallback = callback; + return { + remove: jest.fn(), + }; + }) + ); + + await initialize(); + const builder = getPhotoCaptureBuilder(); + const testUrl = 'http://192.168.1.1/files/100RICOH/R100.JPG'; + + const sendStatus = (status: CapturingStatusEnum) => { + notifyCallback({ + name: 'NOTIFY-CAPTURING', + params: { + status: status, + }, + }); + }; + + jest + .mocked(thetaClient.buildPhotoCapture) + .mockImplementation(jest.fn(async () => {})); + jest.mocked(thetaClient.takePicture).mockImplementation( + jest.fn(async () => { + sendStatus(CapturingStatusEnum.SELF_TIMER_COUNTDOWN); + return testUrl; + }) + ); + + const capture = await builder.build(); + let isOnCapturing = false; + const fileUrl = await capture.takePicture((status) => { + expect(status).toBe(CapturingStatusEnum.SELF_TIMER_COUNTDOWN); + isOnCapturing = true; + }); + expect(fileUrl).toBe(testUrl); + expect(isOnCapturing).toBeTruthy(); + let done: (value: unknown) => void; + const promise = new Promise((resolve) => { + done = resolve; + }); + + setTimeout(() => { + expect(NotifyController.instance.notifyList.size).toBe(0); + expect(isOnCapturing).toBeTruthy(); + done(0); + }, 1); + + return promise; + }); + test('exception', (done) => { const builder = getPhotoCaptureBuilder(); jest diff --git a/react-native/src/capture/capture.ts b/react-native/src/capture/capture.ts index fa83c41f22..78f6c7361f 100644 --- a/react-native/src/capture/capture.ts +++ b/react-native/src/capture/capture.ts @@ -122,3 +122,15 @@ export abstract class CaptureBuilder> { return this as unknown as T; } } + +/** Capturing status */ +export const CapturingStatusEnum = { + /** Capture in progress */ + CAPTURING: 'CAPTURING', + /** Self-timer in progress */ + SELF_TIMER_COUNTDOWN: 'SELF_TIMER_COUNTDOWN', +} as const; + +/** type definition of CapturingStatusEnum */ +export type CapturingStatusEnum = + (typeof CapturingStatusEnum)[keyof typeof CapturingStatusEnum]; diff --git a/react-native/src/capture/multi-bracket-capture.ts b/react-native/src/capture/multi-bracket-capture.ts index 6bb7b6d9ae..044a314b97 100644 --- a/react-native/src/capture/multi-bracket-capture.ts +++ b/react-native/src/capture/multi-bracket-capture.ts @@ -4,7 +4,7 @@ import { BaseNotify, NotifyController, } from '../theta-repository/notify-controller'; -import type { BracketSetting } from 'src/theta-repository/options'; +import type { BracketSetting } from '../theta-repository/options'; const ThetaClientReactNative = NativeModules.ThetaClientReactNative; const NOTIFY_PROGRESS = 'MULTI-BRACKET-PROGRESS'; diff --git a/react-native/src/capture/photo-capture.ts b/react-native/src/capture/photo-capture.ts index 682ddc7a3b..7247e9d5e3 100644 --- a/react-native/src/capture/photo-capture.ts +++ b/react-native/src/capture/photo-capture.ts @@ -1,4 +1,4 @@ -import { CaptureBuilder } from './capture'; +import { CaptureBuilder, CapturingStatusEnum } from './capture'; import type { FilterEnum, PhotoFileFormatEnum, @@ -6,21 +6,52 @@ import type { } from '../theta-repository/options'; import { NativeModules } from 'react-native'; +import { + BaseNotify, + NotifyController, +} from '../theta-repository/notify-controller'; const ThetaClientReactNative = NativeModules.ThetaClientReactNative; +const NOTIFY_CAPTURING = 'NOTIFY-CAPTURING'; + +interface CapturingNotify extends BaseNotify { + params?: { + status: CapturingStatusEnum; + }; +} + /** * PhotoCapture class */ export class PhotoCapture { + notify: NotifyController; + constructor(notify: NotifyController) { + this.notify = notify; + } + /** + * Take a picture + * + * @param onProgress Called when change capture status * @return promise of token file url */ - async takePicture(): Promise { + async takePicture( + onCapturing?: (status: CapturingStatusEnum) => void + ): Promise { + if (onCapturing) { + this.notify.addNotify(NOTIFY_CAPTURING, (event: CapturingNotify) => { + if (event.params?.status) { + onCapturing(event.params.status); + } + }); + } try { const fileUrl = await ThetaClientReactNative.takePicture(); return fileUrl ?? undefined; } catch (error) { throw error; + } finally { + this.notify.removeNotify(NOTIFY_CAPTURING); } } } @@ -29,9 +60,22 @@ export class PhotoCapture { * PhotoCaptureBaseBuilder class */ export class PhotoCaptureBuilder extends CaptureBuilder { + interval?: number; + /** construct PhotoCaptureBuilder instance */ constructor() { super(); + this.interval = undefined; + } + + /** + * set interval of checking take picture status command + * @param timeMillis interval + * @returns PhotoCaptureBuilder + */ + setCheckStatusCommandInterval(timeMillis: number): PhotoCaptureBuilder { + this.interval = timeMillis; + return this; } /** @@ -71,11 +115,16 @@ export class PhotoCaptureBuilder extends CaptureBuilder { * @return promise of PhotoCapture instance */ build(): Promise { + let params = { + ...this.options, + // Cannot pass negative values in IOS, use objects + _capture_interval: this.interval ?? -1, + }; return new Promise(async (resolve, reject) => { try { await ThetaClientReactNative.getPhotoCaptureBuilder(); - await ThetaClientReactNative.buildPhotoCapture(this.options); - resolve(new PhotoCapture()); + await ThetaClientReactNative.buildPhotoCapture(params); + resolve(new PhotoCapture(NotifyController.instance)); } catch (error) { reject(error); } diff --git a/react-native/verification-tool/src/screen/photo-capture-screen/photo-capture-screen.tsx b/react-native/verification-tool/src/screen/photo-capture-screen/photo-capture-screen.tsx index eb61b1b5e8..42e77abefd 100644 --- a/react-native/verification-tool/src/screen/photo-capture-screen/photo-capture-screen.tsx +++ b/react-native/verification-tool/src/screen/photo-capture-screen/photo-capture-screen.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { View, Alert, ScrollView } from 'react-native'; +import { View, Alert, ScrollView, Text } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import styles from './styles'; import Button from '../../components/ui/button'; @@ -17,10 +17,13 @@ import { CaptureCommonOptionsEdit } from '../../components/capture/capture-commo import type { NativeStackScreenProps } from '@react-navigation/native-stack'; import type { RootStackParamList } from '../../App'; import { EnumEdit } from '../../components/options'; +import { InputNumber } from '../../components/ui/input-number'; const PhotoCaptureScreen: React.FC< NativeStackScreenProps > = ({ navigation }) => { + const [interval, setInterval] = React.useState(); + const [message, setMessage] = React.useState(''); const [captureOptions, setCaptureOptions] = React.useState(); const [isTaking, setIsTaking] = React.useState(false); @@ -28,6 +31,9 @@ const PhotoCaptureScreen: React.FC< setIsTaking(true); const builder = getPhotoCaptureBuilder(); + if (interval != null) { + builder.setCheckStatusCommandInterval(interval); + } captureOptions?.filter && builder.setFilter(captureOptions.filter); captureOptions?.fileFormat && builder.setFileFormat(captureOptions.fileFormat as PhotoFileFormatEnum); @@ -56,8 +62,11 @@ const PhotoCaptureScreen: React.FC< console.log('takePicture builder: :' + JSON.stringify(builder)); try { + setMessage(''); const photoCapture = await builder.build(); - const url = await photoCapture.takePicture(); + const url = await photoCapture.takePicture((status) => { + setMessage(`onCapturing: ${status}`); + }); setIsTaking(false); @@ -96,6 +105,7 @@ const PhotoCaptureScreen: React.FC< edges={['left', 'right', 'bottom']} > + {message}