mirror of
https://github.com/laurent22/joplin.git
synced 2026-05-07 20:02:45 +00:00
Mobile: Upgrade to React Native 0.81 (#14232)
This commit is contained in:
@@ -92,6 +92,7 @@ readme/
|
|||||||
packages/react-native-vosk/lib/
|
packages/react-native-vosk/lib/
|
||||||
packages/lib/countable/Countable.js
|
packages/lib/countable/Countable.js
|
||||||
packages/onenote-converter/renderer/pkg/*
|
packages/onenote-converter/renderer/pkg/*
|
||||||
|
packages/whisper-voice-typing/lib/
|
||||||
|
|
||||||
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
|
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
|
||||||
packages/app-cli/app/LinkSelector.js
|
packages/app-cli/app/LinkSelector.js
|
||||||
@@ -691,6 +692,7 @@ packages/app-mobile/components/FeedbackBanner.js
|
|||||||
packages/app-mobile/components/FolderPicker.js
|
packages/app-mobile/components/FolderPicker.js
|
||||||
packages/app-mobile/components/Icon.js
|
packages/app-mobile/components/Icon.js
|
||||||
packages/app-mobile/components/IconButton.js
|
packages/app-mobile/components/IconButton.js
|
||||||
|
packages/app-mobile/components/KeyboardAvoidingView.js
|
||||||
packages/app-mobile/components/Modal.js
|
packages/app-mobile/components/Modal.js
|
||||||
packages/app-mobile/components/ModalDialog.js
|
packages/app-mobile/components/ModalDialog.js
|
||||||
packages/app-mobile/components/NestableFlatList.js
|
packages/app-mobile/components/NestableFlatList.js
|
||||||
@@ -969,6 +971,7 @@ packages/app-mobile/utils/hooks/useSafeAreaPadding.js
|
|||||||
packages/app-mobile/utils/image/fileToImage.web.js
|
packages/app-mobile/utils/image/fileToImage.web.js
|
||||||
packages/app-mobile/utils/image/getImageDimensions.js
|
packages/app-mobile/utils/image/getImageDimensions.js
|
||||||
packages/app-mobile/utils/image/resizeImage.js
|
packages/app-mobile/utils/image/resizeImage.js
|
||||||
|
packages/app-mobile/utils/initReact.js
|
||||||
packages/app-mobile/utils/initializeCommandService.js
|
packages/app-mobile/utils/initializeCommandService.js
|
||||||
packages/app-mobile/utils/ipc/RNToWebViewMessenger.js
|
packages/app-mobile/utils/ipc/RNToWebViewMessenger.js
|
||||||
packages/app-mobile/utils/ipc/WebViewToRNMessenger.js
|
packages/app-mobile/utils/ipc/WebViewToRNMessenger.js
|
||||||
@@ -1942,4 +1945,6 @@ packages/tools/website/utils/pressCarousel.js
|
|||||||
packages/tools/website/utils/processTranslations.js
|
packages/tools/website/utils/processTranslations.js
|
||||||
packages/tools/website/utils/render.js
|
packages/tools/website/utils/render.js
|
||||||
packages/tools/website/utils/types.js
|
packages/tools/website/utils/types.js
|
||||||
|
packages/whisper-voice-typing/src/index.js
|
||||||
|
packages/whisper-voice-typing/src/specs/Whisper.nitro.js
|
||||||
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
|
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
|
||||||
|
|||||||
@@ -665,6 +665,7 @@ packages/app-mobile/components/FeedbackBanner.js
|
|||||||
packages/app-mobile/components/FolderPicker.js
|
packages/app-mobile/components/FolderPicker.js
|
||||||
packages/app-mobile/components/Icon.js
|
packages/app-mobile/components/Icon.js
|
||||||
packages/app-mobile/components/IconButton.js
|
packages/app-mobile/components/IconButton.js
|
||||||
|
packages/app-mobile/components/KeyboardAvoidingView.js
|
||||||
packages/app-mobile/components/Modal.js
|
packages/app-mobile/components/Modal.js
|
||||||
packages/app-mobile/components/ModalDialog.js
|
packages/app-mobile/components/ModalDialog.js
|
||||||
packages/app-mobile/components/NestableFlatList.js
|
packages/app-mobile/components/NestableFlatList.js
|
||||||
@@ -943,6 +944,7 @@ packages/app-mobile/utils/hooks/useSafeAreaPadding.js
|
|||||||
packages/app-mobile/utils/image/fileToImage.web.js
|
packages/app-mobile/utils/image/fileToImage.web.js
|
||||||
packages/app-mobile/utils/image/getImageDimensions.js
|
packages/app-mobile/utils/image/getImageDimensions.js
|
||||||
packages/app-mobile/utils/image/resizeImage.js
|
packages/app-mobile/utils/image/resizeImage.js
|
||||||
|
packages/app-mobile/utils/initReact.js
|
||||||
packages/app-mobile/utils/initializeCommandService.js
|
packages/app-mobile/utils/initializeCommandService.js
|
||||||
packages/app-mobile/utils/ipc/RNToWebViewMessenger.js
|
packages/app-mobile/utils/ipc/RNToWebViewMessenger.js
|
||||||
packages/app-mobile/utils/ipc/WebViewToRNMessenger.js
|
packages/app-mobile/utils/ipc/WebViewToRNMessenger.js
|
||||||
@@ -1916,5 +1918,7 @@ packages/tools/website/utils/pressCarousel.js
|
|||||||
packages/tools/website/utils/processTranslations.js
|
packages/tools/website/utils/processTranslations.js
|
||||||
packages/tools/website/utils/render.js
|
packages/tools/website/utils/render.js
|
||||||
packages/tools/website/utils/types.js
|
packages/tools/website/utils/types.js
|
||||||
|
packages/whisper-voice-typing/src/index.js
|
||||||
|
packages/whisper-voice-typing/src/specs/Whisper.nitro.js
|
||||||
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
|
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"exceptions": [
|
"exceptions": [
|
||||||
"@joplin/editor",
|
"@joplin/editor",
|
||||||
"@joplin/fork-htmlparser2",
|
"@joplin/fork-htmlparser2",
|
||||||
|
"@joplin/whisper-voice-typing",
|
||||||
"@joplin/fork-sax",
|
"@joplin/fork-sax",
|
||||||
"@joplin/fork-uslug",
|
"@joplin/fork-uslug",
|
||||||
"@joplin/htmlpack",
|
"@joplin/htmlpack",
|
||||||
|
|||||||
+1
-1
@@ -33,7 +33,7 @@
|
|||||||
"/packages/app-desktop/build/",
|
"/packages/app-desktop/build/",
|
||||||
"/packages/app-desktop/utils/checkForUpdatesUtilsTestData.ts",
|
"/packages/app-desktop/utils/checkForUpdatesUtilsTestData.ts",
|
||||||
"/packages/app-desktop/vendor/",
|
"/packages/app-desktop/vendor/",
|
||||||
"/packages/app-mobile/android/vendor/",
|
"/packages/whisper-voice-typing/vendor/",
|
||||||
"/packages/app-mobile/ios/Pods/",
|
"/packages/app-mobile/ios/Pods/",
|
||||||
"/packages/app-mobile/lib/rnInjectedJs",
|
"/packages/app-mobile/lib/rnInjectedJs",
|
||||||
"/packages/app-mobile/pluginAssets",
|
"/packages/app-mobile/pluginAssets",
|
||||||
|
|||||||
@@ -2,11 +2,24 @@ apply plugin: "com.android.application"
|
|||||||
apply plugin: "org.jetbrains.kotlin.android"
|
apply plugin: "org.jetbrains.kotlin.android"
|
||||||
apply plugin: "com.facebook.react"
|
apply plugin: "com.facebook.react"
|
||||||
|
|
||||||
|
def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the configuration block to customize your React Native Android app.
|
* This is the configuration block to customize your React Native Android app.
|
||||||
* By default you don't need to apply any configuration, just uncomment the lines you need.
|
* By default you don't need to apply any configuration, just uncomment the lines you need.
|
||||||
*/
|
*/
|
||||||
react {
|
react {
|
||||||
|
entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim())
|
||||||
|
reactNativeDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
|
||||||
|
hermesCommand = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/sdks/hermesc/%OS-BIN%/hermesc"
|
||||||
|
codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
|
||||||
|
|
||||||
|
enableBundleCompression = (findProperty('android.enableBundleCompression') ?: false).toBoolean()
|
||||||
|
// (Disabled) Use Expo CLI to bundle the app, this ensures the Metro config
|
||||||
|
// works correctly with Expo projects.
|
||||||
|
// cliFile = new File(["node", "--print", "require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })"].execute(null, rootDir).text.trim())
|
||||||
|
// bundleCommand = "export:embed"
|
||||||
|
|
||||||
/* Folders */
|
/* Folders */
|
||||||
// The root of your project, i.e. where "package.json" lives. Default is '../..'
|
// The root of your project, i.e. where "package.json" lives. Default is '../..'
|
||||||
// root = file("../..")
|
// root = file("../..")
|
||||||
@@ -55,31 +68,12 @@ react {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set this to true to Run Proguard on Release builds to minify the Java bytecode.
|
* Set this to true in release builds to optimize the app using [R8](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization).
|
||||||
*/
|
*/
|
||||||
def enableProguardInReleaseBuilds = false
|
def enableMinifyInReleaseBuilds = (findProperty('android.enableMinifyInReleaseBuilds') ?: false).toBoolean()
|
||||||
|
|
||||||
/**
|
|
||||||
* The preferred build flavor of JavaScriptCore (JSC)
|
|
||||||
*
|
|
||||||
* For example, to use the international variant, you can use:
|
|
||||||
* `def jscFlavor = io.github.react-native-community:jsc-android-intl:2026004.+`
|
|
||||||
*
|
|
||||||
* The international variant includes ICU i18n library and necessary data
|
|
||||||
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
|
|
||||||
* give correct results when using with locales other than en-US. Note that
|
|
||||||
* this variant is about 6MiB larger per architecture than default.
|
|
||||||
*/
|
|
||||||
def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
||||||
externalNativeBuild {
|
|
||||||
cmake {
|
|
||||||
path file('src/main/cpp/CMakeLists.txt')
|
|
||||||
version '3.22.1'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ndkVersion rootProject.ext.ndkVersion
|
ndkVersion rootProject.ext.ndkVersion
|
||||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||||
compileSdk rootProject.ext.compileSdkVersion
|
compileSdk rootProject.ext.compileSdkVersion
|
||||||
@@ -91,19 +85,15 @@ android {
|
|||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 2097788
|
versionCode 2097788
|
||||||
versionName "3.6.0"
|
versionName "3.6.0"
|
||||||
|
|
||||||
|
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
|
||||||
|
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Needed to fix: The number of method references in a .dex file cannot exceed 64K
|
// Needed to fix: The number of method references in a .dex file cannot exceed 64K
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
externalNativeBuild {
|
|
||||||
cmake {
|
|
||||||
cppFlags '-DCMAKE_BUILD_TYPE=Release'
|
|
||||||
// For 16 KB pages. This should be removable after upgrading to NDK r28
|
|
||||||
arguments "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
debug {
|
debug {
|
||||||
@@ -129,7 +119,7 @@ android {
|
|||||||
// Caution! In production, you need to generate your own keystore file.
|
// Caution! In production, you need to generate your own keystore file.
|
||||||
// see https://reactnative.dev/docs/signed-apk-android.
|
// see https://reactnative.dev/docs/signed-apk-android.
|
||||||
signingConfig signingConfigs.release
|
signingConfig signingConfigs.release
|
||||||
minifyEnabled enableProguardInReleaseBuilds
|
minifyEnabled enableMinifyInReleaseBuilds
|
||||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||||
}
|
}
|
||||||
profileable {
|
profileable {
|
||||||
@@ -149,10 +139,5 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
// The version of react-native is set by the React Native Gradle Plugin
|
// The version of react-native is set by the React Native Gradle Plugin
|
||||||
implementation("com.facebook.react:react-android")
|
implementation("com.facebook.react:react-android")
|
||||||
|
implementation("com.facebook.react:hermes-android")
|
||||||
if (hermesEnabled.toBoolean()) {
|
|
||||||
implementation("com.facebook.react:hermes-android")
|
|
||||||
} else {
|
|
||||||
implementation jscFlavor
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,3 +8,7 @@
|
|||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
# Add any project specific keep options here:
|
# Add any project specific keep options here:
|
||||||
|
|
||||||
|
# Keep classes referenced by JNI
|
||||||
|
# (see https://developer.android.com/topic/performance/app-optimization/add-keep-rules)
|
||||||
|
-keep class com.margelo.nitro.whispervoicetyping.AudioRecorder
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:theme="@style/AppTheme"
|
android:theme="@style/AppTheme"
|
||||||
|
android:usesCleartextTraffic="${usesCleartextTraffic}"
|
||||||
android:supportsRtl="true">
|
android:supportsRtl="true">
|
||||||
|
|
||||||
<!-- Enable profiling in release builds (Android 10+) -->
|
<!-- Enable profiling in release builds (Android 10+) -->
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
|
|
||||||
# For more information about using CMake with Android Studio, read the
|
|
||||||
# documentation: https://d.android.com/studio/projects/add-native-code.html.
|
|
||||||
# For more examples on how to use CMake, see https://github.com/android/ndk-samples.
|
|
||||||
|
|
||||||
# Sets the minimum CMake version required for this project.
|
|
||||||
cmake_minimum_required(VERSION 3.22.1)
|
|
||||||
|
|
||||||
# Declares the project name. The project name can be accessed via ${ PROJECT_NAME},
|
|
||||||
# Since this is the top level CMakeLists.txt, the project name is also accessible
|
|
||||||
# with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level
|
|
||||||
# build script scope).
|
|
||||||
project("joplin")
|
|
||||||
|
|
||||||
# Creates and names a library, sets it as either STATIC
|
|
||||||
# or SHARED, and provides the relative paths to its source code.
|
|
||||||
# You can define multiple libraries, and CMake builds them for you.
|
|
||||||
# Gradle automatically packages shared libraries with your APK.
|
|
||||||
#
|
|
||||||
# In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define
|
|
||||||
# the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME}
|
|
||||||
# is preferred for the same purpose.
|
|
||||||
#
|
|
||||||
# In order to load a library into your app from Java/Kotlin, you must call
|
|
||||||
# System.loadLibrary() and pass the name of the library defined here;
|
|
||||||
# for GameActivity/NativeActivity derived applications, the same library name must be
|
|
||||||
# used in the AndroidManifest.xml file.
|
|
||||||
add_library(${CMAKE_PROJECT_NAME} SHARED
|
|
||||||
# List C/C++ source files with relative paths to this CMakeLists.txt.
|
|
||||||
whisperWrapper.cpp
|
|
||||||
utils/WhisperSession.cpp
|
|
||||||
utils/findLongestSilence.cpp
|
|
||||||
utils/findLongestSilence_test.cpp
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
set(WHISPER_LIB_DIR ${CMAKE_SOURCE_DIR}/../../../../vendor/whisper.cpp)
|
|
||||||
|
|
||||||
# Based on the Whisper.cpp Android example:
|
|
||||||
set(SHARED_FLAGS "-O3 ")
|
|
||||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${SHARED_FLAGS} ")
|
|
||||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${SHARED_FLAGS} -fvisibility=hidden -fvisibility-inlines-hidden -ffunction-sections -fdata-sections")
|
|
||||||
|
|
||||||
# Whisper: See https://stackoverflow.com/a/76290722
|
|
||||||
add_subdirectory(${WHISPER_LIB_DIR} ./whisper)
|
|
||||||
|
|
||||||
# Directories for header files
|
|
||||||
target_include_directories(
|
|
||||||
${CMAKE_PROJECT_NAME}
|
|
||||||
PUBLIC
|
|
||||||
${PROJECT_BASE_DIR}/shared
|
|
||||||
${WHISPER_LIB_DIR}/include
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Specifies libraries CMake should link to your target library. You
|
|
||||||
# can link libraries from various origins, such as libraries defined in this
|
|
||||||
# build script, prebuilt third-party libraries, or Android system libraries.
|
|
||||||
target_link_libraries(${CMAKE_PROJECT_NAME}
|
|
||||||
whisper
|
|
||||||
# List libraries link to the target library
|
|
||||||
android
|
|
||||||
log
|
|
||||||
)
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
// Write C++ code here.
|
|
||||||
//
|
|
||||||
// Do not forget to dynamically load the C++ library into your application.
|
|
||||||
//
|
|
||||||
// For instance,
|
|
||||||
//
|
|
||||||
// In MainActivity.java:
|
|
||||||
// static {
|
|
||||||
// System.loadLibrary("joplin");
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// Or, in MainActivity.kt:
|
|
||||||
// companion object {
|
|
||||||
// init {
|
|
||||||
// System.loadLibrary("joplin")
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
#include <jni.h>
|
|
||||||
#include <memory>
|
|
||||||
#include <string>
|
|
||||||
#include <sstream>
|
|
||||||
#include <android/log.h>
|
|
||||||
#include "whisper.h"
|
|
||||||
#include "utils/WhisperSession.h"
|
|
||||||
#include "utils/androidUtil.h"
|
|
||||||
#include "utils/findLongestSilence_test.h"
|
|
||||||
|
|
||||||
void log_android(enum ggml_log_level level, const char* message, void* user_data) {
|
|
||||||
android_LogPriority priority = level == 4 ? ANDROID_LOG_ERROR : ANDROID_LOG_INFO;
|
|
||||||
__android_log_print(priority, "Whisper::JNI::cpp", "%s", message);
|
|
||||||
}
|
|
||||||
|
|
||||||
jstring stringToJava(JNIEnv *env, const std::string& source) {
|
|
||||||
return env->NewStringUTF(source.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string stringToCXX(JNIEnv *env, jstring jString) {
|
|
||||||
const char *jStringChars = env->GetStringUTFChars(jString, nullptr);
|
|
||||||
std::string result { jStringChars };
|
|
||||||
env->ReleaseStringUTFChars(jString, jStringChars);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
void throwException(JNIEnv *env, const std::string& message) {
|
|
||||||
jclass errorClass = env->FindClass("java/lang/Exception");
|
|
||||||
env->ThrowNew(errorClass, message.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT jlong JNICALL
|
|
||||||
Java_net_cozic_joplin_audio_NativeWhisperLib_00024Companion_init(
|
|
||||||
JNIEnv *env,
|
|
||||||
jobject thiz,
|
|
||||||
jstring modelPath,
|
|
||||||
jstring language,
|
|
||||||
jstring prompt,
|
|
||||||
jboolean useShortAudioContext
|
|
||||||
) {
|
|
||||||
whisper_log_set(log_android, nullptr);
|
|
||||||
|
|
||||||
try {
|
|
||||||
auto *pSession = new WhisperSession(
|
|
||||||
stringToCXX(env, modelPath), stringToCXX(env, language), stringToCXX(env, prompt), useShortAudioContext
|
|
||||||
);
|
|
||||||
return (jlong) pSession;
|
|
||||||
} catch (const std::exception& exception) {
|
|
||||||
LOGW("Failed to init whisper: %s", exception.what());
|
|
||||||
throwException(env, exception.what());
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT void JNICALL
|
|
||||||
Java_net_cozic_joplin_audio_NativeWhisperLib_00024Companion_free(JNIEnv *env, jobject thiz,
|
|
||||||
jlong pointer) {
|
|
||||||
delete reinterpret_cast<WhisperSession *>(pointer);
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT void JNICALL
|
|
||||||
Java_net_cozic_joplin_audio_NativeWhisperLib_00024Companion_addAudio(JNIEnv *env,
|
|
||||||
jobject thiz,
|
|
||||||
jlong pointer,
|
|
||||||
jfloatArray audio_data) {
|
|
||||||
auto *pSession = reinterpret_cast<WhisperSession *> (pointer);
|
|
||||||
jfloat *pAudioData = env->GetFloatArrayElements(audio_data, nullptr);
|
|
||||||
jsize lenAudioData = env->GetArrayLength(audio_data);
|
|
||||||
std::string result;
|
|
||||||
|
|
||||||
try {
|
|
||||||
pSession->addAudio(pAudioData, lenAudioData);
|
|
||||||
} catch (const std::exception& exception) {
|
|
||||||
LOGW("Failed to add to audio buffer: %s", exception.what());
|
|
||||||
throwException(env, exception.what());
|
|
||||||
}
|
|
||||||
|
|
||||||
// JNI_ABORT: "free the buffer without copying back the possible changes", pass 0 to copy
|
|
||||||
// changes (there should be no changes)
|
|
||||||
env->ReleaseFloatArrayElements(audio_data, pAudioData, JNI_ABORT);
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT jstring JNICALL
|
|
||||||
Java_net_cozic_joplin_audio_NativeWhisperLib_00024Companion_transcribeNextChunk(JNIEnv *env,
|
|
||||||
jobject thiz,
|
|
||||||
jlong pointer) {
|
|
||||||
auto *pSession = reinterpret_cast<WhisperSession *> (pointer);
|
|
||||||
std::string result;
|
|
||||||
|
|
||||||
try {
|
|
||||||
result = pSession->transcribeNextChunk();
|
|
||||||
} catch (const std::exception& exception) {
|
|
||||||
LOGW("Failed to run whisper: %s", exception.what());
|
|
||||||
throwException(env, exception.what());
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
return stringToJava(env, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT jstring JNICALL
|
|
||||||
Java_net_cozic_joplin_audio_NativeWhisperLib_00024Companion_transcribeRemaining(JNIEnv *env,
|
|
||||||
jobject thiz,
|
|
||||||
jlong pointer) {
|
|
||||||
auto *pSession = reinterpret_cast<WhisperSession *> (pointer);
|
|
||||||
std::string result;
|
|
||||||
|
|
||||||
try {
|
|
||||||
result = pSession->transcribeAll();
|
|
||||||
} catch (const std::exception& exception) {
|
|
||||||
LOGW("Failed to run whisper: %s", exception.what());
|
|
||||||
throwException(env, exception.what());
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
return stringToJava(env, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT void JNICALL
|
|
||||||
Java_net_cozic_joplin_audio_NativeWhisperLib_00024Companion_runTests(JNIEnv *env, jobject thiz) {
|
|
||||||
try {
|
|
||||||
findLongestSilence_test();
|
|
||||||
} catch (const std::exception& exception) {
|
|
||||||
LOGW("Failed to run tests: %s", exception.what());
|
|
||||||
throwException(env, exception.what());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,37 +6,36 @@ import expo.modules.ReactNativeHostWrapper
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import com.facebook.react.PackageList
|
import com.facebook.react.PackageList
|
||||||
import com.facebook.react.ReactApplication
|
import com.facebook.react.ReactApplication
|
||||||
|
import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
|
||||||
import com.facebook.react.ReactHost
|
import com.facebook.react.ReactHost
|
||||||
import com.facebook.react.ReactNativeHost
|
import com.facebook.react.ReactNativeHost
|
||||||
import com.facebook.react.ReactPackage
|
import com.facebook.react.ReactPackage
|
||||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
|
import com.facebook.react.common.ReleaseLevel
|
||||||
|
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint
|
||||||
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
|
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
|
||||||
import com.facebook.react.defaults.DefaultReactNativeHost
|
import com.facebook.react.defaults.DefaultReactNativeHost
|
||||||
import com.facebook.react.soloader.OpenSourceMergedSoMapping
|
|
||||||
import com.facebook.soloader.SoLoader
|
|
||||||
import net.cozic.joplin.audio.SpeechToTextPackage
|
|
||||||
import net.cozic.joplin.versioninfo.SystemVersionInformationPackage
|
import net.cozic.joplin.versioninfo.SystemVersionInformationPackage
|
||||||
import net.cozic.joplin.share.SharePackage
|
import net.cozic.joplin.share.SharePackage
|
||||||
import net.cozic.joplin.ssl.SslPackage
|
import net.cozic.joplin.ssl.SslPackage
|
||||||
|
|
||||||
class MainApplication : Application(), ReactApplication {
|
class MainApplication : Application(), ReactApplication {
|
||||||
override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper(this, object : DefaultReactNativeHost(this) {
|
override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper(
|
||||||
override fun getPackages(): List<ReactPackage> =
|
this,
|
||||||
|
object : DefaultReactNativeHost(this) {
|
||||||
|
override fun getPackages(): List<ReactPackage> =
|
||||||
PackageList(this).packages.apply {
|
PackageList(this).packages.apply {
|
||||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||||
add(SharePackage())
|
add(SharePackage())
|
||||||
add(SslPackage())
|
add(SslPackage())
|
||||||
add(SystemVersionInformationPackage())
|
add(SystemVersionInformationPackage())
|
||||||
add(SpeechToTextPackage())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry"
|
override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry"
|
||||||
|
|
||||||
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
|
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
|
||||||
|
|
||||||
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
|
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
|
||||||
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
|
})
|
||||||
})
|
|
||||||
|
|
||||||
override val reactHost: ReactHost
|
override val reactHost: ReactHost
|
||||||
get() = ReactNativeHostWrapper.createReactHost(this.applicationContext, reactNativeHost)
|
get() = ReactNativeHostWrapper.createReactHost(this.applicationContext, reactNativeHost)
|
||||||
@@ -44,16 +43,17 @@ class MainApplication : Application(), ReactApplication {
|
|||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
SoLoader.init(this, OpenSourceMergedSoMapping)
|
try {
|
||||||
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
DefaultNewArchitectureEntryPoint.releaseLevel = ReleaseLevel.valueOf(BuildConfig.REACT_NATIVE_RELEASE_LEVEL.uppercase())
|
||||||
// If you opted-in for the New Architecture, we load the native entry point for this app.
|
} catch (e: IllegalArgumentException) {
|
||||||
load()
|
DefaultNewArchitectureEntryPoint.releaseLevel = ReleaseLevel.STABLE
|
||||||
}
|
}
|
||||||
ApplicationLifecycleDispatcher.onApplicationCreate(this)
|
loadReactNative(this)
|
||||||
}
|
ApplicationLifecycleDispatcher.onApplicationCreate(this)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
super.onConfigurationChanged(newConfig)
|
super.onConfigurationChanged(newConfig)
|
||||||
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
|
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-5
@@ -1,5 +0,0 @@
|
|||||||
package net.cozic.joplin.audio
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidSessionIdException(id: Int) : IllegalArgumentException("Invalid session ID $id") {
|
|
||||||
}
|
|
||||||
-64
@@ -1,64 +0,0 @@
|
|||||||
package net.cozic.joplin.audio
|
|
||||||
|
|
||||||
import java.io.Closeable
|
|
||||||
|
|
||||||
class NativeWhisperLib(
|
|
||||||
modelPath: String,
|
|
||||||
languageCode: String,
|
|
||||||
prompt: String,
|
|
||||||
shortAudioContext: Boolean,
|
|
||||||
) : Closeable {
|
|
||||||
companion object {
|
|
||||||
init {
|
|
||||||
System.loadLibrary("joplin")
|
|
||||||
}
|
|
||||||
|
|
||||||
external fun runTests(): Unit;
|
|
||||||
|
|
||||||
// TODO: The example whisper.cpp project transfers pointers as Longs to the Kotlin code.
|
|
||||||
// This seems unsafe. Try changing how this is managed.
|
|
||||||
private external fun init(modelPath: String, languageCode: String, prompt: String, shortAudioContext: Boolean): Long;
|
|
||||||
private external fun free(pointer: Long): Unit;
|
|
||||||
|
|
||||||
private external fun addAudio(pointer: Long, audioData: FloatArray): Unit;
|
|
||||||
private external fun transcribeNextChunk(pointer: Long): String;
|
|
||||||
private external fun transcribeRemaining(pointer: Long): String;
|
|
||||||
}
|
|
||||||
|
|
||||||
private var closed = false
|
|
||||||
private val pointer: Long = init(modelPath, languageCode, prompt, shortAudioContext)
|
|
||||||
|
|
||||||
fun addAudio(audioData: FloatArray) {
|
|
||||||
if (closed) {
|
|
||||||
throw Exception("Cannot add audio data to a closed session")
|
|
||||||
}
|
|
||||||
|
|
||||||
Companion.addAudio(pointer, audioData)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun transcribeNextChunk(): String {
|
|
||||||
if (closed) {
|
|
||||||
throw Exception("Cannot transcribe using a closed session")
|
|
||||||
}
|
|
||||||
|
|
||||||
return Companion.transcribeNextChunk(pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun transcribeRemaining(): String {
|
|
||||||
if (closed) {
|
|
||||||
throw Exception("Cannot transcribeAll using a closed session")
|
|
||||||
}
|
|
||||||
|
|
||||||
return Companion.transcribeRemaining(pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun close() {
|
|
||||||
if (closed) {
|
|
||||||
throw Exception("Cannot close a whisper session twice")
|
|
||||||
}
|
|
||||||
|
|
||||||
closed = true
|
|
||||||
free(pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
-62
@@ -1,62 +0,0 @@
|
|||||||
package net.cozic.joplin.audio
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.Log
|
|
||||||
import java.io.Closeable
|
|
||||||
|
|
||||||
class SpeechToTextConverter(
|
|
||||||
modelPath: String,
|
|
||||||
locale: String,
|
|
||||||
prompt: String,
|
|
||||||
useShortAudioCtx: Boolean,
|
|
||||||
recorderFactory: AudioRecorderFactory,
|
|
||||||
context: Context,
|
|
||||||
) : Closeable {
|
|
||||||
private val recorder = recorderFactory(context)
|
|
||||||
private val languageCode = Regex("_.*").replace(locale, "")
|
|
||||||
private var whisper = NativeWhisperLib(
|
|
||||||
modelPath,
|
|
||||||
languageCode,
|
|
||||||
prompt,
|
|
||||||
useShortAudioCtx,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun start() {
|
|
||||||
recorder.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun convert(data: FloatArray): String {
|
|
||||||
Log.d("Whisper", "Pre-transcribe data of size ${data.size}")
|
|
||||||
whisper.addAudio(data)
|
|
||||||
val result = whisper.transcribeNextChunk()
|
|
||||||
Log.d("Whisper", "Post transcribe. Got $result")
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
fun dropFirstSeconds(seconds: Double) {
|
|
||||||
Log.i("Whisper", "Drop first seconds $seconds")
|
|
||||||
recorder.dropFirstSeconds(seconds)
|
|
||||||
}
|
|
||||||
|
|
||||||
val bufferLengthSeconds: Double get() = recorder.bufferLengthSeconds
|
|
||||||
|
|
||||||
fun convertNext(seconds: Double): String {
|
|
||||||
val buffer = recorder.pullNextSeconds(seconds)
|
|
||||||
val result = convert(buffer)
|
|
||||||
dropFirstSeconds(seconds)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Converts as many seconds of buffered data as possible, without waiting
|
|
||||||
fun convertRemaining(): String {
|
|
||||||
val buffer = recorder.pullAvailable()
|
|
||||||
whisper.addAudio(buffer)
|
|
||||||
return whisper.transcribeRemaining()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun close() {
|
|
||||||
Log.d("Whisper", "Close")
|
|
||||||
recorder.close()
|
|
||||||
whisper.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-87
@@ -1,87 +0,0 @@
|
|||||||
package net.cozic.joplin.audio
|
|
||||||
|
|
||||||
import com.facebook.react.ReactPackage
|
|
||||||
import com.facebook.react.bridge.LifecycleEventListener
|
|
||||||
import com.facebook.react.bridge.NativeModule
|
|
||||||
import com.facebook.react.bridge.Promise
|
|
||||||
import com.facebook.react.bridge.ReactApplicationContext
|
|
||||||
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
||||||
import com.facebook.react.bridge.ReactMethod
|
|
||||||
import com.facebook.react.uimanager.ViewManager
|
|
||||||
import java.util.concurrent.ExecutorService
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
|
|
||||||
class SpeechToTextPackage : ReactPackage {
|
|
||||||
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
|
||||||
return listOf<NativeModule>(SpeechToTextModule(reactContext))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
class SpeechToTextModule(
|
|
||||||
private var context: ReactApplicationContext,
|
|
||||||
) : ReactContextBaseJavaModule(context), LifecycleEventListener {
|
|
||||||
private val executorService: ExecutorService = Executors.newFixedThreadPool(1)
|
|
||||||
private val sessionManager = SpeechToTextSessionManager(executorService)
|
|
||||||
|
|
||||||
override fun getName() = "SpeechToTextModule"
|
|
||||||
|
|
||||||
override fun onHostResume() { }
|
|
||||||
override fun onHostPause() { }
|
|
||||||
override fun onHostDestroy() { }
|
|
||||||
|
|
||||||
@ReactMethod
|
|
||||||
fun runTests(promise: Promise) {
|
|
||||||
try {
|
|
||||||
NativeWhisperLib.runTests()
|
|
||||||
promise.resolve(true)
|
|
||||||
} catch (exception: Throwable) {
|
|
||||||
promise.reject(exception)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ReactMethod
|
|
||||||
fun openSession(modelPath: String, locale: String, prompt: String, useShortAudioCtx: Boolean, promise: Promise) {
|
|
||||||
val appContext = context.applicationContext
|
|
||||||
|
|
||||||
try {
|
|
||||||
val sessionId = sessionManager.openSession(modelPath, locale, prompt, useShortAudioCtx, appContext)
|
|
||||||
promise.resolve(sessionId)
|
|
||||||
} catch (exception: Throwable) {
|
|
||||||
promise.reject(exception)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ReactMethod
|
|
||||||
fun startRecording(sessionId: Int, promise: Promise) {
|
|
||||||
sessionManager.startRecording(sessionId, promise)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ReactMethod
|
|
||||||
fun getBufferLengthSeconds(sessionId: Int, promise: Promise) {
|
|
||||||
sessionManager.getBufferLengthSeconds(sessionId, promise)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ReactMethod
|
|
||||||
fun dropFirstSeconds(sessionId: Int, duration: Double, promise: Promise) {
|
|
||||||
sessionManager.dropFirstSeconds(sessionId, duration, promise)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ReactMethod
|
|
||||||
fun convertNext(sessionId: Int, duration: Double, promise: Promise) {
|
|
||||||
sessionManager.convertNext(sessionId, duration, promise)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ReactMethod
|
|
||||||
fun convertAvailable(sessionId: Int, promise: Promise) {
|
|
||||||
sessionManager.convertAvailable(sessionId, promise)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ReactMethod
|
|
||||||
fun closeSession(sessionId: Int, promise: Promise) {
|
|
||||||
sessionManager.closeSession(sessionId, promise)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-111
@@ -1,111 +0,0 @@
|
|||||||
package net.cozic.joplin.audio
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.facebook.react.bridge.Promise
|
|
||||||
import java.util.concurrent.Executor
|
|
||||||
import java.util.concurrent.locks.ReentrantLock
|
|
||||||
|
|
||||||
class SpeechToTextSession (
|
|
||||||
val converter: SpeechToTextConverter
|
|
||||||
) {
|
|
||||||
val mutex = ReentrantLock()
|
|
||||||
}
|
|
||||||
|
|
||||||
class SpeechToTextSessionManager(
|
|
||||||
private var executor: Executor,
|
|
||||||
) {
|
|
||||||
private val sessions: MutableMap<Int, SpeechToTextSession> = mutableMapOf()
|
|
||||||
private var nextSessionId: Int = 0
|
|
||||||
|
|
||||||
fun openSession(
|
|
||||||
modelPath: String,
|
|
||||||
locale: String,
|
|
||||||
prompt: String,
|
|
||||||
useShortAudioCtx: Boolean,
|
|
||||||
context: Context,
|
|
||||||
): Int {
|
|
||||||
val sessionId = nextSessionId++
|
|
||||||
sessions[sessionId] = SpeechToTextSession(
|
|
||||||
SpeechToTextConverter(
|
|
||||||
modelPath, locale, prompt, useShortAudioCtx, recorderFactory = AudioRecorder.factory, context,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return sessionId
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getSession(id: Int): SpeechToTextSession {
|
|
||||||
return sessions[id] ?: throw InvalidSessionIdException(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun concurrentWithSession(
|
|
||||||
id: Int,
|
|
||||||
callback: (session: SpeechToTextSession)->Unit,
|
|
||||||
) {
|
|
||||||
executor.execute {
|
|
||||||
val session = getSession(id)
|
|
||||||
session.mutex.lock()
|
|
||||||
try {
|
|
||||||
callback(session)
|
|
||||||
} finally {
|
|
||||||
session.mutex.unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private fun concurrentWithSession(
|
|
||||||
id: Int,
|
|
||||||
onError: (error: Throwable)->Unit,
|
|
||||||
callback: (session: SpeechToTextSession)->Unit,
|
|
||||||
) {
|
|
||||||
return concurrentWithSession(id) { session ->
|
|
||||||
try {
|
|
||||||
callback(session)
|
|
||||||
} catch (error: Throwable) {
|
|
||||||
onError(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startRecording(sessionId: Int, promise: Promise) {
|
|
||||||
this.concurrentWithSession(sessionId, promise::reject) { session ->
|
|
||||||
session.converter.start()
|
|
||||||
promise.resolve(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Left-shifts the recording buffer by [duration] seconds
|
|
||||||
fun dropFirstSeconds(sessionId: Int, duration: Double, promise: Promise) {
|
|
||||||
this.concurrentWithSession(sessionId, promise::reject) { session ->
|
|
||||||
session.converter.dropFirstSeconds(duration)
|
|
||||||
promise.resolve(sessionId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getBufferLengthSeconds(sessionId: Int, promise: Promise) {
|
|
||||||
this.concurrentWithSession(sessionId, promise::reject) { session ->
|
|
||||||
promise.resolve(session.converter.bufferLengthSeconds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Waits for the next [duration] seconds to become available, then converts
|
|
||||||
fun convertNext(sessionId: Int, duration: Double, promise: Promise) {
|
|
||||||
this.concurrentWithSession(sessionId, promise::reject) { session ->
|
|
||||||
val result = session.converter.convertNext(duration)
|
|
||||||
promise.resolve(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Converts all available recorded data
|
|
||||||
fun convertAvailable(sessionId: Int, promise: Promise) {
|
|
||||||
this.concurrentWithSession(sessionId, promise::reject) { session ->
|
|
||||||
val result = session.converter.convertRemaining()
|
|
||||||
promise.resolve(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun closeSession(sessionId: Int, promise: Promise) {
|
|
||||||
this.concurrentWithSession(sessionId) { session ->
|
|
||||||
session.converter.close()
|
|
||||||
promise.resolve(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,14 +2,14 @@
|
|||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
ext {
|
ext {
|
||||||
buildToolsVersion = "35.0.0"
|
buildToolsVersion = "36.0.0"
|
||||||
minSdkVersion = 24
|
minSdkVersion = 24
|
||||||
|
|
||||||
compileSdkVersion = 35
|
compileSdkVersion = 36
|
||||||
targetSdkVersion = 35
|
targetSdkVersion = 36
|
||||||
|
|
||||||
ndkVersion = "27.1.12297006"
|
ndkVersion = "27.1.12297006"
|
||||||
kotlinVersion = "2.0.21"
|
kotlinVersion = "2.1.20"
|
||||||
}
|
}
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryEr
|
|||||||
# When configured, Gradle will run in incubating parallel mode.
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
# This option should only be used with decoupled projects. More details, visit
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
# org.gradle.parallel=true
|
org.gradle.parallel=true
|
||||||
|
|
||||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||||
# Android operating system, and which are packaged with your app's APK
|
# Android operating system, and which are packaged with your app's APK
|
||||||
@@ -34,12 +34,17 @@ reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
|
|||||||
# your application. You should enable this flag either if you want
|
# your application. You should enable this flag either if you want
|
||||||
# to write custom TurboModules/Fabric components OR use libraries that
|
# to write custom TurboModules/Fabric components OR use libraries that
|
||||||
# are providing them.
|
# are providing them.
|
||||||
newArchEnabled=false
|
newArchEnabled=true
|
||||||
|
|
||||||
# Use this property to enable or disable the Hermes JS engine.
|
# Use this property to enable or disable the Hermes JS engine.
|
||||||
# If set to false, you will be using JSC instead.
|
# If set to false, you will be using JSC instead.
|
||||||
hermesEnabled=true
|
hermesEnabled=true
|
||||||
|
|
||||||
|
# Use this property to enable edge-to-edge display support.
|
||||||
|
# This allows your app to draw behind system bars for an immersive UI.
|
||||||
|
# Note: Only works with ReactActivity and should not be used with custom Activity.
|
||||||
|
edgeToEdgeEnabled=true
|
||||||
|
|
||||||
# To fix this error:
|
# To fix this error:
|
||||||
#
|
#
|
||||||
# > Failed to transform bcprov-jdk15on-1.68.jar
|
# > Failed to transform bcprov-jdk15on-1.68.jar
|
||||||
|
|||||||
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
Vendored
+2
-2
@@ -114,7 +114,7 @@ case "$( uname )" in #(
|
|||||||
NONSTOP* ) nonstop=true ;;
|
NONSTOP* ) nonstop=true ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
CLASSPATH="\\\"\\\""
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
# Determine the Java command to use to start the JVM.
|
||||||
@@ -213,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
|||||||
set -- \
|
set -- \
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
-classpath "$CLASSPATH" \
|
-classpath "$CLASSPATH" \
|
||||||
org.gradle.wrapper.GradleWrapperMain \
|
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||||
"$@"
|
"$@"
|
||||||
|
|
||||||
# Stop when "xargs" is not available.
|
# Stop when "xargs" is not available.
|
||||||
|
|||||||
+2
-2
@@ -70,11 +70,11 @@ goto fail
|
|||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
set CLASSPATH=
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@rem Execute Gradle
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||||
|
|
||||||
:end
|
:end
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
whisper.cpp/.gitmodules
|
|
||||||
whisper.cpp/scripts/
|
|
||||||
whisper.cpp/samples/
|
|
||||||
whisper.cpp/tests/
|
|
||||||
whisper.cpp/models/
|
|
||||||
whisper.cpp/examples/
|
|
||||||
whisper.cpp/.*/
|
|
||||||
whisper.cpp/bindings/
|
|
||||||
whisper.cpp/**/*.Dockerfile
|
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "Joplin",
|
"name": "Joplin",
|
||||||
"displayName": "Joplin"
|
"displayName": "Joplin",
|
||||||
}
|
"plugins": [
|
||||||
|
"@react-native-community/datetimepicker"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const useStyles = (theme: ThemeStyle) => {
|
|||||||
},
|
},
|
||||||
contentContainer: {
|
contentContainer: {
|
||||||
padding: 20,
|
padding: 20,
|
||||||
paddingBottom: 14,
|
paddingBottom: 14 + safeAreaPadding.paddingBottom,
|
||||||
gap: 8,
|
gap: 8,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import shim from '@joplin/lib/shim';
|
|||||||
import Logger from '@joplin/utils/Logger';
|
import Logger from '@joplin/utils/Logger';
|
||||||
import { Props, WebViewControl } from './types';
|
import { Props, WebViewControl } from './types';
|
||||||
import useCss from './utils/useCss';
|
import useCss from './utils/useCss';
|
||||||
|
import { Platform } from 'react-native';
|
||||||
|
|
||||||
const logger = Logger.create('ExtendedWebView');
|
const logger = Logger.create('ExtendedWebView');
|
||||||
|
|
||||||
@@ -141,7 +142,8 @@ const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
|
|||||||
onLoadEnd={props.onLoadEnd}
|
onLoadEnd={props.onLoadEnd}
|
||||||
onContentProcessDidTerminate={refreshWebViewAfterCrash}
|
onContentProcessDidTerminate={refreshWebViewAfterCrash}
|
||||||
onRenderProcessGone={refreshWebViewAfterCrash}
|
onRenderProcessGone={refreshWebViewAfterCrash}
|
||||||
decelerationRate='normal'
|
// See https://github.com/react-native-webview/react-native-webview/issues/3814
|
||||||
|
decelerationRate={Platform.OS === 'ios' ? 'normal' : undefined}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { KeyboardAvoidingViewProps, KeyboardAvoidingView as NativeKeyboardAvoidingView } from 'react-native';
|
||||||
|
import useKeyboardState from '../utils/hooks/useKeyboardState';
|
||||||
|
|
||||||
|
interface Props extends KeyboardAvoidingViewProps {}
|
||||||
|
|
||||||
|
const KeyboardAvoidingView: React.FC<Props> = ({ enabled, children, ...forwardedProps }) => {
|
||||||
|
const keyboardState = useKeyboardState();
|
||||||
|
|
||||||
|
enabled &&= (
|
||||||
|
// When the floating keyboard is enabled, the KeyboardAvoidingView can have a very small
|
||||||
|
// height. Don't use the KeyboardAvoidingView when the floating keyboard is enabled.
|
||||||
|
// See https://github.com/facebook/react-native/issues/29473
|
||||||
|
!keyboardState.isFloatingKeyboard
|
||||||
|
);
|
||||||
|
|
||||||
|
return <NativeKeyboardAvoidingView
|
||||||
|
behavior='padding'
|
||||||
|
{...forwardedProps}
|
||||||
|
enabled={enabled}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</NativeKeyboardAvoidingView>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default KeyboardAvoidingView;
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { RefObject, useCallback, useMemo, useRef, useState } from 'react';
|
import { RefObject, useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import { GestureResponderEvent, KeyboardAvoidingView, Modal, ModalProps, Platform, Pressable, ScrollView, ScrollViewProps, StyleSheet, View, ViewStyle } from 'react-native';
|
import { GestureResponderEvent, Modal, ModalProps, Platform, Pressable, ScrollView, ScrollViewProps, StyleSheet, View, ViewStyle } from 'react-native';
|
||||||
import FocusControl from './accessibility/FocusControl/FocusControl';
|
import FocusControl from './accessibility/FocusControl/FocusControl';
|
||||||
import { msleep, Second } from '@joplin/utils/time';
|
import { msleep, Second } from '@joplin/utils/time';
|
||||||
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
||||||
import { ModalState } from './accessibility/FocusControl/types';
|
import { ModalState } from './accessibility/FocusControl/types';
|
||||||
import useSafeAreaPadding from '../utils/hooks/useSafeAreaPadding';
|
import useSafeAreaPadding from '../utils/hooks/useSafeAreaPadding';
|
||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
import KeyboardAvoidingView from './KeyboardAvoidingView';
|
||||||
|
|
||||||
export interface ModalElementProps extends ModalProps {
|
export interface ModalElementProps extends ModalProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -175,7 +176,7 @@ const ModalElement: React.FC<ModalElementProps> = ({
|
|||||||
{...modalProps}
|
{...modalProps}
|
||||||
>
|
>
|
||||||
{scrollOverflow ? (
|
{scrollOverflow ? (
|
||||||
<KeyboardAvoidingView behavior='padding' style={styles.keyboardAvoidingView}>
|
<KeyboardAvoidingView style={styles.keyboardAvoidingView} enabled={true}>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
{...extraScrollViewProps}
|
{...extraScrollViewProps}
|
||||||
style={[styles.modalScrollView, extraScrollViewProps.style]}
|
style={[styles.modalScrollView, extraScrollViewProps.style]}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ const ProfileListItem: React.FC<ProfileItemProps> = ({ profile, profileConfig, s
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const switchProfileMessage = _('To switch the profile, the app is going to close and you will need to restart it.');
|
const switchProfileMessage = _('To switch the profile, the app is going to restart.');
|
||||||
if (shim.mobilePlatform() === 'web') {
|
if (shim.mobilePlatform() === 'web') {
|
||||||
if (confirm(switchProfileMessage)) {
|
if (confirm(switchProfileMessage)) {
|
||||||
void doIt();
|
void doIt();
|
||||||
|
|||||||
@@ -688,8 +688,8 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
|
|||||||
const menuComp =
|
const menuComp =
|
||||||
!menuOptions.length || !showContextMenuButton ? null : (
|
!menuOptions.length || !showContextMenuButton ? null : (
|
||||||
<Menu themeId={this.props.themeId} options={menuOptions}>
|
<Menu themeId={this.props.themeId} options={menuOptions}>
|
||||||
<View style={contextMenuStyle} accessibilityLabel={_('Actions')}>
|
<View style={contextMenuStyle}>
|
||||||
<Icon name="ionicon ellipsis-vertical" style={this.styles().contextMenuTrigger} accessibilityLabel={null}/>
|
<Icon name="ionicon ellipsis-vertical" style={this.styles().contextMenuTrigger} accessibilityLabel={_('Actions')}/>
|
||||||
</View>
|
</View>
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import AccessibleView from './accessibility/AccessibleView';
|
|||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
import useReduceMotionEnabled from '../utils/hooks/useReduceMotionEnabled';
|
import useReduceMotionEnabled from '../utils/hooks/useReduceMotionEnabled';
|
||||||
import { themeStyle } from './global-style';
|
import { themeStyle } from './global-style';
|
||||||
|
import useSafeAreaPadding from '../utils/hooks/useSafeAreaPadding';
|
||||||
|
|
||||||
export enum SideMenuPosition {
|
export enum SideMenuPosition {
|
||||||
Left = 'left',
|
Left = 'left',
|
||||||
@@ -40,6 +41,8 @@ interface UseStylesProps {
|
|||||||
|
|
||||||
const useStyles = ({ themeId, isLeftMenu, menuWidth, menuOpenFraction }: UseStylesProps) => {
|
const useStyles = ({ themeId, isLeftMenu, menuWidth, menuOpenFraction }: UseStylesProps) => {
|
||||||
const { height: windowHeight, width: windowWidth } = useWindowDimensions();
|
const { height: windowHeight, width: windowWidth } = useWindowDimensions();
|
||||||
|
const safeAreaInsets = useSafeAreaPadding();
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const theme = themeStyle(themeId);
|
const theme = themeStyle(themeId);
|
||||||
return StyleSheet.create({
|
return StyleSheet.create({
|
||||||
@@ -53,7 +56,7 @@ const useStyles = ({ themeId, isLeftMenu, menuWidth, menuOpenFraction }: UseStyl
|
|||||||
contentOuterWrapper: {
|
contentOuterWrapper: {
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
flexShrink: 1,
|
flexShrink: 1,
|
||||||
width: windowWidth,
|
width: '100%',
|
||||||
height: windowHeight,
|
height: windowHeight,
|
||||||
transform: [{
|
transform: [{
|
||||||
translateX: menuOpenFraction.interpolate({
|
translateX: menuOpenFraction.interpolate({
|
||||||
@@ -71,11 +74,18 @@ const useStyles = ({ themeId, isLeftMenu, menuWidth, menuOpenFraction }: UseStyl
|
|||||||
flexShrink: 1,
|
flexShrink: 1,
|
||||||
},
|
},
|
||||||
menuWrapper: {
|
menuWrapper: {
|
||||||
|
backgroundColor: theme.backgroundColor,
|
||||||
|
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
width: menuWidth,
|
width: menuWidth,
|
||||||
|
|
||||||
|
paddingLeft: isLeftMenu ? safeAreaInsets.paddingLeft : 0,
|
||||||
|
paddingRight: isLeftMenu ? 0 : safeAreaInsets.paddingRight,
|
||||||
|
paddingTop: safeAreaInsets.paddingTop,
|
||||||
|
paddingBottom: safeAreaInsets.paddingBottom,
|
||||||
|
|
||||||
// In React Native, RTL replaces `left` with `right` and `right` with `left`.
|
// In React Native, RTL replaces `left` with `right` and `right` with `left`.
|
||||||
// As such, we need to reverse the normal direction in RTL mode.
|
// As such, we need to reverse the normal direction in RTL mode.
|
||||||
...(isLeftMenu === !I18nManager.isRTL ? {
|
...(isLeftMenu === !I18nManager.isRTL ? {
|
||||||
@@ -107,7 +117,7 @@ const useStyles = ({ themeId, isLeftMenu, menuWidth, menuOpenFraction }: UseStyl
|
|||||||
width: windowWidth,
|
width: windowWidth,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [themeId, isLeftMenu, windowWidth, windowHeight, menuWidth, menuOpenFraction]);
|
}, [themeId, isLeftMenu, windowWidth, windowHeight, menuWidth, menuOpenFraction, safeAreaInsets]);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface UseAnimationsProps {
|
interface UseAnimationsProps {
|
||||||
|
|||||||
@@ -2,13 +2,16 @@ import * as React from 'react';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import NotesScreen from './screens/Notes/Notes';
|
import NotesScreen from './screens/Notes/Notes';
|
||||||
import SearchScreen from './screens/SearchScreen';
|
import SearchScreen from './screens/SearchScreen';
|
||||||
import { KeyboardAvoidingView, Platform, View } from 'react-native';
|
import { Platform, View, StyleSheet } from 'react-native';
|
||||||
import { AppState } from '../utils/types';
|
import { AppState } from '../utils/types';
|
||||||
import { themeStyle } from './global-style';
|
import { themeStyle } from './global-style';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import useKeyboardState from '../utils/hooks/useKeyboardState';
|
import useKeyboardState from '../utils/hooks/useKeyboardState';
|
||||||
import usePrevious from '@joplin/lib/hooks/usePrevious';
|
import usePrevious from '@joplin/lib/hooks/usePrevious';
|
||||||
import FeedbackBanner from './FeedbackBanner';
|
import FeedbackBanner from './FeedbackBanner';
|
||||||
|
import { Theme } from '@joplin/lib/themes/type';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import KeyboardAvoidingView from './KeyboardAvoidingView';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
@@ -20,6 +23,15 @@ interface Props {
|
|||||||
themeId: number;
|
themeId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const useStyles = (theme: Theme) => {
|
||||||
|
return useMemo(() => {
|
||||||
|
return StyleSheet.create({
|
||||||
|
keyboardAvoidingView: { flex: 1, backgroundColor: theme.backgroundColor },
|
||||||
|
});
|
||||||
|
}, [theme]);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
const AppNavComponent: React.FC<Props> = (props) => {
|
const AppNavComponent: React.FC<Props> = (props) => {
|
||||||
const keyboardState = useKeyboardState();
|
const keyboardState = useKeyboardState();
|
||||||
const safeAreaPadding = useSafeAreaInsets();
|
const safeAreaPadding = useSafeAreaInsets();
|
||||||
@@ -50,20 +62,18 @@ const AppNavComponent: React.FC<Props> = (props) => {
|
|||||||
const searchScreenLoaded = searchScreenVisible || (previousRouteName === 'Search' && route.routeName === 'Note');
|
const searchScreenLoaded = searchScreenVisible || (previousRouteName === 'Search' && route.routeName === 'Note');
|
||||||
|
|
||||||
const theme = themeStyle(props.themeId);
|
const theme = themeStyle(props.themeId);
|
||||||
|
const styles = useStyles(theme);
|
||||||
const style = { flex: 1, backgroundColor: theme.backgroundColor };
|
const autocompletionBarPadding = keyboardState.keyboardVisible ? safeAreaPadding.top : 0;
|
||||||
|
|
||||||
// When the floating keyboard is enabled, the KeyboardAvoidingView can have a very small
|
|
||||||
// height. Don't use the KeyboardAvoidingView when the floating keyboard is enabled.
|
|
||||||
// See https://github.com/facebook/react-native/issues/29473
|
|
||||||
const keyboardAvoidingViewEnabled = !keyboardState.isFloatingKeyboard;
|
|
||||||
const autocompletionBarPadding = Platform.OS === 'ios' && keyboardState.keyboardVisible ? safeAreaPadding.top : 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
enabled={keyboardAvoidingViewEnabled}
|
style={styles.keyboardAvoidingView}
|
||||||
behavior={Platform.OS === 'ios' ? 'padding' : null}
|
enabled={
|
||||||
style={style}
|
// Workaround: On Android 15 and 16, the main app content seems to auto-resize when the keyboard is shown.
|
||||||
|
// On earlier Android versions (and in modals), this does not seem to be the case.
|
||||||
|
(Platform.OS === 'android' && Platform.Version < 35)
|
||||||
|
|| Platform.OS === 'ios'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<NotesScreen visible={notesScreenVisible} />
|
<NotesScreen visible={notesScreenVisible} />
|
||||||
{searchScreenLoaded && <SearchScreen visible={searchScreenVisible} />}
|
{searchScreenLoaded && <SearchScreen visible={searchScreenVisible} />}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import usePrevious from '@joplin/lib/hooks/usePrevious';
|
|||||||
import PlatformImplementation from '../../services/plugins/PlatformImplementation';
|
import PlatformImplementation from '../../services/plugins/PlatformImplementation';
|
||||||
import AccessibleView from '../accessibility/AccessibleView';
|
import AccessibleView from '../accessibility/AccessibleView';
|
||||||
import useOnDevPluginsUpdated from './utils/useOnDevPluginsUpdated';
|
import useOnDevPluginsUpdated from './utils/useOnDevPluginsUpdated';
|
||||||
|
import { ViewStyle } from 'react-native';
|
||||||
|
|
||||||
const logger = Logger.create('PluginRunnerWebView');
|
const logger = Logger.create('PluginRunnerWebView');
|
||||||
|
|
||||||
@@ -98,6 +99,17 @@ interface Props {
|
|||||||
themeId: number;
|
themeId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The WebView needs to have a non-zero size to be rendered by
|
||||||
|
// newer React Native versions. This style makes it visually hidden.
|
||||||
|
const hiddenStyle: ViewStyle = {
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
opacity: 0,
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
zIndex: -1,
|
||||||
|
};
|
||||||
|
|
||||||
const PluginRunnerWebViewComponent: React.FC<Props> = props => {
|
const PluginRunnerWebViewComponent: React.FC<Props> = props => {
|
||||||
const webviewRef = useRef<WebViewControl>(null);
|
const webviewRef = useRef<WebViewControl>(null);
|
||||||
|
|
||||||
@@ -189,7 +201,7 @@ const PluginRunnerWebViewComponent: React.FC<Props> = props => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccessibleView style={{ display: 'none' }} inert={true}>
|
<AccessibleView style={hiddenStyle} inert={true}>
|
||||||
{renderWebView()}
|
{renderWebView()}
|
||||||
</AccessibleView>
|
</AccessibleView>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,16 +6,14 @@
|
|||||||
|
|
||||||
// So there's basically still a one way flux: React => SQLite => Redux => React
|
// So there's basically still a one way flux: React => SQLite => Redux => React
|
||||||
|
|
||||||
|
import './utils/initReact';
|
||||||
import './utils/polyfills';
|
import './utils/polyfills';
|
||||||
|
|
||||||
|
import Root from './root';
|
||||||
import { LogBox } from 'react-native';
|
import { LogBox } from 'react-native';
|
||||||
import { registerRootComponent } from 'expo';
|
import { registerRootComponent } from 'expo';
|
||||||
// Allows loading image assets. See https://github.com/expo/expo/issues/31240
|
// Allows loading image assets. See https://github.com/expo/expo/issues/31240
|
||||||
import 'expo-asset';
|
import 'expo-asset';
|
||||||
import shim from '@joplin/lib/shim';
|
|
||||||
shim.setReact(require('react'));
|
|
||||||
|
|
||||||
const Root = require('./root').default;
|
|
||||||
|
|
||||||
// Seems JavaScript developers love adding warnings everywhere, even when these warnings can't be fixed
|
// Seems JavaScript developers love adding warnings everywhere, even when these warnings can't be fixed
|
||||||
// or don't really matter. Because we want important warnings to actually be fixed, we disable
|
// or don't really matter. Because we want important warnings to actually be fixed, we disable
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import './utils/polyfills';
|
import './utils/polyfills';
|
||||||
|
import './utils/initReact';
|
||||||
import { AppRegistry } from 'react-native';
|
import { AppRegistry } from 'react-native';
|
||||||
import Root from './root';
|
import Root from './root';
|
||||||
import Setting from '@joplin/lib/models/Setting';
|
import Setting from '@joplin/lib/models/Setting';
|
||||||
|
|||||||
@@ -35,6 +35,8 @@
|
|||||||
</dict>
|
</dict>
|
||||||
<key>NSLocationWhenInUseUsageDescription</key>
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
<string></string>
|
<string></string>
|
||||||
|
<key>RCTNewArchEnabled</key>
|
||||||
|
<true/>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIRequiredDeviceCapabilities</key>
|
<key>UIRequiredDeviceCapabilities</key>
|
||||||
|
|||||||
@@ -321,6 +321,8 @@
|
|||||||
files = (
|
files = (
|
||||||
);
|
);
|
||||||
inputPaths = (
|
inputPaths = (
|
||||||
|
"$(SRCROOT)/.xcode.env",
|
||||||
|
"$(SRCROOT)/.xcode.env.local",
|
||||||
);
|
);
|
||||||
name = "Bundle React Native code and images";
|
name = "Bundle React Native code and images";
|
||||||
outputPaths = (
|
outputPaths = (
|
||||||
@@ -339,13 +341,12 @@
|
|||||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
|
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/RNDeviceInfo/RNDeviceInfoPrivacyInfo.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/RNDeviceInfo/RNDeviceInfoPrivacyInfo.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/ReactNativeFs/RNFS_PrivacyInfo.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/react-native-image-picker/RNImagePickerPrivacyInfo.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/react-native-image-picker/RNImagePickerPrivacyInfo.bundle",
|
||||||
"${PODS_ROOT}/../../node_modules/@react-native-vector-icons/fontawesome5/fonts/FontAwesome5_Brands.ttf",
|
"${PODS_ROOT}/../../node_modules/@react-native-vector-icons/fontawesome5/fonts/FontAwesome5_Brands.ttf",
|
||||||
"${PODS_ROOT}/../../node_modules/@react-native-vector-icons/fontawesome5/fonts/FontAwesome5_Regular.ttf",
|
"${PODS_ROOT}/../../node_modules/@react-native-vector-icons/fontawesome5/fonts/FontAwesome5_Regular.ttf",
|
||||||
@@ -359,13 +360,12 @@
|
|||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
|
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNDeviceInfoPrivacyInfo.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNDeviceInfoPrivacyInfo.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNFS_PrivacyInfo.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImage.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNImagePickerPrivacyInfo.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNImagePickerPrivacyInfo.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Brands.ttf",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Brands.ttf",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Regular.ttf",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Regular.ttf",
|
||||||
@@ -409,11 +409,15 @@
|
|||||||
inputPaths = (
|
inputPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Joplin/Pods-Joplin-frameworks.sh",
|
"${PODS_ROOT}/Target Support Files/Pods-Joplin/Pods-Joplin-frameworks.sh",
|
||||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework/OpenSSL",
|
"${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework/OpenSSL",
|
||||||
|
"${PODS_XCFRAMEWORKS_BUILD_DIR}/React-Core-prebuilt/React.framework/React",
|
||||||
|
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ReactNativeDependencies/ReactNativeDependencies.framework/ReactNativeDependencies",
|
||||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes",
|
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes",
|
||||||
);
|
);
|
||||||
name = "[CP] Embed Pods Frameworks";
|
name = "[CP] Embed Pods Frameworks";
|
||||||
outputPaths = (
|
outputPaths = (
|
||||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework",
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework",
|
||||||
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React.framework",
|
||||||
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ReactNativeDependencies.framework",
|
||||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@@ -452,11 +456,16 @@
|
|||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
);
|
);
|
||||||
inputPaths = (
|
inputPaths = (
|
||||||
|
"$(SRCROOT)/.xcode.env",
|
||||||
|
"$(SRCROOT)/.xcode.env.local",
|
||||||
|
"$(SRCROOT)/Joplin/Joplin.entitlements",
|
||||||
|
"$(SRCROOT)/Pods/Target Support Files/Pods-Joplin/expo-configure-project.sh",
|
||||||
);
|
);
|
||||||
name = "[Expo] Configure project";
|
name = "[Expo] Configure project";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
);
|
);
|
||||||
outputPaths = (
|
outputPaths = (
|
||||||
|
"$(SRCROOT)/Pods/Target Support Files/Pods-Joplin/ExpoModulesProvider.swift",
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
@@ -653,6 +662,7 @@
|
|||||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
|
||||||
|
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
|
||||||
USE_HERMES = true;
|
USE_HERMES = true;
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
@@ -729,6 +739,7 @@
|
|||||||
);
|
);
|
||||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
|
||||||
USE_HERMES = true;
|
USE_HERMES = true;
|
||||||
VALIDATE_PRODUCT = YES;
|
VALIDATE_PRODUCT = YES;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -44,16 +44,15 @@
|
|||||||
<false/>
|
<false/>
|
||||||
<key>NSAllowsLocalNetworking</key>
|
<key>NSAllowsLocalNetworking</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
|
||||||
<!-- Left over from before upgrading from RN 0.71, 0.73 -->
|
<!-- Left over from before upgrading from RN 0.71, 0.73 -->
|
||||||
<key>NSExceptionDomains</key>
|
<key>NSExceptionDomains</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>localhost</key>
|
<key>api.joplincloud.local</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
<key>api.joplincloud.local</key>
|
<key>localhost</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||||
<true/>
|
<true/>
|
||||||
@@ -62,18 +61,22 @@
|
|||||||
</dict>
|
</dict>
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
<string>To allow attaching a photo to a note</string>
|
<string>To allow attaching a photo to a note</string>
|
||||||
|
<key>NSFaceIDUsageDescription</key>
|
||||||
|
<string>$(PRODUCT_NAME) requires FaceID access to secure access to the application</string>
|
||||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||||
<string>To add geo-location information to a note. Can be disabled in app.</string>
|
<string>To add geo-location information to a note. Can be disabled in app.</string>
|
||||||
<key>NSLocationAlwaysUsageDescription</key>
|
<key>NSLocationAlwaysUsageDescription</key>
|
||||||
<string>To add geo-location information to a note. Can be disabled in app.</string>
|
<string>To add geo-location information to a note. Can be disabled in app.</string>
|
||||||
<key>NSLocationWhenInUseUsageDescription</key>
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
<string>To add geo-location information to a note. Can be disabled in app.</string>
|
<string>To add geo-location information to a note. Can be disabled in app.</string>
|
||||||
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
|
<string>To allow attaching voice recordings to a note</string>
|
||||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||||
<string>The images will be displayed on your notes.</string>
|
<string>The images will be displayed on your notes.</string>
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
<string>To allow attaching images to a note</string>
|
<string>To allow attaching images to a note</string>
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
<key>RCTNewArchEnabled</key>
|
||||||
<string>To allow attaching voice recordings to a note</string>
|
<true/>
|
||||||
<key>UIAppFonts</key>
|
<key>UIAppFonts</key>
|
||||||
<array>
|
<array>
|
||||||
<string>AntDesign.ttf</string>
|
<string>AntDesign.ttf</string>
|
||||||
@@ -86,6 +89,10 @@
|
|||||||
<string>MaterialDesignIcons.ttf</string>
|
<string>MaterialDesignIcons.ttf</string>
|
||||||
<string>MaterialCommunityIcons.ttf</string>
|
<string>MaterialCommunityIcons.ttf</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>UIBackgroundModes</key>
|
||||||
|
<array>
|
||||||
|
<string>audio</string>
|
||||||
|
</array>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIRequiredDeviceCapabilities</key>
|
<key>UIRequiredDeviceCapabilities</key>
|
||||||
@@ -109,11 +116,5 @@
|
|||||||
<string>Automatic</string>
|
<string>Automatic</string>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>NSFaceIDUsageDescription</key>
|
|
||||||
<string>$(PRODUCT_NAME) requires FaceID access to secure access to the application</string>
|
|
||||||
<key>UIBackgroundModes</key>
|
|
||||||
<array>
|
|
||||||
<string>audio</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -5,23 +5,33 @@ require File.join(File.dirname(`node --print "require.resolve('react-native/pack
|
|||||||
require 'json'
|
require 'json'
|
||||||
podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {}
|
podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {}
|
||||||
|
|
||||||
ENV['RCT_NEW_ARCH_ENABLED'] = '0' if podfile_properties['newArchEnabled'] == 'false'
|
def ccache_enabled?(podfile_properties)
|
||||||
ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] = podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR']
|
# Environment variable takes precedence
|
||||||
|
return ENV['USE_CCACHE'] == '1' if ENV['USE_CCACHE']
|
||||||
|
|
||||||
|
# Fall back to Podfile properties
|
||||||
|
podfile_properties['apple.ccacheEnabled'] == 'true'
|
||||||
|
end
|
||||||
|
|
||||||
|
ENV['RCT_NEW_ARCH_ENABLED'] ||= '0' if podfile_properties['newArchEnabled'] == 'false'
|
||||||
|
ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] ||= podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR']
|
||||||
|
ENV['RCT_USE_RN_DEP'] ||= '1' if podfile_properties['ios.buildReactNativeFromSource'] != 'true' && podfile_properties['newArchEnabled'] != 'false'
|
||||||
|
ENV['RCT_USE_PREBUILT_RNCORE'] ||= '1' if podfile_properties['ios.buildReactNativeFromSource'] != 'true' && podfile_properties['newArchEnabled'] != 'false'
|
||||||
platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1'
|
platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1'
|
||||||
install! 'cocoapods',
|
|
||||||
:deterministic_uuids => false
|
|
||||||
|
|
||||||
prepare_react_native_project!
|
prepare_react_native_project!
|
||||||
|
|
||||||
target 'Joplin' do
|
target 'Joplin' do
|
||||||
use_expo_modules!
|
use_expo_modules!
|
||||||
|
|
||||||
if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1'
|
if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] != '0'
|
||||||
config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"];
|
config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"];
|
||||||
else
|
else
|
||||||
config_command = [
|
config_command = [
|
||||||
'npx',
|
'node',
|
||||||
|
'--no-warnings',
|
||||||
|
'--eval',
|
||||||
|
'require(\'expo/bin/autolinking\')',
|
||||||
'expo-modules-autolinking',
|
'expo-modules-autolinking',
|
||||||
'react-native-config',
|
'react-native-config',
|
||||||
'--json',
|
'--json',
|
||||||
@@ -35,13 +45,14 @@ target 'Joplin' do
|
|||||||
use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks']
|
use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks']
|
||||||
use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS']
|
use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS']
|
||||||
|
|
||||||
|
|
||||||
use_react_native!(
|
use_react_native!(
|
||||||
:path => config[:reactNativePath],
|
:path => config[:reactNativePath],
|
||||||
|
:hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes',
|
||||||
# An absolute path to your application root.
|
# An absolute path to your application root.
|
||||||
:app_path => "#{Pod::Config.instance.installation_root}/.."
|
:app_path => "#{Pod::Config.instance.installation_root}/..",
|
||||||
|
:privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false',
|
||||||
)
|
)
|
||||||
|
|
||||||
pod 'JoplinRNShareExtension', :path => 'ShareExtension'
|
pod 'JoplinRNShareExtension', :path => 'ShareExtension'
|
||||||
|
|
||||||
post_install do |installer|
|
post_install do |installer|
|
||||||
@@ -50,7 +61,7 @@ target 'Joplin' do
|
|||||||
installer,
|
installer,
|
||||||
config[:reactNativePath],
|
config[:reactNativePath],
|
||||||
:mac_catalyst_enabled => false,
|
:mac_catalyst_enabled => false,
|
||||||
# :ccache_enabled => true
|
:ccache_enabled => ccache_enabled?(podfile_properties),
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+1049
-830
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"newArchEnabled": "false"
|
"newArchEnabled": "true"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,5 +43,7 @@
|
|||||||
<key>NSExtensionPointIdentifier</key>
|
<key>NSExtensionPointIdentifier</key>
|
||||||
<string>com.apple.share-services</string>
|
<string>com.apple.share-services</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>RCTNewArchEnabled</key>
|
||||||
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ const emptyMockPackages = [
|
|||||||
'@joplin/react-native-saf-x',
|
'@joplin/react-native-saf-x',
|
||||||
'expo-av',
|
'expo-av',
|
||||||
'expo-av/build/Audio',
|
'expo-av/build/Audio',
|
||||||
|
'expo-image-manipulator',
|
||||||
];
|
];
|
||||||
for (const packageName of emptyMockPackages) {
|
for (const packageName of emptyMockPackages) {
|
||||||
jest.doMock(packageName, () => {
|
jest.doMock(packageName, () => {
|
||||||
@@ -130,7 +131,7 @@ mockIconLibrary('@react-native-vector-icons/fontawesome5', 'FontAwesome5');
|
|||||||
// Use a temporary folder instead.
|
// Use a temporary folder instead.
|
||||||
const tempDirectoryPath = path.join(tmpdir(), `appmobile-test-${uuid.createNano()}`);
|
const tempDirectoryPath = path.join(tmpdir(), `appmobile-test-${uuid.createNano()}`);
|
||||||
|
|
||||||
jest.doMock('react-native-fs', () => {
|
jest.doMock('@dr.pogodin/react-native-fs', () => {
|
||||||
return {
|
return {
|
||||||
CachesDirectoryPath: tempDirectoryPath,
|
CachesDirectoryPath: tempDirectoryPath,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const localPackages = {
|
|||||||
'@joplin/tools': path.resolve(__dirname, '../tools/'),
|
'@joplin/tools': path.resolve(__dirname, '../tools/'),
|
||||||
'@joplin/utils': path.resolve(__dirname, '../utils/'),
|
'@joplin/utils': path.resolve(__dirname, '../utils/'),
|
||||||
'@joplin/fork-htmlparser2': path.resolve(__dirname, '../fork-htmlparser2/'),
|
'@joplin/fork-htmlparser2': path.resolve(__dirname, '../fork-htmlparser2/'),
|
||||||
|
'@joplin/whisper-voice-typing': path.resolve(__dirname, '../whisper-voice-typing/'),
|
||||||
'@joplin/fork-uslug': path.resolve(__dirname, '../fork-uslug/'),
|
'@joplin/fork-uslug': path.resolve(__dirname, '../fork-uslug/'),
|
||||||
'@joplin/react-native-saf-x': path.resolve(__dirname, '../react-native-saf-x/'),
|
'@joplin/react-native-saf-x': path.resolve(__dirname, '../react-native-saf-x/'),
|
||||||
'@joplin/react-native-alarm-notification': path.resolve(__dirname, '../react-native-alarm-notification/'),
|
'@joplin/react-native-alarm-notification': path.resolve(__dirname, '../react-native-alarm-notification/'),
|
||||||
|
|||||||
@@ -21,13 +21,14 @@
|
|||||||
"postinstall": "jetify"
|
"postinstall": "jetify"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bam.tech/react-native-image-resizer": "3.0.11",
|
"@dr.pogodin/react-native-fs": "2.36.2",
|
||||||
"@joplin/editor": "~3.6",
|
"@joplin/editor": "~3.6",
|
||||||
"@joplin/lib": "~3.6",
|
"@joplin/lib": "~3.6",
|
||||||
"@joplin/react-native-alarm-notification": "~3.6",
|
"@joplin/react-native-alarm-notification": "~3.6",
|
||||||
"@joplin/react-native-saf-x": "~3.6",
|
"@joplin/react-native-saf-x": "~3.6",
|
||||||
"@joplin/renderer": "~3.6",
|
"@joplin/renderer": "~3.6",
|
||||||
"@joplin/utils": "~3.6",
|
"@joplin/utils": "~3.6",
|
||||||
|
"@joplin/whisper-voice-typing": "~3.6",
|
||||||
"@js-draw/material-icons": "1.33.0",
|
"@js-draw/material-icons": "1.33.0",
|
||||||
"@react-native-clipboard/clipboard": "1.16.3",
|
"@react-native-clipboard/clipboard": "1.16.3",
|
||||||
"@react-native-community/datetimepicker": "8.4.7",
|
"@react-native-community/datetimepicker": "8.4.7",
|
||||||
@@ -47,40 +48,41 @@
|
|||||||
"crypto-browserify": "3.12.1",
|
"crypto-browserify": "3.12.1",
|
||||||
"deprecated-react-native-prop-types": "5.0.0",
|
"deprecated-react-native-prop-types": "5.0.0",
|
||||||
"events": "3.3.0",
|
"events": "3.3.0",
|
||||||
"expo": "53.0.23",
|
"expo": "54.0.31",
|
||||||
"expo-av": "15.1.7",
|
"expo-av": "16.0.8",
|
||||||
"expo-camera": "16.1.11",
|
"expo-camera": "17.0.10",
|
||||||
"expo-local-authentication": "16.0.5",
|
"expo-image-manipulator": "14.0.8",
|
||||||
|
"expo-local-authentication": "17.0.8",
|
||||||
"js-draw": "1.33.0",
|
"js-draw": "1.33.0",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"md5": "2.3.0",
|
"md5": "2.3.0",
|
||||||
"path-browserify": "1.0.1",
|
"path-browserify": "1.0.1",
|
||||||
"prop-types": "15.8.1",
|
"prop-types": "15.8.1",
|
||||||
"punycode": "2.3.1",
|
"punycode": "2.3.1",
|
||||||
"react": "19.0.0",
|
"react": "19.1.0",
|
||||||
"react-native": "0.79.2",
|
"react-native": "0.81.5",
|
||||||
"react-native-device-info": "14.1.1",
|
"react-native-device-info": "14.1.1",
|
||||||
"react-native-dropdownalert": "5.2.0",
|
"react-native-dropdownalert": "5.2.0",
|
||||||
"react-native-exit-app": "2.0.0",
|
|
||||||
"react-native-file-viewer": "2.1.5",
|
"react-native-file-viewer": "2.1.5",
|
||||||
"react-native-fs": "2.20.0",
|
|
||||||
"react-native-get-random-values": "1.11.0",
|
"react-native-get-random-values": "1.11.0",
|
||||||
"react-native-image-picker": "8.2.1",
|
"react-native-image-picker": "8.2.1",
|
||||||
"react-native-localize": "3.5.4",
|
"react-native-localize": "3.5.4",
|
||||||
"react-native-modal-datetime-picker": "18.0.0",
|
"react-native-modal-datetime-picker": "18.0.0",
|
||||||
|
"react-native-nitro-modules": "0.33.2",
|
||||||
"react-native-paper": "5.14.5",
|
"react-native-paper": "5.14.5",
|
||||||
"react-native-popup-menu": "0.17.0",
|
"react-native-popup-menu": "0.17.0",
|
||||||
"react-native-quick-actions": "0.3.13",
|
"react-native-quick-actions": "0.3.13",
|
||||||
|
"react-native-quick-base64": "2.2.2",
|
||||||
"react-native-quick-crypto": "0.7.17",
|
"react-native-quick-crypto": "0.7.17",
|
||||||
"react-native-rsa-native": "2.0.5",
|
"react-native-rsa-native": "2.0.5",
|
||||||
"react-native-safe-area-context": "5.6.2",
|
"react-native-safe-area-context": "5.6.2",
|
||||||
"react-native-securerandom": "1.0.1",
|
"react-native-securerandom": "1.0.1",
|
||||||
"react-native-share": "12.2.0",
|
"react-native-share": "12.2.0",
|
||||||
"react-native-sqlite-storage": "6.0.1",
|
"react-native-sqlite-storage": "6.0.1",
|
||||||
"react-native-svg": "15.13.0",
|
"react-native-svg": "15.12.1",
|
||||||
"react-native-url-polyfill": "2.0.0",
|
"react-native-url-polyfill": "2.0.0",
|
||||||
"react-native-version-info": "1.1.1",
|
"react-native-version-info": "1.1.1",
|
||||||
"react-native-webview": "13.16.0",
|
"react-native-webview": "13.15.0",
|
||||||
"react-native-zip-archive": "7.0.2",
|
"react-native-zip-archive": "7.0.2",
|
||||||
"react-redux": "8.1.3",
|
"react-redux": "8.1.3",
|
||||||
"redux": "4.2.1",
|
"redux": "4.2.1",
|
||||||
@@ -101,18 +103,18 @@
|
|||||||
"@joplin/turndown": "~4.0.80",
|
"@joplin/turndown": "~4.0.80",
|
||||||
"@joplin/turndown-plugin-gfm": "~1.0.62",
|
"@joplin/turndown-plugin-gfm": "~1.0.62",
|
||||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.0",
|
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.0",
|
||||||
"@react-native-community/cli": "16.0.3",
|
"@react-native-community/cli": "20.0.0",
|
||||||
"@react-native-community/cli-platform-android": "16.0.3",
|
"@react-native-community/cli-platform-android": "20.0.0",
|
||||||
"@react-native-community/cli-platform-ios": "16.0.3",
|
"@react-native-community/cli-platform-ios": "20.0.0",
|
||||||
"@react-native/babel-preset": "0.80.1",
|
"@react-native/babel-preset": "0.81.5",
|
||||||
"@react-native/metro-config": "0.79.5",
|
"@react-native/metro-config": "0.81.5",
|
||||||
"@react-native/typescript-config": "0.80.2",
|
"@react-native/typescript-config": "0.81.5",
|
||||||
"@sqlite.org/sqlite-wasm": "3.46.0-build2",
|
"@sqlite.org/sqlite-wasm": "3.46.0-build2",
|
||||||
"@testing-library/react-native": "13.2.0",
|
"@testing-library/react-native": "13.2.0",
|
||||||
"@types/fs-extra": "11.0.4",
|
"@types/fs-extra": "11.0.4",
|
||||||
"@types/jest": "29.5.14",
|
"@types/jest": "29.5.14",
|
||||||
"@types/node": "18.19.130",
|
"@types/node": "18.19.130",
|
||||||
"@types/react": "19.0.14",
|
"@types/react": "19.1.10",
|
||||||
"@types/react-redux": "7.1.33",
|
"@types/react-redux": "7.1.33",
|
||||||
"@types/serviceworker": "0.0.164",
|
"@types/serviceworker": "0.0.164",
|
||||||
"@types/tar-stream": "3.1.4",
|
"@types/tar-stream": "3.1.4",
|
||||||
@@ -130,10 +132,10 @@
|
|||||||
"jsdom": "26.1.0",
|
"jsdom": "26.1.0",
|
||||||
"nodemon": "3.1.10",
|
"nodemon": "3.1.10",
|
||||||
"punycode": "2.3.1",
|
"punycode": "2.3.1",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.1.0",
|
||||||
"react-native-web": "0.21.2",
|
"react-native-web": "0.21.2",
|
||||||
"react-refresh": "0.18.0",
|
"react-refresh": "0.18.0",
|
||||||
"react-test-renderer": "19.0.0",
|
"react-test-renderer": "19.1.0",
|
||||||
"sharp": "0.34.4",
|
"sharp": "0.34.4",
|
||||||
"sqlite3": "5.1.6",
|
"sqlite3": "5.1.6",
|
||||||
"timers-browserify": "2.0.12",
|
"timers-browserify": "2.0.12",
|
||||||
@@ -147,7 +149,7 @@
|
|||||||
"webpack-dev-server": "5.2.2"
|
"webpack-dev-server": "5.2.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
"expo": {
|
"expo": {
|
||||||
"autolinking": {
|
"autolinking": {
|
||||||
@@ -157,7 +159,6 @@
|
|||||||
},
|
},
|
||||||
"install": {
|
"install": {
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"react-native@~0.76.6",
|
|
||||||
"react-native-reanimated@~3.16.1",
|
"react-native-reanimated@~3.16.1",
|
||||||
"react-native-gesture-handler@~2.20.0",
|
"react-native-gesture-handler@~2.20.0",
|
||||||
"react-native-screens@~4.4.0",
|
"react-native-screens@~4.4.0",
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import shim from '@joplin/lib/shim';
|
|
||||||
import PerformanceLogger from '@joplin/lib/PerformanceLogger';
|
import PerformanceLogger from '@joplin/lib/PerformanceLogger';
|
||||||
|
|
||||||
shim.setReact(React);
|
|
||||||
PerformanceLogger.onAppStartBegin();
|
PerformanceLogger.onAppStartBegin();
|
||||||
|
|
||||||
import setupQuickActions from './setupQuickActions';
|
import setupQuickActions from './setupQuickActions';
|
||||||
@@ -695,12 +692,12 @@ class AppComponent extends React.Component<AppComponentProps, AppComponentState>
|
|||||||
let disableSideMenuGestures = this.props.disableSideMenuGestures;
|
let disableSideMenuGestures = this.props.disableSideMenuGestures;
|
||||||
|
|
||||||
if (this.props.routeName === 'Note') {
|
if (this.props.routeName === 'Note') {
|
||||||
sideMenuContent = <SafeAreaView style={{ flex: 1, backgroundColor: theme.backgroundColor }}><SideMenuContentNote options={this.props.noteSideMenuOptions}/></SafeAreaView>;
|
sideMenuContent = <SideMenuContentNote options={this.props.noteSideMenuOptions}/>;
|
||||||
menuPosition = SideMenuPosition.Right;
|
menuPosition = SideMenuPosition.Right;
|
||||||
} else if (this.props.routeName === 'Config') {
|
} else if (this.props.routeName === 'Config') {
|
||||||
disableSideMenuGestures = true;
|
disableSideMenuGestures = true;
|
||||||
} else {
|
} else {
|
||||||
sideMenuContent = <SafeAreaView style={{ flex: 1, backgroundColor: theme.backgroundColor }}><SideMenuContent/></SafeAreaView>;
|
sideMenuContent = <SideMenuContent/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const appNavInit = {
|
const appNavInit = {
|
||||||
|
|||||||
@@ -71,7 +71,10 @@ const crypto: Crypto = {
|
|||||||
|
|
||||||
digest: async (algorithm: Digest, data: Uint8Array) => {
|
digest: async (algorithm: Digest, data: Uint8Array) => {
|
||||||
const hash = QuickCrypto.createHash(digestNameMap[algorithm]);
|
const hash = QuickCrypto.createHash(digestNameMap[algorithm]);
|
||||||
hash.update(data);
|
hash.update(
|
||||||
|
// Cast: hash.update accepts TypedArrays, despite its declared types
|
||||||
|
data as unknown as ArrayBuffer,
|
||||||
|
);
|
||||||
return hash.digest();
|
return hash.digest();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -3,35 +3,29 @@ import whisper from './whisper';
|
|||||||
import { dirname, join } from 'path';
|
import { dirname, join } from 'path';
|
||||||
import { exists, mkdir, remove, writeFile } from 'fs-extra';
|
import { exists, mkdir, remove, writeFile } from 'fs-extra';
|
||||||
import Setting from '@joplin/lib/models/Setting';
|
import Setting from '@joplin/lib/models/Setting';
|
||||||
import { NativeModules } from 'react-native';
|
const { testing__lastPrompt } = require('@joplin/whisper-voice-typing');
|
||||||
const SpeechToTextModule = NativeModules.SpeechToTextModule;
|
|
||||||
|
|
||||||
jest.mock('react-native', () => {
|
|
||||||
const reactNative = jest.requireActual('react-native');
|
|
||||||
|
|
||||||
|
jest.mock('@joplin/whisper-voice-typing', () => {
|
||||||
let lastPrompt: string|null = null;
|
let lastPrompt: string|null = null;
|
||||||
|
|
||||||
// Set properties on reactNative rather than creating a new object with
|
|
||||||
// {...reactNative, ...}. Creating a new object triggers deprecation warnings.
|
|
||||||
// See https://github.com/facebook/react-native/issues/28839.
|
|
||||||
reactNative.NativeModules.SpeechToTextModule = {
|
|
||||||
convertNext: () => 'Test. This is test output. Test!',
|
|
||||||
runTests: ()=> {},
|
|
||||||
openSession: jest.fn((_path, _locale, prompt) => {
|
|
||||||
lastPrompt = prompt;
|
|
||||||
|
|
||||||
const someId = 1234;
|
return {
|
||||||
return someId;
|
runTests: ()=> {},
|
||||||
|
openSession: jest.fn((options) => {
|
||||||
|
lastPrompt = options.prompt;
|
||||||
|
|
||||||
|
return {
|
||||||
|
open: jest.fn(),
|
||||||
|
close: jest.fn(),
|
||||||
|
convertNext: jest.fn(() => 'Test. This is test output. Test!'),
|
||||||
|
convertAvailable: jest.fn(() => ''),
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
closeSession: jest.fn(),
|
test: ()=>{},
|
||||||
startRecording: jest.fn(),
|
|
||||||
convertAvailable: jest.fn(() => ''),
|
|
||||||
testing__lastPrompt: () => {
|
testing__lastPrompt: () => {
|
||||||
return lastPrompt;
|
return lastPrompt;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return reactNative;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
interface ModelConfig {
|
interface ModelConfig {
|
||||||
@@ -135,6 +129,6 @@ describe('whisper', () => {
|
|||||||
});
|
});
|
||||||
await session.start();
|
await session.start();
|
||||||
|
|
||||||
expect(SpeechToTextModule.testing__lastPrompt()).toBe(expectedPrompt);
|
expect(testing__lastPrompt()).toBe(expectedPrompt);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,14 +3,13 @@ import shim from '@joplin/lib/shim';
|
|||||||
import Logger from '@joplin/utils/Logger';
|
import Logger from '@joplin/utils/Logger';
|
||||||
import { rtrimSlashes } from '@joplin/utils/path';
|
import { rtrimSlashes } from '@joplin/utils/path';
|
||||||
import { dirname, join } from 'path';
|
import { dirname, join } from 'path';
|
||||||
import { NativeModules } from 'react-native';
|
import { openSession, test as testWhisper, Session as WhisperSession } from '@joplin/whisper-voice-typing';
|
||||||
import { SpeechToTextCallbacks, VoiceTypingProvider, VoiceTypingSession } from './VoiceTyping';
|
import { SpeechToTextCallbacks, VoiceTypingProvider, VoiceTypingSession } from './VoiceTyping';
|
||||||
import { languageCodeOnly, stringByLocale } from '@joplin/lib/locale';
|
import { languageCodeOnly, stringByLocale } from '@joplin/lib/locale';
|
||||||
|
import { Platform } from 'react-native';
|
||||||
|
|
||||||
const logger = Logger.create('voiceTyping/whisper');
|
const logger = Logger.create('voiceTyping/whisper');
|
||||||
|
|
||||||
const { SpeechToTextModule } = NativeModules;
|
|
||||||
|
|
||||||
class WhisperConfig {
|
class WhisperConfig {
|
||||||
public prompts: Map<string, string> = new Map();
|
public prompts: Map<string, string> = new Map();
|
||||||
public supportsShortAudioCtx = false;
|
public supportsShortAudioCtx = false;
|
||||||
@@ -86,7 +85,7 @@ class Whisper implements VoiceTypingSession {
|
|||||||
private isFirstParagraph = true;
|
private isFirstParagraph = true;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private sessionId: number|null,
|
private session: WhisperSession|null,
|
||||||
private callbacks: SpeechToTextCallbacks,
|
private callbacks: SpeechToTextCallbacks,
|
||||||
private config: WhisperConfig,
|
private config: WhisperConfig,
|
||||||
) {
|
) {
|
||||||
@@ -124,18 +123,18 @@ class Whisper implements VoiceTypingSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async start() {
|
public async start() {
|
||||||
if (this.sessionId === null) {
|
if (this.session === null) {
|
||||||
throw new Error('Session closed.');
|
throw new Error('Session closed.');
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
logger.debug('starting recorder');
|
logger.debug('starting recorder');
|
||||||
await SpeechToTextModule.startRecording(this.sessionId);
|
await this.session.open();
|
||||||
logger.debug('recorder started');
|
logger.debug('recorder started');
|
||||||
|
|
||||||
const loopStartCounter = this.closeCounter;
|
const loopStartCounter = this.closeCounter;
|
||||||
while (this.closeCounter === loopStartCounter && this.sessionId !== null) {
|
while (this.closeCounter === loopStartCounter && this.session !== null) {
|
||||||
logger.debug('reading block');
|
logger.debug('reading block');
|
||||||
const data: string = await SpeechToTextModule.convertNext(this.sessionId, 4);
|
const data: string = await this.session.convertNext(4);
|
||||||
this.onDataFinalize(data);
|
this.onDataFinalize(data);
|
||||||
|
|
||||||
logger.debug('done reading block. Length', data?.length);
|
logger.debug('done reading block. Length', data?.length);
|
||||||
@@ -148,13 +147,13 @@ class Whisper implements VoiceTypingSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async stop() {
|
public async stop() {
|
||||||
if (this.sessionId === null) {
|
if (this.session === null) {
|
||||||
logger.debug('Session already closed.');
|
logger.debug('Session already closed.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data: string = await SpeechToTextModule.convertAvailable(this.sessionId);
|
const data: string = await this.session.convertNext(null);
|
||||||
this.onDataFinalize(data);
|
this.onDataFinalize(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error stopping session: ', error);
|
logger.error('Error stopping session: ', error);
|
||||||
@@ -164,17 +163,17 @@ class Whisper implements VoiceTypingSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public cancel() {
|
public cancel() {
|
||||||
if (this.sessionId === null) {
|
if (this.session === null) {
|
||||||
logger.debug('No session to cancel.');
|
logger.debug('No session to cancel.');
|
||||||
return;
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('Closing session...');
|
logger.info('Closing session...');
|
||||||
const sessionId = this.sessionId;
|
this.session.close();
|
||||||
this.sessionId = null;
|
this.session = null;
|
||||||
this.closeCounter ++;
|
this.closeCounter ++;
|
||||||
|
|
||||||
return SpeechToTextModule.closeSession(sessionId);
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,7 +212,7 @@ const modelLocalFilepath = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const whisper: VoiceTypingProvider = {
|
const whisper: VoiceTypingProvider = {
|
||||||
supported: () => !!SpeechToTextModule && Setting.value('buildFlag.voiceTypingEnabled'),
|
supported: () => Platform.OS === 'android' && Setting.value('buildFlag.voiceTypingEnabled'),
|
||||||
modelLocalFilepath: modelLocalFilepath,
|
modelLocalFilepath: modelLocalFilepath,
|
||||||
getDownloadUrl: (locale) => {
|
getDownloadUrl: (locale) => {
|
||||||
const lang = languageCodeOnly(locale).toLowerCase();
|
const lang = languageCodeOnly(locale).toLowerCase();
|
||||||
@@ -251,7 +250,7 @@ const whisper: VoiceTypingProvider = {
|
|||||||
|
|
||||||
if (Setting.value('env') === Env.Dev) {
|
if (Setting.value('env') === Env.Dev) {
|
||||||
try {
|
try {
|
||||||
await SpeechToTextModule.runTests();
|
await testWhisper();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Testing error', error);
|
logger.error('Testing error', error);
|
||||||
await shim.showErrorDialog(`Test failure: ${error}`);
|
await shim.showErrorDialog(`Test failure: ${error}`);
|
||||||
@@ -268,10 +267,10 @@ const whisper: VoiceTypingProvider = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.debug('Starting whisper session', config.supportsShortAudioCtx ? '(short audio context)' : '');
|
logger.debug('Starting whisper session', config.supportsShortAudioCtx ? '(short audio context)' : '');
|
||||||
const sessionId = await SpeechToTextModule.openSession(
|
const session = openSession({
|
||||||
modelPath, locale, getPrompt(locale, config.prompts), config.supportsShortAudioCtx,
|
modelPath, locale, prompt: getPrompt(locale, config.prompts), shortAudioContext: config.supportsShortAudioCtx,
|
||||||
);
|
});
|
||||||
return new Whisper(sessionId, callbacks, config);
|
return new Whisper(session, callbacks, config);
|
||||||
},
|
},
|
||||||
modelName: 'whisper',
|
modelName: 'whisper',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import FsDriverBase, { ReadDirStatsOptions } from '@joplin/lib/fs-driver-base';
|
import FsDriverBase, { ReadDirStatsOptions } from '@joplin/lib/fs-driver-base';
|
||||||
const RNFetchBlob = require('rn-fetch-blob').default;
|
const RNFetchBlob = require('rn-fetch-blob').default;
|
||||||
import * as RNFS from 'react-native-fs';
|
import * as RNFS from '@dr.pogodin/react-native-fs';
|
||||||
import RNSAF, { DocumentFileDetail, openDocumentTree } from '@joplin/react-native-saf-x';
|
import RNSAF, { DocumentFileDetail, openDocumentTree } from '@joplin/react-native-saf-x';
|
||||||
import { Platform } from 'react-native';
|
import { Platform } from 'react-native';
|
||||||
import tarCreate from './tarCreate';
|
import tarCreate from './tarCreate';
|
||||||
|
|||||||
@@ -310,7 +310,7 @@ export class WorkerApi {
|
|||||||
at = writer.getSize();
|
at = writer.getSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
write = (data: ArrayBufferLike) => writer.write(data, { at });
|
write = (data: BufferSource) => writer.write(data, { at });
|
||||||
close = () => writer.close();
|
close = () => writer.close();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// In some cases, createSyncAccessHandle isn't available. In other cases,
|
// In some cases, createSyncAccessHandle isn't available. In other cases,
|
||||||
@@ -318,7 +318,7 @@ export class WorkerApi {
|
|||||||
|
|
||||||
logger.warn('Failed to createSyncAccessHandle', error);
|
logger.warn('Failed to createSyncAccessHandle', error);
|
||||||
const writer = await handle.createWritable({ keepExistingData: options?.keepExistingData });
|
const writer = await handle.createWritable({ keepExistingData: options?.keepExistingData });
|
||||||
write = (data: ArrayBufferLike) => writer.write(data);
|
write = (data: BufferSource) => writer.write(data);
|
||||||
close = () => writer.close();
|
close = () => writer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { Size } from '@joplin/utils/types';
|
|
||||||
import { fileUriToPath } from '@joplin/utils/url';
|
import { fileUriToPath } from '@joplin/utils/url';
|
||||||
import { Image as NativeImage, Platform } from 'react-native';
|
import { Image as NativeImage, Platform } from 'react-native';
|
||||||
import fileToImage from './fileToImage.web';
|
import fileToImage from './fileToImage.web';
|
||||||
|
|
||||||
|
interface ImageDimensions {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
const getImageDimensions = async (uri: string): Promise<Size> => {
|
const getImageDimensions = async (uri: string): Promise<ImageDimensions> => {
|
||||||
if (uri.startsWith('/')) {
|
if (uri.startsWith('/')) {
|
||||||
uri = `file://${uri}`;
|
uri = `file://${uri}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import shim from '@joplin/lib/shim';
|
import shim from '@joplin/lib/shim';
|
||||||
import Logger from '@joplin/utils/Logger';
|
import Logger from '@joplin/utils/Logger';
|
||||||
import ImageResizer from '@bam.tech/react-native-image-resizer';
|
import { ImageManipulator, SaveFormat } from 'expo-image-manipulator';
|
||||||
import fileToImage from './fileToImage.web';
|
import fileToImage from './fileToImage.web';
|
||||||
import FsDriverWeb from '../fs-driver/fs-driver-rn.web';
|
import FsDriverWeb from '../fs-driver/fs-driver-rn.web';
|
||||||
|
import getImageDimensions from './getImageDimensions';
|
||||||
|
|
||||||
const logger = Logger.create('resizeImage');
|
const logger = Logger.create('resizeImage');
|
||||||
|
|
||||||
@@ -18,17 +19,23 @@ interface Options {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const resizeImage = async (options: Options) => {
|
const resizeImage = async (options: Options) => {
|
||||||
|
type Sized = { width: number; height: number };
|
||||||
|
const computeScale = (image: Sized) => {
|
||||||
|
// Choose a scale factor such that the resized image fits within a
|
||||||
|
// maxWidth x maxHeight box.
|
||||||
|
const scale = Math.min(
|
||||||
|
options.maxWidth / image.width,
|
||||||
|
options.maxHeight / image.height,
|
||||||
|
);
|
||||||
|
return scale;
|
||||||
|
};
|
||||||
|
|
||||||
if (shim.mobilePlatform() === 'web') {
|
if (shim.mobilePlatform() === 'web') {
|
||||||
const image = await fileToImage(options.inputPath);
|
const image = await fileToImage(options.inputPath);
|
||||||
try {
|
try {
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
|
|
||||||
// Choose a scale factor such that the resized image fits within a
|
const scale = computeScale(image.image);
|
||||||
// maxWidth x maxHeight box.
|
|
||||||
const scale = Math.min(
|
|
||||||
options.maxWidth / image.image.width,
|
|
||||||
options.maxHeight / image.image.height,
|
|
||||||
);
|
|
||||||
canvas.width = image.image.width * scale;
|
canvas.width = image.image.width * scale;
|
||||||
canvas.height = image.image.height * scale;
|
canvas.height = image.image.height * scale;
|
||||||
|
|
||||||
@@ -54,18 +61,27 @@ const resizeImage = async (options: Options) => {
|
|||||||
image.free();
|
image.free();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const resizedImage = await ImageResizer.createResizedImage(
|
const originalSize = await getImageDimensions(options.inputPath);
|
||||||
options.inputPath,
|
logger.debug('Processing image with size', originalSize.width, 'x', originalSize.height);
|
||||||
options.maxWidth,
|
|
||||||
options.maxHeight,
|
|
||||||
options.format,
|
|
||||||
options.quality, // quality
|
|
||||||
undefined, // rotation
|
|
||||||
undefined, // outputPath
|
|
||||||
true, // keep metadata
|
|
||||||
);
|
|
||||||
|
|
||||||
const resizedImagePath = resizedImage.uri;
|
let context = ImageManipulator.manipulate(options.inputPath);
|
||||||
|
|
||||||
|
// Only rescale the image if it's bigger than the maximum size:
|
||||||
|
if (originalSize.width > options.maxWidth || originalSize.height > options.maxHeight) {
|
||||||
|
const scale = computeScale(originalSize);
|
||||||
|
context = context.resize({
|
||||||
|
width: originalSize.width * scale,
|
||||||
|
height: originalSize.height * scale,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const final = await context.renderAsync();
|
||||||
|
const saved = await final.saveAsync({
|
||||||
|
format: options.format === 'PNG' ? SaveFormat.PNG : SaveFormat.JPEG,
|
||||||
|
compress: options.quality,
|
||||||
|
});
|
||||||
|
|
||||||
|
const resizedImagePath = saved.uri;
|
||||||
logger.info('Resized image ', resizedImagePath);
|
logger.info('Resized image ', resizedImagePath);
|
||||||
logger.info(`Moving ${resizedImagePath} => ${options.outputPath}`);
|
logger.info(`Moving ${resizedImagePath} => ${options.outputPath}`);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import shim from '@joplin/lib/shim';
|
||||||
|
|
||||||
|
// .setReact needs to be called very early in the application startup process.
|
||||||
|
// This file can be imported to ensure that .setReact has been called prior to
|
||||||
|
// all other import/requires.
|
||||||
|
const React = require('react');
|
||||||
|
shim.setReact(React);
|
||||||
@@ -7,7 +7,7 @@ import { generateSecureRandom } from 'react-native-securerandom';
|
|||||||
import FsDriverRN from '../fs-driver/fs-driver-rn';
|
import FsDriverRN from '../fs-driver/fs-driver-rn';
|
||||||
import { Linking, Platform } from 'react-native';
|
import { Linking, Platform } from 'react-native';
|
||||||
import crypto from '../../services/e2ee/crypto';
|
import crypto from '../../services/e2ee/crypto';
|
||||||
const RNExitApp = require('react-native-exit-app').default;
|
import { reloadAppAsync } from 'expo';
|
||||||
|
|
||||||
export default function shimInit() {
|
export default function shimInit() {
|
||||||
shim.Geolocation = GeolocationReact;
|
shim.Geolocation = GeolocationReact;
|
||||||
@@ -178,7 +178,7 @@ export default function shimInit() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
shim.restartApp = () => {
|
shim.restartApp = () => {
|
||||||
RNExitApp.exitApp();
|
void reloadAppAsync();
|
||||||
};
|
};
|
||||||
|
|
||||||
shimInitShared();
|
shimInitShared();
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ const buildSharedConfig = (hotReload: boolean): webpack.Configuration => {
|
|||||||
'@react-native-documents/picker': emptyLibraryMock,
|
'@react-native-documents/picker': emptyLibraryMock,
|
||||||
'react-native-exit-app': emptyLibraryMock,
|
'react-native-exit-app': emptyLibraryMock,
|
||||||
'expo-camera': emptyLibraryMock,
|
'expo-camera': emptyLibraryMock,
|
||||||
|
'react-native-nitro-modules': emptyLibraryMock,
|
||||||
'react-native-vector-icons/MaterialCommunityIcons': throwOnLoadLibraryMock,
|
'react-native-vector-icons/MaterialCommunityIcons': throwOnLoadLibraryMock,
|
||||||
|
|
||||||
// Workaround for applying serviceworker types to a single file.
|
// Workaround for applying serviceworker types to a single file.
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ function shimInit(options: ShimInitOptions = null) {
|
|||||||
if (shim.isElectron()) {
|
if (shim.isElectron()) {
|
||||||
return shim.electronBridge().showMessageBox(message, options ?? {});
|
return shim.electronBridge().showMessageBox(message, options ?? {});
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Not implemented');
|
throw new Error(`Not implemented: showMessageBox(${JSON.stringify(message)})`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -46,8 +46,8 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.16.0",
|
"@babel/core": "7.16.0",
|
||||||
"@types/react-native": "0.64.19",
|
"@types/react-native": "0.64.19",
|
||||||
"react": "18.3.1",
|
"react": "19.1.0",
|
||||||
"react-native": "0.70.6",
|
"react-native": "0.81.5",
|
||||||
"typescript": "5.8.3"
|
"typescript": "5.8.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|||||||
@@ -241,6 +241,7 @@ xhdpi
|
|||||||
xxhdpi
|
xxhdpi
|
||||||
xxxhdpi
|
xxxhdpi
|
||||||
scrollend
|
scrollend
|
||||||
|
pogodin
|
||||||
youtube
|
youtube
|
||||||
youtu
|
youtu
|
||||||
nocookie
|
nocookie
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ async function main() {
|
|||||||
await updatePackageVersion(`${rootDir}/packages/default-plugins/package.json`, majorMinorVersion, options);
|
await updatePackageVersion(`${rootDir}/packages/default-plugins/package.json`, majorMinorVersion, options);
|
||||||
await updatePackageVersion(`${rootDir}/packages/editor/package.json`, majorMinorVersion, options);
|
await updatePackageVersion(`${rootDir}/packages/editor/package.json`, majorMinorVersion, options);
|
||||||
await updatePackageVersion(`${rootDir}/packages/transcribe/package.json`, majorMinorVersion, options);
|
await updatePackageVersion(`${rootDir}/packages/transcribe/package.json`, majorMinorVersion, options);
|
||||||
|
await updatePackageVersion(`${rootDir}/packages/whisper-voice-typing/package.json`, majorMinorVersion, options);
|
||||||
|
|
||||||
if (options.updateVersion) {
|
if (options.updateVersion) {
|
||||||
await updateGradleVersion(`${rootDir}/packages/app-mobile/android/app/build.gradle`, majorMinorVersion);
|
await updateGradleVersion(`${rootDir}/packages/app-mobile/android/app/build.gradle`, majorMinorVersion);
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# OSX
|
||||||
|
#
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# XDE
|
||||||
|
.expo/
|
||||||
|
|
||||||
|
# VSCode
|
||||||
|
.vscode/
|
||||||
|
jsconfig.json
|
||||||
|
|
||||||
|
# Xcode
|
||||||
|
#
|
||||||
|
build/
|
||||||
|
*.pbxuser
|
||||||
|
!default.pbxuser
|
||||||
|
*.mode1v3
|
||||||
|
!default.mode1v3
|
||||||
|
*.mode2v3
|
||||||
|
!default.mode2v3
|
||||||
|
*.perspectivev3
|
||||||
|
!default.perspectivev3
|
||||||
|
xcuserdata
|
||||||
|
*.xccheckout
|
||||||
|
*.moved-aside
|
||||||
|
DerivedData
|
||||||
|
*.hmap
|
||||||
|
*.ipa
|
||||||
|
*.xcuserstate
|
||||||
|
project.xcworkspace
|
||||||
|
|
||||||
|
# Android/IJ
|
||||||
|
#
|
||||||
|
.classpath
|
||||||
|
.cxx
|
||||||
|
.gradle
|
||||||
|
.idea
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
local.properties
|
||||||
|
android.iml
|
||||||
|
|
||||||
|
# Cocoapods
|
||||||
|
#
|
||||||
|
example/ios/Pods
|
||||||
|
|
||||||
|
# Ruby
|
||||||
|
example/vendor/
|
||||||
|
|
||||||
|
# node.js
|
||||||
|
#
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log
|
||||||
|
yarn-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
# BUCK
|
||||||
|
buck-out/
|
||||||
|
\.buckd/
|
||||||
|
android/app/libs
|
||||||
|
android/keystores/debug.keystore
|
||||||
|
|
||||||
|
# Yarn
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/sdks
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# Expo
|
||||||
|
.expo/
|
||||||
|
|
||||||
|
# Turborepo
|
||||||
|
.turbo/
|
||||||
|
|
||||||
|
# generated
|
||||||
|
lib/
|
||||||
|
nitrogen/
|
||||||
|
|
||||||
|
# caches
|
||||||
|
.eslintcache
|
||||||
|
.cache
|
||||||
|
*.tsbuildinfo
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# react-native-whisper-voice-typing
|
||||||
|
|
||||||
|
Wraps `whisper.cpp` for use in React Native.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
This package uses [react-native-nitro-modules](https://nitro.margelo.com/docs/what-is-nitro) and was scaffolded using [`nitrogen`](https://nitro.margelo.com/docs/how-to-build-a-nitro-module#1-create-a-nitro-module).
|
||||||
|
|
||||||
|
### Adding a new native hybrid object
|
||||||
|
|
||||||
|
Process summary:
|
||||||
|
1. Add a TypeScript type declaration to `src/specs/Whisper.nitro.ts`.
|
||||||
|
2. (Optional) Update `nitro.json`'s autolinking section.
|
||||||
|
3. Run `yarn build`. This regenerates the `nitrogen/generated` folder.
|
||||||
|
4. Implement the new interface(s) in `android/src/main/java/` or `cpp/`.
|
||||||
|
|
||||||
|
See [the documentation](https://nitro.margelo.com/docs/nitro-modules) for full instructions.
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
require "json"
|
||||||
|
|
||||||
|
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
||||||
|
|
||||||
|
# Currently disabled on iOS.
|
||||||
|
# To enable,
|
||||||
|
# 1. Uncomment the code after "DISABLED:"
|
||||||
|
# 2. Run vencor/whisper.cpp/build-xcframework.sh
|
||||||
|
# 3. Add vendor/whisper.cpp as a dependency?
|
||||||
|
# 4. Implement cpp/IAudioRecorder.hpp for iOS.
|
||||||
|
|
||||||
|
|
||||||
|
Pod::Spec.new do |s|
|
||||||
|
s.name = "WhisperVoiceTyping"
|
||||||
|
s.version = package["version"]
|
||||||
|
s.summary = package["description"]
|
||||||
|
s.homepage = package["homepage"]
|
||||||
|
s.license = package["license"]
|
||||||
|
s.authors = package["author"]
|
||||||
|
|
||||||
|
s.platforms = { :ios => min_ios_version_supported, :visionos => 1.0 }
|
||||||
|
s.source = { :git => "https://github.com/mrousavy/nitro.git", :tag => "#{s.version}" }
|
||||||
|
|
||||||
|
s.source_files = [
|
||||||
|
# Implementation (Swift)
|
||||||
|
"ios/**/*.{swift}",
|
||||||
|
# Autolinking/Registration (Objective-C++)
|
||||||
|
"ios/**/*.{m,mm}",
|
||||||
|
# Implementation (C++ objects)
|
||||||
|
# DISABLED: for now:
|
||||||
|
#"cpp/**/*.{hpp,cpp}",
|
||||||
|
]
|
||||||
|
|
||||||
|
# DISABLED: Currently, this package lacks iOS support
|
||||||
|
#load 'nitrogen/generated/ios/WhisperVoiceTyping+autolinking.rb'
|
||||||
|
#add_nitrogen_files(s)
|
||||||
|
|
||||||
|
s.dependency 'React-jsi'
|
||||||
|
s.dependency 'React-callinvoker'
|
||||||
|
install_modules_dependencies(s)
|
||||||
|
end
|
||||||
|
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.9.0)
|
||||||
|
project(WhisperVoiceTyping)
|
||||||
|
|
||||||
|
set(PACKAGE_NAME WhisperVoiceTyping)
|
||||||
|
set(CMAKE_VERBOSE_MAKEFILE ON)
|
||||||
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
|
|
||||||
|
# Define C++ library and add all sources
|
||||||
|
add_library(
|
||||||
|
${PACKAGE_NAME} SHARED
|
||||||
|
src/main/cpp/cpp-adapter.cpp
|
||||||
|
../cpp/utils/testing.cpp
|
||||||
|
../cpp/utils/findLongestSilence_test.cpp
|
||||||
|
../cpp/utils/findLongestSilence.cpp
|
||||||
|
../cpp/utils/WhisperSession.cpp
|
||||||
|
../cpp/utils/SingleThread.cpp
|
||||||
|
../cpp/utils/SingleThread_test.cpp
|
||||||
|
../cpp/HybridWhisperSession.cpp
|
||||||
|
../cpp/HybridWhisperVoiceTyping.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add Nitrogen specs :)
|
||||||
|
include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/WhisperVoiceTyping+autolinking.cmake)
|
||||||
|
|
||||||
|
# Set up local includes
|
||||||
|
include_directories("src/main/cpp" "../cpp" "../cpp/utils")
|
||||||
|
|
||||||
|
################################
|
||||||
|
# Whisper.cpp configuration #
|
||||||
|
################################
|
||||||
|
|
||||||
|
set(WHISPER_LIB_DIR ${CMAKE_SOURCE_DIR}/../vendor/whisper.cpp)
|
||||||
|
|
||||||
|
# Based on the Whisper.cpp Android example:
|
||||||
|
set(SHARED_FLAGS "-O3 ")
|
||||||
|
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${SHARED_FLAGS} ")
|
||||||
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${SHARED_FLAGS} -fvisibility=hidden -fvisibility-inlines-hidden -ffunction-sections -fdata-sections")
|
||||||
|
|
||||||
|
# Whisper: See https://stackoverflow.com/a/76290722
|
||||||
|
add_subdirectory(${WHISPER_LIB_DIR} ./whisper)
|
||||||
|
|
||||||
|
include_directories(${WHISPER_LIB_DIR}/include)
|
||||||
|
|
||||||
|
#################################
|
||||||
|
# END Whisper.cpp configuration #
|
||||||
|
#################################
|
||||||
|
|
||||||
|
find_library(LOG_LIB log)
|
||||||
|
|
||||||
|
# Link all libraries together
|
||||||
|
target_link_libraries(
|
||||||
|
${PACKAGE_NAME}
|
||||||
|
${LOG_LIB}
|
||||||
|
whisper
|
||||||
|
android # <-- Android core
|
||||||
|
)
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
// Note: Based on part of the [react-native-builder-bob](https://github.com/callstack/react-native-builder-bob)
|
||||||
|
// Nitro module template.
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
ext.WhisperVoiceTyping = [
|
||||||
|
kotlinVersion: "2.0.21",
|
||||||
|
minSdkVersion: 24,
|
||||||
|
compileSdkVersion: 36,
|
||||||
|
targetSdkVersion: 36
|
||||||
|
]
|
||||||
|
|
||||||
|
ext.getExtOrDefault = { prop ->
|
||||||
|
if (rootProject.ext.has(prop)) {
|
||||||
|
return rootProject.ext.get(prop)
|
||||||
|
}
|
||||||
|
|
||||||
|
return WhisperVoiceTyping[prop]
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath "com.android.tools.build:gradle:8.7.2"
|
||||||
|
// noinspection DifferentKotlinGradleVersion
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def reactNativeArchitectures() {
|
||||||
|
def value = rootProject.getProperties().get("reactNativeArchitectures")
|
||||||
|
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: "com.android.library"
|
||||||
|
apply plugin: "kotlin-android"
|
||||||
|
apply from: '../nitrogen/generated/android/WhisperVoiceTyping+autolinking.gradle'
|
||||||
|
|
||||||
|
// Disabled: Including the "com.facebook.react" plugin seems to cause "ActivityIndicatorViewManagerDelegate is defined multiple times"
|
||||||
|
// errors:
|
||||||
|
// apply plugin: "com.facebook.react"
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace "com.margelo.nitro.whispervoicetyping"
|
||||||
|
|
||||||
|
compileSdkVersion getExtOrDefault("compileSdkVersion")
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion getExtOrDefault("minSdkVersion")
|
||||||
|
targetSdkVersion getExtOrDefault("targetSdkVersion")
|
||||||
|
|
||||||
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
cppFlags "-frtti -fexceptions -Wall -fstack-protector-all"
|
||||||
|
arguments "-DANDROID_STL=c++_shared", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
|
||||||
|
abiFilters (*reactNativeArchitectures())
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
debug {
|
||||||
|
cppFlags "-O1 -g"
|
||||||
|
}
|
||||||
|
release {
|
||||||
|
cppFlags "-O2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
path "CMakeLists.txt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
packagingOptions {
|
||||||
|
excludes = [
|
||||||
|
"META-INF",
|
||||||
|
"META-INF/**",
|
||||||
|
"**/libc++_shared.so",
|
||||||
|
"**/libNitroModules.so",
|
||||||
|
"**/libfbjni.so",
|
||||||
|
"**/libjsi.so",
|
||||||
|
"**/libfolly_json.so",
|
||||||
|
"**/libfolly_runtime.so",
|
||||||
|
"**/libglog.so",
|
||||||
|
"**/libhermes.so",
|
||||||
|
"**/libhermes-executor-debug.so",
|
||||||
|
"**/libhermes_executor.so",
|
||||||
|
"**/libreactnative.so",
|
||||||
|
"**/libreactnativejni.so",
|
||||||
|
"**/libturbomodulejsijni.so",
|
||||||
|
"**/libreact_nativemodule_core.so",
|
||||||
|
"**/libjscexecutor.so"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
buildConfig true
|
||||||
|
prefab true
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lint {
|
||||||
|
disable "GradleCompatible"
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation "com.facebook.react:react-android"
|
||||||
|
implementation project(":react-native-nitro-modules")
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
WhisperVoiceTyping_kotlinVersion=2.1.20
|
||||||
|
WhisperVoiceTyping_minSdkVersion=23
|
||||||
|
WhisperVoiceTyping_targetSdkVersion=36
|
||||||
|
WhisperVoiceTyping_compileSdkVersion=36
|
||||||
|
WhisperVoiceTyping_ndkVersion=27.1.12297006
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#include <jni.h>
|
||||||
|
#include "WhisperVoiceTypingOnLoad.hpp"
|
||||||
|
|
||||||
|
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
|
||||||
|
return margelo::nitro::whispervoicetyping::initialize(vm);
|
||||||
|
}
|
||||||
+28
-34
@@ -1,4 +1,4 @@
|
|||||||
package net.cozic.joplin.audio
|
package com.margelo.nitro.whispervoicetyping
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
@@ -10,22 +10,20 @@ import android.media.MediaRecorder.AudioSource
|
|||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
import com.margelo.nitro.NitroModules
|
||||||
|
import java.nio.FloatBuffer
|
||||||
|
|
||||||
typealias AudioRecorderFactory = (context: Context)->AudioRecorder;
|
class AudioRecorder (context: Context) : Closeable {
|
||||||
|
|
||||||
class AudioRecorder(context: Context) : Closeable {
|
|
||||||
private val sampleRate = 16_000
|
private val sampleRate = 16_000
|
||||||
// Don't allow the unprocessed audio buffer to grow indefinitely -- discard
|
// Don't allow the unprocessed audio buffer to grow indefinitely -- discard
|
||||||
// data if longer than this:
|
// data if longer than this:
|
||||||
private val maxLengthSeconds = 120
|
private val maxLengthSeconds = 120
|
||||||
private val maxRecorderBufferLengthSeconds = 20
|
private val maxRecorderBufferLengthSeconds = 20
|
||||||
private val maxBufferSize = sampleRate * maxLengthSeconds
|
private val maxBufferSizeFloats = sampleRate * maxLengthSeconds
|
||||||
private val buffer = FloatArray(maxBufferSize)
|
private val buffer = FloatArray(maxBufferSizeFloats)
|
||||||
|
|
||||||
private var bufferWriteOffset = 0
|
private var bufferWriteOffset = 0
|
||||||
|
|
||||||
// Accessor must not modify result
|
|
||||||
private val bufferedData: FloatArray get() = buffer.sliceArray(0 until bufferWriteOffset)
|
|
||||||
val bufferLengthSeconds: Double get() = bufferWriteOffset.toDouble() / sampleRate
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val permissionResult = context.checkSelfPermission(Manifest.permission.RECORD_AUDIO)
|
val permissionResult = context.checkSelfPermission(Manifest.permission.RECORD_AUDIO)
|
||||||
@@ -46,65 +44,61 @@ class AudioRecorder(context: Context) : Closeable {
|
|||||||
.setChannelMask(AudioFormat.CHANNEL_IN_MONO)
|
.setChannelMask(AudioFormat.CHANNEL_IN_MONO)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
|
// Use a smaller internal buffer size in the recorder
|
||||||
.setBufferSizeInBytes(maxRecorderBufferLengthSeconds * sampleRate * Float.SIZE_BYTES)
|
.setBufferSizeInBytes(maxRecorderBufferLengthSeconds * sampleRate * Float.SIZE_BYTES)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
// Discards the first [samples] samples from the start of the buffer. Conceptually, this
|
// Discards the first [samples] samples from the start of the buffer. Conceptually, this
|
||||||
// advances the buffer's start point.
|
// advances the buffer's start point.
|
||||||
private fun advanceStartBySamples(samples: Int) {
|
private fun advanceStartBySamples(samples: Int) {
|
||||||
val samplesClamped = min(samples, maxBufferSize)
|
val samplesClamped = min(samples, maxBufferSizeFloats)
|
||||||
val remainingBuffer = buffer.sliceArray(samplesClamped until maxBufferSize)
|
val remainingBuffer = buffer.sliceArray(samplesClamped until maxBufferSizeFloats)
|
||||||
|
|
||||||
buffer.fill(0f, samplesClamped, maxBufferSize)
|
buffer.fill(0f, samplesClamped, maxBufferSizeFloats)
|
||||||
remainingBuffer.copyInto(buffer, 0)
|
remainingBuffer.copyInto(buffer, 0)
|
||||||
bufferWriteOffset = max(bufferWriteOffset - samplesClamped, 0)
|
bufferWriteOffset = max(bufferWriteOffset - samplesClamped, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dropFirstSeconds(seconds: Double) {
|
|
||||||
advanceStartBySamples((seconds * sampleRate).toInt())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
recorder.startRecording()
|
recorder.startRecording()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
private fun read(requestedSize: Int, mode: Int) {
|
private fun read(requestedSize: Int, mode: Int) {
|
||||||
val size = min(requestedSize, maxBufferSize - bufferWriteOffset)
|
val size = min(requestedSize, maxBufferSizeFloats - bufferWriteOffset)
|
||||||
val sizeRead = recorder.read(buffer, bufferWriteOffset, size, mode)
|
val sizeRead = recorder.read(buffer, bufferWriteOffset, size, mode)
|
||||||
if (sizeRead > 0) {
|
if (sizeRead > 0) {
|
||||||
bufferWriteOffset += sizeRead
|
bufferWriteOffset += sizeRead
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pulls all available data from the audio recorder's buffer
|
// Returns a pointer to the buffered data
|
||||||
fun pullAvailable(): FloatArray {
|
fun pullAvailable(): FloatArray {
|
||||||
read(maxBufferSize, AudioRecord.READ_NON_BLOCKING)
|
read(maxBufferSizeFloats, AudioRecord.READ_NON_BLOCKING)
|
||||||
|
return buffer.sliceArray(0 until bufferWriteOffset)
|
||||||
val result = bufferedData
|
|
||||||
buffer.fill(0.0f, 0, maxBufferSize);
|
|
||||||
bufferWriteOffset = 0
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pullNextSeconds(seconds: Double):FloatArray {
|
// Sets the buffer write offset back to zero.
|
||||||
val remainingSize = maxBufferSize - bufferWriteOffset
|
fun resetBuffer() {
|
||||||
|
bufferWriteOffset = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pushes at least [seconds] seconds of new data to the recording buffer.
|
||||||
|
// Call "pullAvailable" to read the buffered data.
|
||||||
|
fun bufferAdditionalData(seconds: Double) {
|
||||||
|
val remainingSize = maxBufferSizeFloats - bufferWriteOffset
|
||||||
val requestedSize = (seconds * sampleRate).toInt()
|
val requestedSize = (seconds * sampleRate).toInt()
|
||||||
|
|
||||||
// If low on size, make more room.
|
// If low on size, make more room.
|
||||||
if (remainingSize < maxBufferSize / 3) {
|
if (remainingSize < maxBufferSizeFloats / 3) {
|
||||||
advanceStartBySamples(maxBufferSize / 3)
|
advanceStartBySamples(maxBufferSizeFloats / 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
read(requestedSize, AudioRecord.READ_BLOCKING)
|
read(requestedSize, AudioRecord.READ_BLOCKING)
|
||||||
return pullAvailable()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
recorder.stop()
|
recorder.stop()
|
||||||
recorder.release()
|
recorder.release()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
companion object {
|
|
||||||
val factory: AudioRecorderFactory = { context -> AudioRecorder(context) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
package com.margelo.nitro.whispervoicetyping
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.margelo.nitro.core.ArrayBuffer
|
||||||
|
import com.margelo.nitro.core.Promise
|
||||||
|
import com.margelo.nitro.NitroModules
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.ByteOrder
|
||||||
|
|
||||||
|
// Wraps an AudioRecorder in an interface that can be accessed from C++
|
||||||
|
class HybridAudioRecorder : HybridAudioRecorderSpec() {
|
||||||
|
private val recorder = AudioRecorder(NitroModules.applicationContext!!)
|
||||||
|
|
||||||
|
override fun start() {
|
||||||
|
recorder.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stop() {
|
||||||
|
recorder.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun waitForData(seconds: Double): Promise<Unit> {
|
||||||
|
return Promise.async {
|
||||||
|
recorder.bufferAdditionalData(seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pullAvailable(): ArrayBuffer {
|
||||||
|
val floatData = recorder.pullAvailable()
|
||||||
|
val bytes = convertToBytes(floatData)
|
||||||
|
|
||||||
|
val buffer = ArrayBuffer.copy(bytes)
|
||||||
|
recorder.resetBuffer()
|
||||||
|
return buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun convertToBytes(floatData: FloatArray): ByteArray {
|
||||||
|
// Convert to a ByteBuffer first. (Similar approach to https://stackoverflow.com/q/11385596)
|
||||||
|
val output = ByteBuffer.allocate(floatData.size * Float.SIZE_BYTES)
|
||||||
|
output.order(ByteOrder.nativeOrder())
|
||||||
|
output.asFloatBuffer().put(floatData)
|
||||||
|
return output.array()
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
package com.margelo.nitro.whispervoicetyping
|
||||||
|
|
||||||
|
import com.facebook.react.BaseReactPackage
|
||||||
|
import com.facebook.react.bridge.NativeModule
|
||||||
|
import com.facebook.react.bridge.ReactApplicationContext
|
||||||
|
import com.facebook.react.module.model.ReactModuleInfoProvider
|
||||||
|
|
||||||
|
class WhisperVoiceTypingPackage : BaseReactPackage() {
|
||||||
|
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
|
||||||
|
return ReactModuleInfoProvider { HashMap() }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
init {
|
||||||
|
System.loadLibrary("WhisperVoiceTyping")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: ['module:@react-native/babel-preset'],
|
||||||
|
};
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#include "HybridWhisperSession.hpp"
|
||||||
|
#include "androidUtil.h"
|
||||||
|
#include <NitroModules/ArrayBuffer.hpp>
|
||||||
|
|
||||||
|
using namespace margelo::nitro::whispervoicetyping;
|
||||||
|
|
||||||
|
struct HybridWhisperSession::State_ {
|
||||||
|
State_(const SessionOptions& options)
|
||||||
|
: session_(options.modelPath, options.locale, options.prompt, options.shortAudioContext)
|
||||||
|
{};
|
||||||
|
|
||||||
|
void addAudio(ArrayBuffer& data);
|
||||||
|
|
||||||
|
std::mutex mutex_;
|
||||||
|
WhisperSession session_;
|
||||||
|
};
|
||||||
|
|
||||||
|
void HybridWhisperSession::State_::addAudio(ArrayBuffer& data) {
|
||||||
|
float* dataFloat = reinterpret_cast<float*>(data.data());
|
||||||
|
size_t sizeFloats = data.size() / sizeof(float);
|
||||||
|
|
||||||
|
if (dataFloat == nullptr) {
|
||||||
|
throw std::logic_error("Attempting to add audio from a source that has already been deleted.");
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGD("Add audio (size %zu)", sizeFloats);
|
||||||
|
session_.addAudio(dataFloat, sizeFloats);
|
||||||
|
}
|
||||||
|
|
||||||
|
HybridWhisperSession::HybridWhisperSession(
|
||||||
|
const SessionOptions& options
|
||||||
|
)
|
||||||
|
: HybridObject(TAG),
|
||||||
|
state_(std::make_shared<HybridWhisperSession::State_>(options))
|
||||||
|
{ }
|
||||||
|
|
||||||
|
void HybridWhisperSession::pushAudio(const std::shared_ptr<ArrayBuffer>& audio) {
|
||||||
|
std::lock_guard<std::mutex> lock { state_->mutex_ };
|
||||||
|
|
||||||
|
state_->addAudio(*audio);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<Promise<std::string>> HybridWhisperSession::convertNext() {
|
||||||
|
return Promise<std::string>::async([state = state_] () -> std::string {
|
||||||
|
std::lock_guard<std::mutex> lock { state->mutex_ };
|
||||||
|
return state->session_.transcribeNextChunk();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "HybridWhisperSessionSpec.hpp"
|
||||||
|
#include "SessionOptions.hpp"
|
||||||
|
#include "utils/WhisperSession.hpp"
|
||||||
|
|
||||||
|
namespace margelo::nitro::whispervoicetyping {
|
||||||
|
class HybridWhisperSession : public HybridWhisperSessionSpec {
|
||||||
|
public:
|
||||||
|
explicit HybridWhisperSession(const SessionOptions& options);
|
||||||
|
|
||||||
|
void pushAudio(const std::shared_ptr<ArrayBuffer>& audio) override;
|
||||||
|
std::shared_ptr<Promise<std::string>> convertNext() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct State_;
|
||||||
|
std::shared_ptr<State_> state_;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "HybridWhisperVoiceTyping.hpp"
|
||||||
|
#include "HybridWhisperSession.hpp"
|
||||||
|
#include "findLongestSilence_test.hpp"
|
||||||
|
#include "SingleThread_test.hpp"
|
||||||
|
|
||||||
|
|
||||||
|
using namespace margelo::nitro::whispervoicetyping;
|
||||||
|
|
||||||
|
SessionPointer HybridWhisperVoiceTyping::openSession(const SessionOptions& options) {
|
||||||
|
return std::make_shared<HybridWhisperSession> (options);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<Promise<void>> HybridWhisperVoiceTyping::test() {
|
||||||
|
return Promise<void>::async([=] () -> void {
|
||||||
|
findLongestSilence_test();
|
||||||
|
SingleThread_test();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "HybridWhisperVoiceTypingSpec.hpp"
|
||||||
|
|
||||||
|
namespace margelo::nitro::whispervoicetyping {
|
||||||
|
using SessionPointer = std::shared_ptr<HybridWhisperSessionSpec>;
|
||||||
|
|
||||||
|
class HybridWhisperVoiceTyping : public HybridWhisperVoiceTypingSpec {
|
||||||
|
public:
|
||||||
|
HybridWhisperVoiceTyping(): HybridObject(TAG) {};
|
||||||
|
|
||||||
|
SessionPointer openSession(const SessionOptions& options) override;
|
||||||
|
|
||||||
|
// Runs tests
|
||||||
|
std::shared_ptr<Promise<void>> test() override;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
#include "SingleThread.hpp"
|
||||||
|
#include "androidUtil.h"
|
||||||
|
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <thread>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
struct SingleThread::State_ {
|
||||||
|
std::vector<std::function<void()>> tasks;
|
||||||
|
|
||||||
|
// mutex_ and cv_ are used as a signaling mechanism.
|
||||||
|
// See, for example https://cplusplus.com/reference/condition_variable/condition_variable/
|
||||||
|
// https://en.cppreference.com/w/cpp/thread/condition_variable.html
|
||||||
|
std::condition_variable cv;
|
||||||
|
std::mutex mutex;
|
||||||
|
|
||||||
|
bool cancelled = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
void threadLoop(std::shared_ptr<SingleThread::State_> state) {
|
||||||
|
while (!state->cancelled) {
|
||||||
|
// Create a lock that can be used by state->cv (see https://stackoverflow.com/a/13102893
|
||||||
|
// for why).
|
||||||
|
std::unique_lock lock {state->mutex};
|
||||||
|
|
||||||
|
// Run all tasks **first**, to handle the case where the threadLoop starts after the first
|
||||||
|
// task has been posted.
|
||||||
|
for (auto& task : state->tasks) {
|
||||||
|
task();
|
||||||
|
}
|
||||||
|
state->tasks.clear();
|
||||||
|
|
||||||
|
state->cv.wait(lock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SingleThread::SingleThread()
|
||||||
|
: state_ { std::make_shared<State_>() },
|
||||||
|
thread_ { threadLoop, state_ } {
|
||||||
|
}
|
||||||
|
|
||||||
|
SingleThread::~SingleThread() {
|
||||||
|
{
|
||||||
|
std::lock_guard lock { state_->mutex };
|
||||||
|
state_->cancelled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
state_->cv.notify_all();
|
||||||
|
thread_.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SingleThread::post(std::function<void()> task) {
|
||||||
|
{
|
||||||
|
std::lock_guard lock { state_->mutex };
|
||||||
|
state_->tasks.push_back(std::move(task));
|
||||||
|
}
|
||||||
|
state_->cv.notify_all();
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <thread>
|
||||||
|
#include <functional>
|
||||||
|
#include <future>
|
||||||
|
|
||||||
|
// Reserves a single thread. Similar to Java/Android's "Handler"
|
||||||
|
class SingleThread {
|
||||||
|
public:
|
||||||
|
SingleThread();
|
||||||
|
// Waits for all pending tasks to complete before destruction
|
||||||
|
~SingleThread();
|
||||||
|
|
||||||
|
// Run the `task` on the thread and return a future that will resolve to the result
|
||||||
|
template <typename T>
|
||||||
|
std::future<T> run(std::function<T()> task) {
|
||||||
|
auto promise = std::make_shared<std::promise<T>>();
|
||||||
|
post([promise = promise, task = std::move(task)] {
|
||||||
|
try {
|
||||||
|
T result = task();
|
||||||
|
promise->set_value(result);
|
||||||
|
} catch (...) {
|
||||||
|
// Use current_exception to create an exception pointer.
|
||||||
|
// See https://en.cppreference.com/w/cpp/thread/promise/set_exception
|
||||||
|
promise->set_exception(std::current_exception());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return promise->get_future();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explicit definition of run<void> to work around templating issues:
|
||||||
|
std::future<void> run(std::function<void()> task) {
|
||||||
|
auto promise = std::make_shared<std::promise<void>>();
|
||||||
|
post([promise = promise, task = std::move(task)] {
|
||||||
|
try {
|
||||||
|
task();
|
||||||
|
promise->set_value();
|
||||||
|
} catch (...) {
|
||||||
|
promise->set_exception(std::current_exception());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return promise->get_future();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the `task` on the thread and wait for it to complete
|
||||||
|
template <typename T>
|
||||||
|
T runAndWait(std::function<T()> task) {
|
||||||
|
return run(task).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
struct State_;
|
||||||
|
private:
|
||||||
|
// Asynchronously runs the given `task` on the thread
|
||||||
|
void post(std::function<void()> task);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::shared_ptr<State_> state_;
|
||||||
|
|
||||||
|
std::thread thread_;
|
||||||
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
#include "SingleThread_test.hpp"
|
||||||
|
#include "SingleThread.hpp"
|
||||||
|
#include "testing.hpp"
|
||||||
|
#include "androidUtil.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <sstream>
|
||||||
|
#include <cmath>
|
||||||
|
#include <random>
|
||||||
|
|
||||||
|
void shouldConstructAndDestruct();
|
||||||
|
void shouldReturnSingleValue();
|
||||||
|
void shouldReturnMultipleAsync();
|
||||||
|
void shouldForwardExceptions();
|
||||||
|
|
||||||
|
void SingleThread_test() {
|
||||||
|
LOGD("SingleThread: Starting tests...");
|
||||||
|
shouldConstructAndDestruct();
|
||||||
|
shouldReturnSingleValue();
|
||||||
|
shouldReturnMultipleAsync();
|
||||||
|
shouldForwardExceptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
void shouldConstructAndDestruct() {
|
||||||
|
{
|
||||||
|
SingleThread thread {};
|
||||||
|
LOGD("SingleThread: Constructed!");
|
||||||
|
}
|
||||||
|
LOGD("SingleThread: Destructed!");
|
||||||
|
logTestPass("shouldConstructAndDestruct");
|
||||||
|
}
|
||||||
|
|
||||||
|
void shouldReturnSingleValue() {
|
||||||
|
SingleThread thread {};
|
||||||
|
|
||||||
|
LOGD("SingleThread: Posting a value and waiting!");
|
||||||
|
|
||||||
|
assertEqual(thread.runAndWait<int>([] {
|
||||||
|
LOGD("SingleThread: Inside thread!");
|
||||||
|
return 3;
|
||||||
|
}), 3, "should support returning a value from a lambda");
|
||||||
|
LOGD("SingleThread: Posted a value and waited!");
|
||||||
|
|
||||||
|
logTestPass("shouldReturnSingleValue");
|
||||||
|
}
|
||||||
|
|
||||||
|
void shouldReturnMultipleAsync() {
|
||||||
|
SingleThread thread {};
|
||||||
|
|
||||||
|
auto future1 = thread.run<int>([] {
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
auto future2 = thread.run<int>([] {
|
||||||
|
return 2;
|
||||||
|
});
|
||||||
|
auto future3 = thread.run<int>([] {
|
||||||
|
return 3;
|
||||||
|
});
|
||||||
|
|
||||||
|
assertEqual(future1.get(), 1, "getting future1");
|
||||||
|
assertEqual(future3.get(), 3, "getting future3");
|
||||||
|
assertEqual(future2.get(), 2, "getting future2");
|
||||||
|
|
||||||
|
logTestPass("shouldReturnMultipleAsync");
|
||||||
|
}
|
||||||
|
|
||||||
|
void shouldForwardExceptions() {
|
||||||
|
SingleThread thread {};
|
||||||
|
|
||||||
|
bool caught = false;
|
||||||
|
try {
|
||||||
|
thread.runAndWait<void>([] {
|
||||||
|
throw std::runtime_error("testing...");
|
||||||
|
});
|
||||||
|
} catch (std::runtime_error ex) {
|
||||||
|
caught = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEqual(caught, true, "should have caught the exception");
|
||||||
|
logTestPass("shouldForwardExceptions");
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
void SingleThread_test();
|
||||||
+7
-7
@@ -1,10 +1,10 @@
|
|||||||
#include "WhisperSession.h"
|
#include "WhisperSession.hpp"
|
||||||
|
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include "whisper.h"
|
#include "whisper.h"
|
||||||
#include "findLongestSilence.h"
|
#include "findLongestSilence.hpp"
|
||||||
#include "androidUtil.h"
|
#include "androidUtil.h"
|
||||||
|
|
||||||
WhisperSession::WhisperSession(const std::string& modelPath, std::string lang, std::string prompt, bool shortAudioContext)
|
WhisperSession::WhisperSession(const std::string& modelPath, std::string lang, std::string prompt, bool shortAudioContext)
|
||||||
@@ -216,11 +216,11 @@ WhisperSession::transcribeNextChunk() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void WhisperSession::addAudio(const float *pAudio, int sizeAudio) {
|
void WhisperSession::addAudio(float* data, size_t count) {
|
||||||
// Update the local audio buffer
|
// See https://en.cppreference.com/w/cpp/algorithm/copy_n.html,
|
||||||
for (int i = 0; i < sizeAudio; i++) {
|
// and the suggestions at https://stackoverflow.com/q/34552783.
|
||||||
audioBuffer_.push_back(pAudio[i]);
|
audioBuffer_.reserve(count);
|
||||||
}
|
std::copy_n(data, count, std::back_inserter(audioBuffer_));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string WhisperSession::transcribeAll() {
|
std::string WhisperSession::transcribeAll() {
|
||||||
+1
-1
@@ -10,7 +10,7 @@ public:
|
|||||||
WhisperSession(const std::string& modelPath, std::string lang, std::string prompt, bool shortAudioContext);
|
WhisperSession(const std::string& modelPath, std::string lang, std::string prompt, bool shortAudioContext);
|
||||||
~WhisperSession();
|
~WhisperSession();
|
||||||
// Adds to the buffer
|
// Adds to the buffer
|
||||||
void addAudio(const float *pAudio, int sizeAudio);
|
void addAudio(float* audio, size_t count);
|
||||||
// Returns the next finalized slice of audio (if any) and updates the preview.
|
// Returns the next finalized slice of audio (if any) and updates the preview.
|
||||||
std::string transcribeNextChunk();
|
std::string transcribeNextChunk();
|
||||||
// Transcribes all buffered audio data that hasn't been finalized yet
|
// Transcribes all buffered audio data that hasn't been finalized yet
|
||||||
+12
@@ -1,5 +1,15 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef __APPLE__
|
||||||
|
|
||||||
|
// For now, logging methods are no-ops on iOS
|
||||||
|
#define LOGW(...) void;
|
||||||
|
#define LOGI(...) void;
|
||||||
|
#define LOGD(...) void;
|
||||||
|
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
#include <android/log.h>
|
#include <android/log.h>
|
||||||
|
|
||||||
// Use macros for these rather than functions. Functions generate a "may be unsafe"
|
// Use macros for these rather than functions. Functions generate a "may be unsafe"
|
||||||
@@ -8,3 +18,5 @@
|
|||||||
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, "Whisper::JNI", __VA_ARGS__);
|
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, "Whisper::JNI", __VA_ARGS__);
|
||||||
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "Whisper::JNI", __VA_ARGS__);
|
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "Whisper::JNI", __VA_ARGS__);
|
||||||
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, "Whisper::JNI", __VA_ARGS__);
|
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, "Whisper::JNI", __VA_ARGS__);
|
||||||
|
|
||||||
|
#endif
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
#include "findLongestSilence.h"
|
#include "findLongestSilence.hpp"
|
||||||
#include "androidUtil.h"
|
#include "androidUtil.h"
|
||||||
|
|
||||||
static void highpass(std::vector<float>& data, int sampleRate) {
|
static void highpass(std::vector<float>& data, int sampleRate) {
|
||||||
+3
-11
@@ -1,6 +1,7 @@
|
|||||||
#include "findLongestSilence_test.h"
|
#include "findLongestSilence_test.hpp"
|
||||||
#include "findLongestSilence.h"
|
#include "findLongestSilence.hpp"
|
||||||
#include "androidUtil.h"
|
#include "androidUtil.h"
|
||||||
|
#include "testing.hpp"
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -13,8 +14,6 @@ static void testToneWithPause();
|
|||||||
static void testSilence();
|
static void testSilence();
|
||||||
static void testNoise();
|
static void testNoise();
|
||||||
|
|
||||||
static void fail(const std::string& message);
|
|
||||||
|
|
||||||
struct GeneratedAudio {
|
struct GeneratedAudio {
|
||||||
std::vector<float> data;
|
std::vector<float> data;
|
||||||
int sampleRate;
|
int sampleRate;
|
||||||
@@ -91,10 +90,6 @@ static void testNoise() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static void fail(const std::string& message) {
|
|
||||||
throw std::runtime_error(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
static GeneratedAudio makeAudio(const AudioGenerator& generator, int sampleRate, float duration) {
|
static GeneratedAudio makeAudio(const AudioGenerator& generator, int sampleRate, float duration) {
|
||||||
std::vector<float> result { };
|
std::vector<float> result { };
|
||||||
|
|
||||||
@@ -111,9 +106,6 @@ static GeneratedAudio makeAudio(const AudioGenerator& generator, int sampleRate,
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static void logTestPass(const std::string& message) {
|
|
||||||
LOGI("Test PASS: %s", message.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
static float samplesToSeconds(int samples, int sampleRate) {
|
static float samplesToSeconds(int samples, int sampleRate) {
|
||||||
return static_cast<float>(samples) / static_cast<float>(sampleRate);
|
return static_cast<float>(samples) / static_cast<float>(sampleRate);
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#include "testing.hpp"
|
||||||
|
|
||||||
|
void fail(const std::string& message) {
|
||||||
|
throw std::runtime_error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
void logTestPass(const std::string& message) {
|
||||||
|
LOGI("Test PASS: %s", message.c_str());
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <sstream>
|
||||||
|
#include "androidUtil.h"
|
||||||
|
|
||||||
|
// Test-related utilities
|
||||||
|
|
||||||
|
void fail(const std::string& message);
|
||||||
|
|
||||||
|
void logTestPass(const std::string& message);
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
void assertEqual(const T& a, const T& b, const std::string& message) {
|
||||||
|
if (a != b) {
|
||||||
|
std::stringstream fullMessage;
|
||||||
|
fullMessage << "Not equal:" << a << " != " << b << " (" << message << ")";
|
||||||
|
fail(fullMessage.str());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// Auto-generated
|
||||||
|
#pragma once
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://nitro.margelo.com/nitro.schema.json",
|
||||||
|
"cxxNamespace": [
|
||||||
|
"whispervoicetyping"
|
||||||
|
],
|
||||||
|
"ios": {
|
||||||
|
"iosModuleName": "WhisperVoiceTyping"
|
||||||
|
},
|
||||||
|
"android": {
|
||||||
|
"androidNamespace": [
|
||||||
|
"whispervoicetyping"
|
||||||
|
],
|
||||||
|
"androidCxxLibName": "WhisperVoiceTyping"
|
||||||
|
},
|
||||||
|
"autolinking": {
|
||||||
|
"WhisperVoiceTyping": {
|
||||||
|
"cpp": "HybridWhisperVoiceTyping"
|
||||||
|
},
|
||||||
|
"AudioRecorder": {
|
||||||
|
"kotlin": "HybridAudioRecorder"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ignorePaths": [
|
||||||
|
"**/node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"name": "@joplin/whisper-voice-typing",
|
||||||
|
"version": "3.6.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Whisper.cpp bindings for React Native!",
|
||||||
|
"author": "Joplin team",
|
||||||
|
"main": "lib/index",
|
||||||
|
"module": "lib/index",
|
||||||
|
"types": "lib/index.d.ts",
|
||||||
|
"react-native": "src/index",
|
||||||
|
"source": "src/index",
|
||||||
|
"files": [
|
||||||
|
"src",
|
||||||
|
"react-native.config.js",
|
||||||
|
"lib",
|
||||||
|
"nitrogen",
|
||||||
|
"android/build.gradle",
|
||||||
|
"android/gradle.properties",
|
||||||
|
"android/CMakeLists.txt",
|
||||||
|
"android/src",
|
||||||
|
"ios/**/*.h",
|
||||||
|
"ios/**/*.m",
|
||||||
|
"ios/**/*.mm",
|
||||||
|
"ios/**/*.cpp",
|
||||||
|
"ios/**/*.swift",
|
||||||
|
"cppxx/**/*",
|
||||||
|
"app.plugin.js",
|
||||||
|
"nitro.json",
|
||||||
|
"*.podspec",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"clean": "rm -rf android/build node_modules/**/android/build lib",
|
||||||
|
"tsc": "tsc",
|
||||||
|
"build": "tsc && nitrogen"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"react-native",
|
||||||
|
"nitro"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/laurent22/joplin.git"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/laurent22/joplin",
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "19.2.10",
|
||||||
|
"nitrogen": "0.33.2",
|
||||||
|
"react": "19.1.0",
|
||||||
|
"react-native": "0.81.5",
|
||||||
|
"react-native-nitro-modules": "0.33.2",
|
||||||
|
"typescript": "5.8.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*",
|
||||||
|
"react-native-nitro-modules": "*"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// https://github.com/react-native-community/cli/blob/main/docs/dependencies.md
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
dependency: {
|
||||||
|
platforms: {
|
||||||
|
// * @type {import('@react-native-community/cli-types').IOSDependencyParams}
|
||||||
|
ios: {},
|
||||||
|
// * @type {import('@react-native-community/cli-types').AndroidDependencyParams}
|
||||||
|
android: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { NitroModules } from 'react-native-nitro-modules';
|
||||||
|
import type { AudioRecorder, SessionOptions, WhisperSession, WhisperVoiceTyping } from './specs/Whisper.nitro';
|
||||||
|
|
||||||
|
let WhisperVoiceTypingHybridObject: WhisperVoiceTyping|null = null;
|
||||||
|
|
||||||
|
export type { SessionOptions };
|
||||||
|
|
||||||
|
const getVoiceTyping = () => {
|
||||||
|
WhisperVoiceTypingHybridObject ??= NitroModules.createHybridObject<WhisperVoiceTyping>('WhisperVoiceTyping');
|
||||||
|
return WhisperVoiceTypingHybridObject;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Session {
|
||||||
|
private recorder_: AudioRecorder|null = null;
|
||||||
|
private nativeSession_: WhisperSession|null = null;
|
||||||
|
private convertCancellationCounter_ = 0;
|
||||||
|
|
||||||
|
public constructor(private options_: SessionOptions) {}
|
||||||
|
|
||||||
|
private isOpen_() {
|
||||||
|
return this.recorder_ || this.nativeSession_;
|
||||||
|
}
|
||||||
|
|
||||||
|
public open() {
|
||||||
|
if (this.isOpen_()) {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.recorder_ = NitroModules.createHybridObject<AudioRecorder>('AudioRecorder');
|
||||||
|
this.nativeSession_ = getVoiceTyping().openSession(this.options_);
|
||||||
|
this.recorder_.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async convertNext(duration: number|null) {
|
||||||
|
if (!this.nativeSession_ || !this.recorder_) {
|
||||||
|
throw new Error('No available session! Call .open() before .convertNext().');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.convertCancellationCounter_++;
|
||||||
|
const cancelCounter = this.convertCancellationCounter_;
|
||||||
|
|
||||||
|
if (duration) {
|
||||||
|
await this.recorder_.waitForData(duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancelled?
|
||||||
|
if (cancelCounter !== this.convertCancellationCounter_) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const available = this.recorder_.pullAvailable();
|
||||||
|
this.nativeSession_.pushAudio(available);
|
||||||
|
return this.nativeSession_.convertNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
public close() {
|
||||||
|
this.recorder_?.stop();
|
||||||
|
|
||||||
|
// Allow recorder_ and nativeSession_ to be garbage collected:
|
||||||
|
this.recorder_ = null;
|
||||||
|
this.nativeSession_ = null;
|
||||||
|
this.convertCancellationCounter_++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openSession(options: SessionOptions) {
|
||||||
|
return new Session(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function test() {
|
||||||
|
return getVoiceTyping().test();
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import type { HybridObject } from 'react-native-nitro-modules';
|
||||||
|
|
||||||
|
export interface AudioRecorder extends HybridObject<{ android: 'kotlin' }> {
|
||||||
|
start(): void;
|
||||||
|
stop(): void;
|
||||||
|
waitForData(seconds: number): Promise<void>;
|
||||||
|
pullAvailable(): ArrayBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WhisperSession extends HybridObject<{ android: 'c++' }> {
|
||||||
|
pushAudio(audio: ArrayBuffer): void;
|
||||||
|
convertNext(): Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionOptions {
|
||||||
|
modelPath: string;
|
||||||
|
locale: string;
|
||||||
|
prompt: string;
|
||||||
|
shortAudioContext: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WhisperVoiceTyping extends HybridObject<{ android: 'c++' }> {
|
||||||
|
openSession(options: SessionOptions): WhisperSession;
|
||||||
|
test(): Promise<void>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"outDir": "lib",
|
||||||
|
"rootDir": "src",
|
||||||
|
"allowUnreachableCode": false,
|
||||||
|
"allowUnusedLabels": false,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"jsx": "react",
|
||||||
|
"lib": ["esnext"],
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"noEmit": false,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noImplicitUseStrict": false,
|
||||||
|
"noStrictGenericChecks": false,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"target": "esnext",
|
||||||
|
"verbatimModuleSyntax": true
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user