Compare commits
10 Commits
52bb6f812c
...
4549442bd8
Author | SHA1 | Date | |
---|---|---|---|
|
4549442bd8 | ||
|
87446784ae | ||
|
6f941c56d1 | ||
|
c3d543be39 | ||
|
eb7599bfe8 | ||
|
6673663883 | ||
|
b3c2e09987 | ||
|
bc3d481d25 | ||
|
081ac367ba | ||
|
68cc052417 |
39
.ollama/ExampleComponent.tsx
Normal file
39
.ollama/ExampleComponent.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* NOTE: this file is for ollama. The AI assistant should
|
||||||
|
* follow this format, but modify it according to the user's prompt.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import any necessary packages
|
||||||
|
**/
|
||||||
|
|
||||||
|
import { StyleSheet } from "react-native";
|
||||||
|
|
||||||
|
type ExampleComponentProps = {
|
||||||
|
/**
|
||||||
|
* Declare any properties here
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Don't include the typing when defiining the function. Define it normally.
|
||||||
|
*/
|
||||||
|
const ISpeakButton = (props : ExampleComponentProps) => {
|
||||||
|
|
||||||
|
// Do any housekeeping up here, like `useState`, `useEffect`, etc.
|
||||||
|
|
||||||
|
// Return the TSX component below
|
||||||
|
return (
|
||||||
|
<></>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create style sheets here
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
// Export the component
|
||||||
|
|
||||||
|
export default ISpeakButton;
|
37
.ollama/ExampleTest.spec.tsx
Normal file
37
.ollama/ExampleTest.spec.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* NOTE: this file is for ollama. The AI assistant should
|
||||||
|
* follow this format, but modify it according to the user's prompt.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import any necessary packages
|
||||||
|
**/
|
||||||
|
import React, { act } from 'react';
|
||||||
|
import { render, screen } from '@testing-library/react-native'
|
||||||
|
import { MyComponent } from '@/app/component/MyComponent';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IMPORTANT NOTE: If you need to use jest mock, remember that
|
||||||
|
* you cannot include any external components in the mock.
|
||||||
|
* You absolutely must use `jest.requireActual` to import the
|
||||||
|
* component *within* the jest.mock callback.
|
||||||
|
*/
|
||||||
|
jest.mock('@/app/component/index.tsx', () => {
|
||||||
|
// Require-actual the component
|
||||||
|
const { Text } = jest.requireActual('react-native');
|
||||||
|
// Use the component.
|
||||||
|
return () => <Text>Index</Text>
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Message Component', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// do any set up here
|
||||||
|
});
|
||||||
|
|
||||||
|
it('A test that renders a widget', async () => {
|
||||||
|
render(<MyComponent />);
|
||||||
|
// IMPORTANT: use jest.expect(...) functions for assertions.
|
||||||
|
expect(await screen.findByText("something")).toBeOnTheScreen();
|
||||||
|
});
|
||||||
|
});
|
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -1,3 +1,2 @@
|
|||||||
{
|
{
|
||||||
"jestTestExplorer.pathToJest": "./node_modules/.bin/jest"
|
|
||||||
}
|
}
|
44
__mocks__/api.ts
Normal file
44
__mocks__/api.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
// __mocks__/api.ts
|
||||||
|
|
||||||
|
import { language_matrix, language_matrix_entry } from "@/app/i18n/api";
|
||||||
|
|
||||||
|
// Import the actual API module to extend its functionality
|
||||||
|
const origApi = jest.requireActual('@/app/i18n/api.ts');
|
||||||
|
|
||||||
|
class LanguageServer {
|
||||||
|
constructor(...args: any[]) { }
|
||||||
|
fetchLanguages(): language_matrix {
|
||||||
|
return {
|
||||||
|
"en" : { code: "en", name: "English", targets: ['fr', 'es'] },
|
||||||
|
"fr" : { code: "fr", name: "French", targets: ["en", "es"] },
|
||||||
|
"es": { code: "es", name: "Spanish", targets: ['fr', 'en'] },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static getDefault() {
|
||||||
|
return new LanguageServer("http://localhost:5002");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Translator {
|
||||||
|
constructor(...args : any []) {}
|
||||||
|
translate(message : string, target : string) {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
static getDefault(code : string) {
|
||||||
|
return new Translator(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CachedTranslator extends Translator{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
...origApi,
|
||||||
|
LanguageServer,
|
||||||
|
Translator,
|
||||||
|
CachedTranslator,
|
||||||
|
// Mock the specific functions you want to override
|
||||||
|
fetchData: jest.fn(() => Promise.resolve({ data: 'mocked data' })),
|
||||||
|
// Add more mock implementations as needed
|
||||||
|
};
|
10
__mocks__/db.ts
Normal file
10
__mocks__/db.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export default {
|
||||||
|
getDb: jest.fn(() => {
|
||||||
|
return {
|
||||||
|
runAsync: jest.fn((statement: string, value: string) => {}),
|
||||||
|
getFirstAsync: jest.fn((statement: string, value: string) => {
|
||||||
|
return [];
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
4
__mocks__/expo-file-system/next.js
Normal file
4
__mocks__/expo-file-system/next.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export const File = jest.fn();
|
||||||
|
export const Paths = {
|
||||||
|
join: jest.fn(),
|
||||||
|
};
|
@ -1,44 +0,0 @@
|
|||||||
// __mocks__/expo-sqlite.js
|
|
||||||
const sqlite3 = require('sqlite3').verbose();
|
|
||||||
|
|
||||||
class SQLiteDatabase {
|
|
||||||
constructor(name) {
|
|
||||||
this.db = new sqlite3.Database(':memory:');
|
|
||||||
}
|
|
||||||
|
|
||||||
runAsync(sql, params = []) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.db.run(sql, params, function (err) {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
resolve({ changes: this.changes });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
execAsync(sql) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.db.exec(sql, err => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
closeAsync() {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
this.db.close(() => resolve());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const SQLite = {
|
|
||||||
openDatabaseAsync: jest.fn(name => new SQLiteDatabase(name)),
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = SQLite;
|
|
16
__mocks__/settings.ts
Normal file
16
__mocks__/settings.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
const originalModule = jest.requireActual("@/app/lib/settings");
|
||||||
|
class MockSettings {
|
||||||
|
public constructor(public db = {}) {}
|
||||||
|
public setHostLanguage = jest.fn((val: string) => {});
|
||||||
|
public setLibretranslateBaseUrl(val: string) {}
|
||||||
|
getHostLanguage = jest.fn(() => {
|
||||||
|
return "en";
|
||||||
|
});
|
||||||
|
getLibretranslateBaseUrl = jest.fn(() => {
|
||||||
|
return "http://localhost:5004";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export default {
|
||||||
|
...originalModule,
|
||||||
|
Settings: MockSettings,
|
||||||
|
};
|
@ -1,8 +1,10 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
<queries>
|
<queries>
|
||||||
<intent>
|
<intent>
|
||||||
|
6
app.json
6
app.json
@ -3,7 +3,7 @@
|
|||||||
"name": "translation-terrace",
|
"name": "translation-terrace",
|
||||||
"slug": "translation-terrace",
|
"slug": "translation-terrace",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"orientation": "portrait",
|
"orientation": "landscape",
|
||||||
"icon": "./assets/images/icon.png",
|
"icon": "./assets/images/icon.png",
|
||||||
"scheme": "myapp",
|
"scheme": "myapp",
|
||||||
"userInterfaceStyle": "automatic",
|
"userInterfaceStyle": "automatic",
|
||||||
@ -30,7 +30,7 @@
|
|||||||
[
|
[
|
||||||
"expo-screen-orientation",
|
"expo-screen-orientation",
|
||||||
{
|
{
|
||||||
"initialOrientation": "LANDSCAPE"
|
"orientation": "landscape"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@ -48,12 +48,10 @@
|
|||||||
"enableFTS": true,
|
"enableFTS": true,
|
||||||
"useSQLCipher": true,
|
"useSQLCipher": true,
|
||||||
"android": {
|
"android": {
|
||||||
// Override the shared configuration for Android
|
|
||||||
"enableFTS": false,
|
"enableFTS": false,
|
||||||
"useSQLCipher": false
|
"useSQLCipher": false
|
||||||
},
|
},
|
||||||
"ios": {
|
"ios": {
|
||||||
// You can also override the shared configurations for iOS
|
|
||||||
"customBuildFlags": [
|
"customBuildFlags": [
|
||||||
"-DSQLITE_ENABLE_DBSTAT_VTAB=1 -DSQLITE_ENABLE_SNAPSHOT=1"
|
"-DSQLITE_ENABLE_DBSTAT_VTAB=1 -DSQLITE_ENABLE_SNAPSHOT=1"
|
||||||
]
|
]
|
||||||
|
@ -1,19 +1,21 @@
|
|||||||
import { Stack } from 'expo-router';
|
import * as React from "react";
|
||||||
|
import TTNavStack from "@/components/TTNavStack";
|
||||||
|
import * as ScreenOrientation from "expo-screen-orientation";
|
||||||
|
import { NavigationContainer } from "@react-navigation/native";
|
||||||
|
import { migrateDb } from "./lib/db";
|
||||||
|
import { Text } from "react-native";
|
||||||
|
|
||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
return (
|
const [loaded, setLoaded] = React.useState<boolean>(true);
|
||||||
<Stack
|
React.useEffect(() => {
|
||||||
screenOptions={{
|
(async function () {
|
||||||
headerStyle: {
|
await migrateDb();
|
||||||
backgroundColor: '#f4511e',
|
await ScreenOrientation.unlockAsync();
|
||||||
},
|
await ScreenOrientation.lockAsync(
|
||||||
headerTintColor: '#fff',
|
ScreenOrientation.OrientationLock.LANDSCAPE_LEFT
|
||||||
headerTitleStyle: {
|
);
|
||||||
fontWeight: 'bold',
|
setLoaded(true);
|
||||||
},
|
})();
|
||||||
}}
|
});
|
||||||
>
|
return loaded ? <TTNavStack /> : <Text>Loading...</Text>;
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Cache } from "react-native-cache";
|
import { Cache } from "react-native-cache";
|
||||||
import { LIBRETRANSLATE_BASE_URL } from "@/constants/api";
|
import { LIBRETRANSLATE_BASE_URL } from "@/constants/api";
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import { Settings } from "../lib/settings";
|
||||||
|
|
||||||
type language_t = string;
|
type language_t = string;
|
||||||
|
|
||||||
@ -23,17 +24,23 @@ export type language_matrix = {
|
|||||||
[key:string] : language_matrix_entry
|
[key:string] : language_matrix_entry
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Translator {
|
export async function fetchWithTimeout(url : string, options : RequestInit, timeout = 5000) : Promise<Response> {
|
||||||
constructor(public source : language_t, public defaultTarget : string = "en", private baseUrl = LIBRETRANSLATE_BASE_URL) {
|
return Promise.race([
|
||||||
}
|
fetch(url, options),
|
||||||
|
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout))
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
async fetchLanguages() : Promise<language_matrix> {
|
export class LanguageServer {
|
||||||
|
constructor(public baseUrl : string) {}
|
||||||
|
|
||||||
|
async fetchLanguages(timeout = 500) : Promise<language_matrix> {
|
||||||
let data = {};
|
let data = {};
|
||||||
const res = await fetch(this.baseUrl + "/languages", {
|
const res = await fetchWithTimeout(this.baseUrl + "/languages", {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
});
|
}, timeout);
|
||||||
try {
|
try {
|
||||||
data = await res.json();
|
data = await res.json();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -53,8 +60,22 @@ export class Translator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async getDefault() {
|
||||||
|
const settings = await Settings.getDefault();
|
||||||
|
return new LanguageServer(await settings.getLibretranslateBaseUrl() || LIBRETRANSLATE_BASE_URL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Translator {
|
||||||
|
constructor(public source : language_t, public defaultTarget : string = "en", private _languageServer : LanguageServer) {
|
||||||
|
}
|
||||||
|
|
||||||
|
get languageServer() {
|
||||||
|
return this._languageServer;
|
||||||
|
}
|
||||||
|
|
||||||
async translate(text : string, target : string|undefined = undefined) {
|
async translate(text : string, target : string|undefined = undefined) {
|
||||||
const url = LIBRETRANSLATE_BASE_URL + `/translate`;
|
const url = this._languageServer.baseUrl + `/translate`;
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@ -73,6 +94,12 @@ export class Translator {
|
|||||||
console.log(data)
|
console.log(data)
|
||||||
return data.translatedText
|
return data.translatedText
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async getDefault(defaultTarget: string | undefined = undefined) {
|
||||||
|
const settings = await Settings.getDefault();
|
||||||
|
const source = await settings.getHostLanguage();
|
||||||
|
return new Translator(source, defaultTarget, await LanguageServer.getDefault())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CachedTranslator extends Translator {
|
export class CachedTranslator extends Translator {
|
||||||
@ -87,4 +114,10 @@ export class CachedTranslator extends Translator {
|
|||||||
await cache.set(key2, tr2);
|
await cache.set(key2, tr2);
|
||||||
return tr2;
|
return tr2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async getDefault(defaultTarget: string | undefined = undefined) {
|
||||||
|
const settings = await Settings.getDefault();
|
||||||
|
const source = await settings.getHostLanguage();
|
||||||
|
return new CachedTranslator(source, defaultTarget, await LanguageServer.getDefault())
|
||||||
|
}
|
||||||
}
|
}
|
@ -12,7 +12,7 @@ export function chooseCountry(lang_a2 : string) {
|
|||||||
c => c.languages.includes(lang_a3.alpha3)
|
c => c.languages.includes(lang_a3.alpha3)
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log("cc = %x, ", cs.map(c => c.alpha2))
|
// console.log("cc = %x, ", cs.map(c => c.alpha2))
|
||||||
|
|
||||||
return cs.filter(cc => Object.keys(LANG_FLAGS).includes(cc.alpha2.toLowerCase())).map(c => c.alpha2.toLowerCase());
|
return cs.filter(cc => Object.keys(LANG_FLAGS).includes(cc.alpha2.toLowerCase())).map(c => c.alpha2.toLowerCase());
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,9 @@ import _LANGUAGES from "@/assets/languages.min.json"
|
|||||||
export const LANG_FLAGS = _LANG_FLAGS
|
export const LANG_FLAGS = _LANG_FLAGS
|
||||||
|
|
||||||
export function longLang(shortLang : string) {
|
export function longLang(shortLang : string) {
|
||||||
return ((LANG_FLAGS as any)[shortLang] as any)["nameEnglish"] as string
|
const obj = LANG_FLAGS[shortLang];
|
||||||
|
if (!obj) return undefined;
|
||||||
|
return obj["name"] as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function lang_a3_a2(a3 : string) {
|
export function lang_a3_a2(a3 : string) {
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { LanguageSelection } from "@/components/LanguageSelection";
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { Link, Stack } from "expo-router";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Image, Text, View, StyleSheet, Button, Pressable } from "react-native";
|
import { Image, Text, View, StyleSheet, Button, Pressable } from "react-native";
|
||||||
import { Translator, language_matrix_entry } from "./i18n/api";
|
import { LanguageServer, Translator, language_matrix_entry } from "./i18n/api";
|
||||||
import ConversationThread from "@/components/ConversationThread";
|
|
||||||
import { Conversation } from "./lib/conversation";
|
import { Conversation } from "./lib/conversation";
|
||||||
|
import { LanguageSelection } from "@/components/LanguageSelection";
|
||||||
|
import { Link } from 'expo-router';
|
||||||
|
|
||||||
function LogoTitle() {
|
function LogoTitle() {
|
||||||
return (
|
return (
|
||||||
@ -16,21 +16,25 @@ function LogoTitle() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
const navigation = useNavigation();
|
||||||
|
|
||||||
const [lang, setLang] = useState<language_matrix_entry | undefined>();
|
const [lang, setLang] = useState<language_matrix_entry | undefined>();
|
||||||
const [conversation, setConversation] = useState<Conversation | undefined>();
|
const [conversation, setConversation] = useState<Conversation | undefined>();
|
||||||
const [setShowSettings, showSettings] = useState<boolean>(false);
|
const [setShowSettings, showSettings] = useState<boolean>(false);
|
||||||
|
|
||||||
function onLangSelected(lang: language_matrix_entry | undefined) {
|
async function onLangSelected(lang: language_matrix_entry) {
|
||||||
console.log("Language %s selected", lang?.code);
|
console.log("Language %s selected", lang.code);
|
||||||
setLang(lang);
|
setLang(lang);
|
||||||
if (!lang?.code) return;
|
if (!lang?.code) return;
|
||||||
setConversation(
|
const langServer = await LanguageServer.getDefault();
|
||||||
new Conversation(
|
const conversation = new Conversation(
|
||||||
new Translator("en", lang.code),
|
new Translator("en", lang.code, langServer),
|
||||||
{ id: "host", language: "en" },
|
{ id: "host", language: "en" },
|
||||||
{ id: "guest", language: lang.code }
|
{ id: "guest", language: lang.code }
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
navigation.navigate("Conversation", {
|
||||||
|
conversation,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onGoBack() {
|
function onGoBack() {
|
||||||
@ -39,9 +43,10 @@ export default function Home() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Stack.Screen name="index" />
|
<Pressable onPress={() => navigation.navigate("Settings")}>
|
||||||
<Stack.Screen name="settings" />
|
<Text>Settings</Text>
|
||||||
<Stack.Screen name="conversation" />
|
</Pressable>
|
||||||
|
<LanguageSelection onLangSelected={onLangSelected} />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
// conversation.test.ts
|
// conversation.test.ts
|
||||||
|
|
||||||
import { Translator } from '@/app/i18n/api';
|
import { LanguageServer, Translator } from '@/app/i18n/api';
|
||||||
import { Conversation, Message, Speaker } from '@/app/lib/conversation';
|
import { Conversation, Message, Speaker } from '@/app/lib/conversation';
|
||||||
|
import { LIBRETRANSLATE_BASE_URL } from '@/constants/api';
|
||||||
import { describe, beforeEach, it, expect, test } from '@jest/globals';
|
import { describe, beforeEach, it, expect, test } from '@jest/globals';
|
||||||
|
|
||||||
describe('Conversation', () => {
|
describe('Conversation', () => {
|
||||||
let conversation: Conversation;
|
let conversation: Conversation;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const translator = new Translator("en", "es");
|
const translator = new Translator("en", "es", new LanguageServer(LIBRETRANSLATE_BASE_URL));
|
||||||
const s1: Speaker = { language: "en", id: "host" };
|
const s1: Speaker = { language: "en", id: "host" };
|
||||||
const s2: Speaker = { id: "guest", language: "es" }
|
const s2: Speaker = { id: "guest", language: "es" }
|
||||||
conversation = new Conversation(translator, s1, s2);
|
conversation = new Conversation(translator, s1, s2);
|
||||||
|
@ -1,23 +1,27 @@
|
|||||||
import {describe, expect, beforeEach} from '@jest/globals';
|
import {describe, expect, beforeEach} from '@jest/globals';
|
||||||
import {Settings} from '@/app/lib/settings';
|
import {Settings} from '@/app/lib/settings';
|
||||||
import { getDb } from '@/app/lib/db';
|
import { getDb, migrateDb } from '@/app/lib/db';
|
||||||
|
|
||||||
describe('Settings', () => {
|
describe('Settings', () => {
|
||||||
let settings: Settings;
|
let settings: Settings;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
// Initialize your Settings class here with a fresh database instance
|
// Initialize your Settings class here with a fresh database instance
|
||||||
settings = new Settings(await getDb());
|
await migrateDb();
|
||||||
|
const db = await getDb();
|
||||||
|
if (!db) throw new Error("Could not get db");
|
||||||
|
settings = new Settings(db);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
// Clean up the database after each test
|
// Clean up the database after each test
|
||||||
await settings.db.executeSql('DELETE FROM settings');
|
settings && await settings.db.executeSql('DELETE FROM settings');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('setHostLanguage', () => {
|
describe('setHostLanguage', () => {
|
||||||
it('should set the host language in the database', async () => {
|
it('should set the host language in the database', async () => {
|
||||||
const value = 'en';
|
const value = 'en';
|
||||||
|
await settings.db.runAsync("REPLACE INTO settings (host_language) VALUES (?)", "en");
|
||||||
await settings.setHostLanguage(value);
|
await settings.setHostLanguage(value);
|
||||||
|
|
||||||
const result = await settings.getHostLanguage();
|
const result = await settings.getHostLanguage();
|
||||||
@ -28,10 +32,7 @@ describe('Settings', () => {
|
|||||||
describe('getHostLanguage', () => {
|
describe('getHostLanguage', () => {
|
||||||
it('should return the host language from the database', async () => {
|
it('should return the host language from the database', async () => {
|
||||||
const value = 'fr';
|
const value = 'fr';
|
||||||
await settings.db.executeSql(
|
await settings.setHostLanguage(value);
|
||||||
`INSERT INTO settings (host_language) VALUES (?)`,
|
|
||||||
[value]
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await settings.getHostLanguage();
|
const result = await settings.getHostLanguage();
|
||||||
expect(result).toEqual(value);
|
expect(result).toEqual(value);
|
||||||
@ -39,14 +40,14 @@ describe('Settings', () => {
|
|||||||
|
|
||||||
it('should return null if the host language is not set', async () => {
|
it('should return null if the host language is not set', async () => {
|
||||||
const result = await settings.getHostLanguage();
|
const result = await settings.getHostLanguage();
|
||||||
expect(result).not.toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('setLibretranslateBaseUrl', () => {
|
describe('setLibretranslateBaseUrl', () => {
|
||||||
it('should set the LibreTranslate base URL in the database', async () => {
|
it('should set the LibreTranslate base URL in the database', async () => {
|
||||||
const value = 'https://example.com';
|
const value = 'https://example.com';
|
||||||
await settings.setLibetransalteBaseUrl(value);
|
await settings.setLibretranslateBaseUrl(value);
|
||||||
|
|
||||||
const result = await settings.getLibretranslateBaseUrl();
|
const result = await settings.getLibretranslateBaseUrl();
|
||||||
expect(result).toEqual(value);
|
expect(result).toEqual(value);
|
||||||
@ -54,20 +55,9 @@ describe('Settings', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('getLibretranslateBaseUrl', () => {
|
describe('getLibretranslateBaseUrl', () => {
|
||||||
it('should return the LibreTranslate base URL from the database', async () => {
|
|
||||||
const value = 'https://another-example.com';
|
|
||||||
await settings.db.executeSql(
|
|
||||||
`INSERT INTO settings (libetransalte_base_url) VALUES (?)`,
|
|
||||||
[value]
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await settings.getLibretranslateBaseUrl();
|
|
||||||
expect(result).toEqual(value);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return null if the LibreTranslate base URL is not set', async () => {
|
it('should return null if the LibreTranslate base URL is not set', async () => {
|
||||||
const result = await settings.getLibretranslateBaseUrl();
|
const result = await settings.getLibretranslateBaseUrl();
|
||||||
expect(result).not.toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
@ -31,11 +31,15 @@ export class Message {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type conversation_event_t = "add_message" | "translation_done"
|
||||||
|
type conversation_callback_t = (conversation : Conversation) => any;
|
||||||
|
|
||||||
export class Conversation extends Array<Message> {
|
export class Conversation extends Array<Message> {
|
||||||
|
|
||||||
public onAddMessage? : (conversation : Conversation) => any;
|
public onAddMessage? : (conversation : Conversation) => any;
|
||||||
public onTranslationDone? : (conversation : Conversation) => any;
|
public onTranslationDone? : (conversation : Conversation) => any;
|
||||||
|
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
public translator : Translator,
|
public translator : Translator,
|
||||||
public host : Speaker,
|
public host : Speaker,
|
||||||
@ -44,6 +48,15 @@ export class Conversation extends Array<Message> {
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public on(event : conversation_event_t, callback : conversation_callback_t) {
|
||||||
|
if (event === "add_message") {
|
||||||
|
this.onAddMessage = callback;
|
||||||
|
}
|
||||||
|
if (event === "translation_done") {
|
||||||
|
this.onTranslationDone = callback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public addMessage(speaker : Speaker, text? : string) {
|
public addMessage(speaker : Speaker, text? : string) {
|
||||||
this.push(new Message(this, speaker, text));
|
this.push(new Message(this, speaker, text));
|
||||||
}
|
}
|
||||||
|
@ -1,30 +1,26 @@
|
|||||||
import * as SQLite from 'expo-sqlite';
|
import * as SQLite from "expo-sqlite";
|
||||||
|
import { MIGRATE_UP, MIGRATE_DOWN } from "./migrations";
|
||||||
|
|
||||||
export const MIGRATE_UP = {
|
export async function getDb() {
|
||||||
1: [
|
return await SQLite.openDatabaseAsync("translation_terrace");
|
||||||
`CREATE TABLE IF NOT EXIST settings (
|
|
||||||
host_language TEXT,
|
|
||||||
libretranslate_base_url TEXT,
|
|
||||||
ui_direction INTEGER
|
|
||||||
)`,
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MIGRATE_DOWN = {
|
|
||||||
1: [
|
|
||||||
`DROP TABLE IF EXISTS settings`
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getDb(migrationDirection : "up" | "down" = "up") {
|
export async function migrateDb(direction: "up" | "down" = "up") {
|
||||||
const db = await SQLite.openDatabaseAsync('translation_terrace');
|
|
||||||
|
|
||||||
for (let [migration, statements] of Object.entries(MIGRATE_UP)) {
|
const db = await getDb();
|
||||||
for (let statement of statements) {
|
|
||||||
console.log(statement)
|
const m = direction === "up" ? MIGRATE_UP : MIGRATE_DOWN;
|
||||||
await db.runAsync(statement);
|
|
||||||
}
|
for (let [migration, statements] of Object.entries(m)) {
|
||||||
|
for (let statement of statements) {
|
||||||
|
console.log(statement);
|
||||||
|
try {
|
||||||
|
const result = await db.runAsync(statement);
|
||||||
|
console.log(result);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return db;
|
|
||||||
}
|
}
|
23
app/lib/migrations.ts
Normal file
23
app/lib/migrations.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
|
||||||
|
export const MIGRATE_UP = {
|
||||||
|
1: [
|
||||||
|
`CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
host_language TEXT,
|
||||||
|
libretranslate_base_url TEXT,
|
||||||
|
ui_direction INTEGER,
|
||||||
|
whisper_model TEXT
|
||||||
|
)`,
|
||||||
|
],
|
||||||
|
2: [
|
||||||
|
`CREATE TABLE IF NOT EXISTS whisper_models (
|
||||||
|
model TEXT PRIMARY KEY,
|
||||||
|
bytes_done INTEGER,
|
||||||
|
bytes_total INTEGER
|
||||||
|
)`,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MIGRATE_DOWN = {
|
||||||
|
1: [`DROP TABLE IF EXISTS settings`],
|
||||||
|
2: [`DROP TABLE IF EXISTS whisper_models`],
|
||||||
|
};
|
@ -1,4 +1,6 @@
|
|||||||
import { SQLiteDatabase } from "expo-sqlite";
|
import { SQLiteDatabase } from "expo-sqlite";
|
||||||
|
import FileSystem from "expo-file-system"
|
||||||
|
import { getDb } from "./db";
|
||||||
|
|
||||||
export class Settings {
|
export class Settings {
|
||||||
|
|
||||||
@ -6,6 +8,7 @@ export class Settings {
|
|||||||
"host_language",
|
"host_language",
|
||||||
"libretranslate_base_url",
|
"libretranslate_base_url",
|
||||||
'ui_direction',
|
'ui_direction',
|
||||||
|
"whisper_model",
|
||||||
]
|
]
|
||||||
|
|
||||||
constructor(public db: SQLiteDatabase) {
|
constructor(public db: SQLiteDatabase) {
|
||||||
@ -19,7 +22,7 @@ export class Settings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const query = `
|
const query = `
|
||||||
SELECT ?
|
SELECT ${key}
|
||||||
FROM settings
|
FROM settings
|
||||||
LIMIT 1`
|
LIMIT 1`
|
||||||
const result = await this.db.getFirstAsync(
|
const result = await this.db.getFirstAsync(
|
||||||
@ -33,10 +36,8 @@ LIMIT 1`
|
|||||||
if (!Settings.KEYS.includes(key)) {
|
if (!Settings.KEYS.includes(key)) {
|
||||||
throw new Error(`Invalid setting: '${key}'`)
|
throw new Error(`Invalid setting: '${key}'`)
|
||||||
}
|
}
|
||||||
const statement = `INSERT INTO settings (${key})
|
const statement = `REPLACE INTO settings (${key}) VALUES (?)`
|
||||||
SELECT '?'
|
await this.db.runAsync(statement, value);
|
||||||
ON CONFLICT DO UPDATE SET ${key} = ?`
|
|
||||||
await this.db.runAsync(statement, [value, value]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async setHostLanguage(value: string) {
|
async setHostLanguage(value: string) {
|
||||||
@ -47,12 +48,24 @@ ON CONFLICT DO UPDATE SET ${key} = ?`
|
|||||||
return await this.getValue("host_language")
|
return await this.getValue("host_language")
|
||||||
}
|
}
|
||||||
|
|
||||||
async setLibetransalteBaseUrl(value : string) {
|
async setLibretranslateBaseUrl(value : string) {
|
||||||
await this.setValue("libretranslate_base_url", value)
|
await this.setValue("libretranslate_base_url", value)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLibretranslateBaseUrl() {
|
async getLibretranslateBaseUrl() {
|
||||||
await this.getValue("libretranslate_base_url")
|
return await this.getValue("libretranslate_base_url")
|
||||||
|
}
|
||||||
|
|
||||||
|
async setWhisperModel(value : string) {
|
||||||
|
await this.setValue("whisper_model", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getWhisperModel() {
|
||||||
|
return await this.getValue("whisper_model");
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getDefault() {
|
||||||
|
return new Settings(await getDb())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
213
app/lib/whisper.ts
Normal file
213
app/lib/whisper.ts
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
import { Platform } from "react-native";
|
||||||
|
import * as FileSystem from "expo-file-system";
|
||||||
|
import { File, Paths } from 'expo-file-system/next';
|
||||||
|
import { getDb } from "./db";
|
||||||
|
|
||||||
|
export const WHISPER_MODEL_PATH = Paths.join(FileSystem.bundleDirectory || "file:///", "whisper");
|
||||||
|
export const WHISPER_MODEL_DIR = new File(WHISPER_MODEL_PATH);
|
||||||
|
|
||||||
|
// Thanks to https://medium.com/@fabi.mofar/downloading-and-saving-files-in-react-native-expo-5b3499adda84
|
||||||
|
|
||||||
|
export async function saveFile(
|
||||||
|
uri: string,
|
||||||
|
filename: string,
|
||||||
|
mimetype: string
|
||||||
|
) {
|
||||||
|
if (Platform.OS === "android") {
|
||||||
|
const permissions =
|
||||||
|
await FileSystem.StorageAccessFramework.requestDirectoryPermissionsAsync();
|
||||||
|
|
||||||
|
if (permissions.granted) {
|
||||||
|
const base64 = await FileSystem.readAsStringAsync(uri, {
|
||||||
|
encoding: FileSystem.EncodingType.Base64,
|
||||||
|
});
|
||||||
|
|
||||||
|
await FileSystem.StorageAccessFramework.createFileAsync(
|
||||||
|
permissions.directoryUri,
|
||||||
|
filename,
|
||||||
|
mimetype
|
||||||
|
)
|
||||||
|
.then(async (uri) => {
|
||||||
|
await FileSystem.writeAsStringAsync(uri, base64, {
|
||||||
|
encoding: FileSystem.EncodingType.Base64,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((e) => console.log(e));
|
||||||
|
} else {
|
||||||
|
shareAsync(uri);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
shareAsync(uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function shareAsync(uri: string) {
|
||||||
|
throw new Error("Function not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WHISPER_MODEL_TAGS = ["small", "medium", "large"];
|
||||||
|
export type whisper_model_tag_t = (typeof WHISPER_MODEL_TAGS)[number];
|
||||||
|
|
||||||
|
export const WHISPER_MODELS = {
|
||||||
|
small: {
|
||||||
|
source:
|
||||||
|
"https://huggingface.co/openai/whisper-small/blob/main/pytorch_model.bin",
|
||||||
|
target: "small.bin",
|
||||||
|
label: "Small",
|
||||||
|
},
|
||||||
|
medium: {
|
||||||
|
source:
|
||||||
|
"https://huggingface.co/openai/whisper-medium/blob/main/pytorch_model.bin",
|
||||||
|
target: "medium.bin",
|
||||||
|
label: "Medium",
|
||||||
|
},
|
||||||
|
large: {
|
||||||
|
source:
|
||||||
|
"https://huggingface.co/openai/whisper-large/blob/main/pytorch_model.bin",
|
||||||
|
target: "large.bin",
|
||||||
|
label: "Large",
|
||||||
|
},
|
||||||
|
} as {
|
||||||
|
[key: whisper_model_tag_t]: { source: string; target: string; label: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getWhisperTarget(key : whisper_model_tag_t) {
|
||||||
|
const path = Paths.join(WHISPER_MODEL_DIR, WHISPER_MODELS[key].target);
|
||||||
|
return new File(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
export type download_status =
|
||||||
|
| {
|
||||||
|
status: "not_started" | "complete";
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
status: "in_progress";
|
||||||
|
bytes: {
|
||||||
|
total: number;
|
||||||
|
done: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getModelFileSize(whisper_model: whisper_model_tag_t) {
|
||||||
|
const target = getWhisperTarget(whisper_model)
|
||||||
|
if (!target.exists) return undefined;
|
||||||
|
return target.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param whisper_model The whisper model key to check (e.g. `"small"`)
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function getWhisperDownloadStatus(
|
||||||
|
whisper_model: whisper_model_tag_t
|
||||||
|
): Promise<download_status> {
|
||||||
|
// const files = await FileSystem.readDirectoryAsync("file:///whisper");
|
||||||
|
const result = (await (
|
||||||
|
await getDb()
|
||||||
|
).getFirstSync(
|
||||||
|
`
|
||||||
|
SELECT (bytes_done, total) WHERE model = ?
|
||||||
|
`,
|
||||||
|
[whisper_model]
|
||||||
|
)) as { bytes_done: number; total: number } | undefined;
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
return {
|
||||||
|
status: "not_started",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (result.bytes_done < result.total)
|
||||||
|
return {
|
||||||
|
status: "in_progress",
|
||||||
|
bytes: {
|
||||||
|
done: result.bytes_done,
|
||||||
|
total: result.total,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: "complete",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function whisperFileExists(whisper_model : whisper_model_tag_t) {
|
||||||
|
const target = getWhisperTarget(whisper_model);
|
||||||
|
return target.exists
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DownloadCallback = (arg0 : FileSystem.DownloadProgressData) => any;
|
||||||
|
|
||||||
|
async function updateModelSize(model_label : string, size : number) {
|
||||||
|
const db = await getDb();
|
||||||
|
const query = "INSERT OR REPLACE INTO whisper_models (model, bytes_total) VALUES (?, ?)"
|
||||||
|
const stmt = db.prepareSync(query);
|
||||||
|
stmt.executeSync(model_label, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getExpectedModelSize(model_label : string) : Promise<number | undefined> {
|
||||||
|
const db = await getDb();
|
||||||
|
const query = "SELECT bytes_total FROM whisper_models WHERE model = ?"
|
||||||
|
const stmt = db.prepareSync(query);
|
||||||
|
const curs = stmt.executeSync(model_label);
|
||||||
|
const row = curs.getFirstSync()
|
||||||
|
return row ? row.bytes_total : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initiateWhisperDownload(
|
||||||
|
whisper_model: whisper_model_tag_t,
|
||||||
|
options: {
|
||||||
|
force_redownload?: boolean;
|
||||||
|
onDownload?: DownloadCallback | undefined;
|
||||||
|
} = {
|
||||||
|
force_redownload: false,
|
||||||
|
onDownload: undefined,
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
|
||||||
|
console.debug("Starting download of %s", whisper_model);
|
||||||
|
|
||||||
|
if (!WHISPER_MODEL_DIR.exists) {
|
||||||
|
await FileSystem.makeDirectoryAsync(WHISPER_MODEL_PATH, {
|
||||||
|
intermediates: true,
|
||||||
|
});
|
||||||
|
console.debug("Created %s", WHISPER_MODEL_DIR);
|
||||||
|
}
|
||||||
|
|
||||||
|
const whisperTarget = getWhisperTarget(whisper_model);
|
||||||
|
|
||||||
|
// If the target file exists, delete it.
|
||||||
|
if (whisperTarget.exists) {
|
||||||
|
if (options.force_redownload) {
|
||||||
|
whisperTarget.delete()
|
||||||
|
} else {
|
||||||
|
const expected = await getExpectedModelSize(whisper_model);
|
||||||
|
if (whisperTarget.size === expected) {
|
||||||
|
console.warn("Whisper model for %s already exists", whisper_model);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initiate a new resumable download.
|
||||||
|
const spec = WHISPER_MODELS[whisper_model];
|
||||||
|
|
||||||
|
console.log("Downloading %s", spec.source);
|
||||||
|
|
||||||
|
const resumable = FileSystem.createDownloadResumable(
|
||||||
|
spec.source,
|
||||||
|
whisperTarget.uri,
|
||||||
|
{},
|
||||||
|
// On each data write, update the whisper model download status.
|
||||||
|
// Note that since createDownloadResumable callback only works in the foreground,
|
||||||
|
// a background process will also be updating the file size.
|
||||||
|
async (data) => {
|
||||||
|
console.log("%s: %d bytes of %d", whisperTarget.uri, data.totalBytesWritten, data.totalBytesExpectedToWrite);
|
||||||
|
await updateModelSize(whisper_model, data.totalBytesExpectedToWrite)
|
||||||
|
if (options.onDownload) await options.onDownload(data);
|
||||||
|
},
|
||||||
|
whisperTarget.exists ? whisperTarget.base64() : undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
return resumable;
|
||||||
|
}
|
36
app/service/download.ts
Normal file
36
app/service/download.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
import { Text, View, Button, Platform } from 'react-native';
|
||||||
|
import * as Device from 'expo-device';
|
||||||
|
import * as Notifications from 'expo-notifications';
|
||||||
|
import Constants from 'expo-constants';
|
||||||
|
|
||||||
|
|
||||||
|
export function initNotifications() {
|
||||||
|
Notifications.setNotificationHandler({
|
||||||
|
handleNotification: async () => ({
|
||||||
|
shouldShowAlert: true,
|
||||||
|
shouldPlaySound: true,
|
||||||
|
shouldSetBadge: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendPushNotification(expoPushToken: string) {
|
||||||
|
const message = {
|
||||||
|
to: expoPushToken,
|
||||||
|
sound: 'default',
|
||||||
|
title: 'Original Title',
|
||||||
|
body: 'And here is the body!',
|
||||||
|
data: { someData: 'goes here' },
|
||||||
|
};
|
||||||
|
|
||||||
|
await fetch('https://exp.host/--/api/v2/push/send', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Accept-encoding': 'gzip, deflate',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(message),
|
||||||
|
});
|
||||||
|
}
|
@ -1,72 +1,100 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { ScrollView, Text, TouchableHighlight, View } from "react-native";
|
import { ScrollView, StyleSheet, Text, TouchableHighlight, View } from "react-native";
|
||||||
|
import { useNavigation, Route } from "@react-navigation/native";
|
||||||
import { Conversation, Message } from "@/app/lib/conversation";
|
import { Conversation, Message } from "@/app/lib/conversation";
|
||||||
import MessageBubble from "@/components/ui/MessageBubble";
|
import MessageBubble from "@/components/ui/MessageBubble";
|
||||||
import { WhisperContext } from "whisper.rn";
|
import { CachedTranslator, LanguageServer, language_matrix_entry } from "@/app/i18n/api";
|
||||||
import { NavigationProp, ParamListBase } from "@react-navigation/native";
|
|
||||||
import {
|
|
||||||
CachedTranslator,
|
|
||||||
language_matrix_entry,
|
|
||||||
Translator,
|
|
||||||
} from "@/app/i18n/api";
|
|
||||||
import { getDb } from "@/app/lib/db";
|
|
||||||
import LiveAudioStream from 'react-native-live-audio-stream';
|
|
||||||
|
|
||||||
const lasOptions = {
|
const lasOptions = {
|
||||||
sampleRate: 32000, // default is 44100 but 32000 is adequate for accurate voice recognition
|
sampleRate: 32000, // default is 44100 but 32000 is adequate for accurate voice recognition
|
||||||
channels: 1, // 1 or 2, default 1
|
channels: 1, // 1 or 2, default 1
|
||||||
bitsPerSample: 16, // 8 or 16, default 16
|
bitsPerSample: 16, // 8 or 16, default 16
|
||||||
audioSource: 6, // android only (see below)
|
audioSource: 6, // android only (see below)
|
||||||
bufferSize: 4096 // default is 2048
|
bufferSize: 4096, // default is 2048
|
||||||
};
|
};
|
||||||
// LiveAudioStream.init(lasOptions as any);
|
// LiveAudioStream.init(lasOptions as any);
|
||||||
|
|
||||||
interface ConversationThreadProps {
|
const ConversationThread = ({ route } : {route?: Route<"Conversation", {conversation : Conversation}>}) => {
|
||||||
conversation: Conversation;
|
const navigation = useNavigation();
|
||||||
whisperContext: WhisperContext;
|
|
||||||
onGoBack?: () => any;
|
if (!route) {
|
||||||
}
|
return (<View><Text>Missing Params!</Text></View>)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2. Get the param */
|
||||||
|
const { conversation } = route?.params;
|
||||||
|
|
||||||
const ConversationThread = (p: ConversationThreadProps) => {
|
|
||||||
const [messages, setMessages] = useState<Message[]>([]);
|
const [messages, setMessages] = useState<Message[]>([]);
|
||||||
const [guestSpeak, setGuestSpeak] = useState<string | undefined>();
|
const [guestSpeak, setGuestSpeak] = useState<string | undefined>();
|
||||||
const [guestSpeakLoaded, setGuestSpeakLoaded] = useState<boolean>(false);
|
const [guestSpeakLoaded, setGuestSpeakLoaded] = useState<boolean>(false);
|
||||||
const ct = new CachedTranslator("en", p.conversation.guest.language);
|
const [cachedTranslator, setCachedTranslator] = useState<
|
||||||
|
undefined | CachedTranslator
|
||||||
|
>();
|
||||||
|
|
||||||
|
const [languageLabels, setLanguageLabels] = useState<undefined | {
|
||||||
|
hostNative: {
|
||||||
|
host: string,
|
||||||
|
guest: string,
|
||||||
|
},
|
||||||
|
guestNative: {
|
||||||
|
host: string,
|
||||||
|
guest: string,
|
||||||
|
}
|
||||||
|
}>()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
|
||||||
|
const languageServer = await LanguageServer.getDefault();
|
||||||
|
const languages = await languageServer.fetchLanguages();
|
||||||
|
const cc = new CachedTranslator(
|
||||||
|
"en",
|
||||||
|
conversation.guest.language,
|
||||||
|
languageServer,
|
||||||
|
)
|
||||||
|
setCachedTranslator(cc);
|
||||||
|
setGuestSpeak(await cc.translate("Speak"));
|
||||||
|
const hostLang1 = languages[conversation.host.language].name;
|
||||||
|
const guestLang1 = languages[conversation.host.language].name;
|
||||||
|
const hostLang2 = await cc.translate(languages[conversation.host.language].name);
|
||||||
|
const guestLang2 = await cc.translate(languages[conversation.host.language].name);
|
||||||
|
setLanguageLabels({
|
||||||
|
hostNative: {
|
||||||
|
host: hostLang1,
|
||||||
|
guest: guestLang1,
|
||||||
|
},
|
||||||
|
guestNative: {
|
||||||
|
host: hostLang2,
|
||||||
|
guest: guestLang2,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})();
|
||||||
|
|
||||||
const updateMessages = (c: Conversation) => {
|
const updateMessages = (c: Conversation) => {
|
||||||
setMessages([...c]);
|
setMessages([...c]);
|
||||||
};
|
};
|
||||||
|
|
||||||
p.conversation.onAddMessage = updateMessages;
|
conversation.onAddMessage = updateMessages;
|
||||||
p.conversation.onTranslationDone = updateMessages;
|
conversation.onTranslationDone = updateMessages;
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
p.conversation.onAddMessage = undefined;
|
conversation.onAddMessage = undefined;
|
||||||
p.conversation.onTranslationDone = undefined;
|
conversation.onTranslationDone = undefined;
|
||||||
};
|
};
|
||||||
}, [p.conversation, guestSpeak]);
|
}, [conversation, guestSpeak]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async () => {
|
|
||||||
setGuestSpeak(await ct.translate("Speak"));
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchData();
|
|
||||||
}, [guestSpeak])
|
|
||||||
|
|
||||||
const renderMessages = () =>
|
const renderMessages = () =>
|
||||||
messages.map((message, index) => (
|
messages.map((message, index) => (
|
||||||
<MessageBubble key={index} message={message} />
|
<MessageBubble key={index} message={message} />
|
||||||
));
|
));
|
||||||
|
|
||||||
function onGoBack() {
|
return cachedTranslator ? (
|
||||||
p.onGoBack && p.onGoBack();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={{ flex: 1, flexDirection: "column" }}>
|
<View style={{ flex: 1, flexDirection: "column" }}>
|
||||||
|
{languageLabels && (<View style={styles.languageLabels}>
|
||||||
|
<Text style={styles.nativeHostLabel}>{ languageLabels.hostNative.host } / { languageLabels.hostNative.guest }</Text>
|
||||||
|
<Text style={styles.nativeGuestLabel}>{ languageLabels.guestNative.host } / { languageLabels.guestNative.guest }</Text>
|
||||||
|
</View>)
|
||||||
|
}
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={{
|
style={{
|
||||||
borderColor: "black",
|
borderColor: "black",
|
||||||
@ -85,7 +113,7 @@ const ConversationThread = (p: ConversationThreadProps) => {
|
|||||||
</TouchableHighlight>
|
</TouchableHighlight>
|
||||||
<TouchableHighlight
|
<TouchableHighlight
|
||||||
style={{ backgroundColor: "gray", padding: 3, borderRadius: 5 }}
|
style={{ backgroundColor: "gray", padding: 3, borderRadius: 5 }}
|
||||||
onPress={onGoBack}
|
onPress={navigation.goBack}
|
||||||
>
|
>
|
||||||
<Text style={{ color: "white", fontSize: 30 }}>Go Back</Text>
|
<Text style={{ color: "white", fontSize: 30 }}>Go Back</Text>
|
||||||
</TouchableHighlight>
|
</TouchableHighlight>
|
||||||
@ -98,7 +126,23 @@ const ConversationThread = (p: ConversationThreadProps) => {
|
|||||||
</TouchableHighlight>
|
</TouchableHighlight>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
) : (
|
||||||
|
<View>
|
||||||
|
<Text>Loading...</Text>
|
||||||
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
languageLabels: {
|
||||||
|
|
||||||
|
},
|
||||||
|
nativeHostLabel: {
|
||||||
|
|
||||||
|
},
|
||||||
|
nativeGuestLabel: {
|
||||||
|
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
export default ConversationThread;
|
export default ConversationThread;
|
||||||
|
@ -1,23 +1,27 @@
|
|||||||
import { CachedTranslator, Translator, language_matrix, language_matrix_entry } from "@/app/i18n/api";
|
import { CachedTranslator, LanguageServer, Translator, language_matrix, language_matrix_entry } from "@/app/i18n/api";
|
||||||
import { LIBRETRANSLATE_BASE_URL } from "@/constants/api";
|
import { LIBRETRANSLATE_BASE_URL } from "@/constants/api";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import ISpeakButton from "./ui/ISpeakButton";
|
import ISpeakButton from "./ui/ISpeakButton";
|
||||||
import { LANG_FLAGS } from "@/app/i18n/lang";
|
import { LANG_FLAGS } from "@/app/i18n/lang";
|
||||||
import { ScrollView, StyleSheet, Text, View } from "react-native";
|
import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
|
||||||
import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
|
import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
|
||||||
import { Conversation, Speaker } from "@/app/lib/conversation";
|
import { Conversation, Speaker } from "@/app/lib/conversation";
|
||||||
import { NavigationProp, ParamListBase } from "@react-navigation/native";
|
import { NavigationProp, ParamListBase } from "@react-navigation/native";
|
||||||
|
import { Link, useNavigation } from "expo-router";
|
||||||
|
|
||||||
|
|
||||||
export function LanguageSelection(props: {
|
export function LanguageSelection(props: {
|
||||||
navigation?: NavigationProp<ParamListBase>
|
navigation?: NavigationProp<ParamListBase>
|
||||||
translator?: Translator
|
translator?: Translator
|
||||||
onLangSelected? : (lang : language_matrix_entry) => any
|
onLangSelected?: (lang: language_matrix_entry) => any
|
||||||
}) {
|
}) {
|
||||||
const [languages, setLanguages] = useState<language_matrix | undefined>();
|
const [languages, setLanguages] = useState<language_matrix | undefined>();
|
||||||
const [languagesLoaded, setLanguagesLoaded] = useState<boolean>(false);
|
const [languagesLoaded, setLanguagesLoaded] = useState<boolean>(false);
|
||||||
|
const [translator, setTranslator] = useState<Translator|undefined>();
|
||||||
|
|
||||||
const translator = props.translator || new CachedTranslator("en")
|
const nav = useNavigation();
|
||||||
|
|
||||||
|
const languageServer = new LanguageServer(LIBRETRANSLATE_BASE_URL);
|
||||||
|
|
||||||
function onLangSelected(language: language_matrix_entry) {
|
function onLangSelected(language: language_matrix_entry) {
|
||||||
props.onLangSelected && props.onLangSelected(language)
|
props.onLangSelected && props.onLangSelected(language)
|
||||||
@ -25,36 +29,40 @@ export function LanguageSelection(props: {
|
|||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
// Replace with your actual async data fetching logic
|
// Replace with your actual async data fetching logic
|
||||||
const languages = await translator.fetchLanguages();
|
setTranslator(await CachedTranslator.getDefault());
|
||||||
|
const languageServer = await LanguageServer.getDefault();
|
||||||
|
const languages = await languageServer.fetchLanguages(5000);
|
||||||
setLanguages(languages);
|
setLanguages(languages);
|
||||||
setLanguagesLoaded(true);
|
setLanguagesLoaded(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
error = error as Response;
|
console.error('Error fetching languages from %s: %s', languageServer.baseUrl, error);
|
||||||
console.error('Error fetching data (%d %s): %s', (error as Response).status, (error as Response).statusText, (error as Response).body);
|
|
||||||
}
|
}
|
||||||
};
|
})();
|
||||||
|
|
||||||
fetchData();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView >
|
<View>
|
||||||
<SafeAreaProvider >
|
<Pressable onPress={() => nav.navigate('Settings')}>
|
||||||
<SafeAreaView>
|
<Text>Settings</Text>
|
||||||
{(languages && languagesLoaded) ? Object.entries(languages).filter((l) => (LANG_FLAGS as any)[l[0]] !== undefined).map(
|
</Pressable>
|
||||||
([lang, lang_entry]) => {
|
<ScrollView >
|
||||||
return (
|
<SafeAreaProvider >
|
||||||
<ISpeakButton language={lang_entry} key={lang_entry.code} onLangSelected={onLangSelected} />
|
<SafeAreaView>
|
||||||
);
|
{(languages && languagesLoaded) ? Object.entries(languages).filter((l) => (LANG_FLAGS as any)[l[0]] !== undefined).map(
|
||||||
|
([lang, lang_entry]) => {
|
||||||
|
return (
|
||||||
|
<ISpeakButton language={lang_entry} key={lang_entry.code} onLangSelected={onLangSelected} translator={translator} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
) : <Text>Waiting...</Text>
|
||||||
}
|
}
|
||||||
) : <Text>Waiting...</Text>
|
</SafeAreaView>
|
||||||
}
|
</SafeAreaProvider>
|
||||||
</SafeAreaView>
|
</ScrollView>
|
||||||
</SafeAreaProvider>
|
</View>
|
||||||
</ScrollView>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,89 +1,290 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
// Import necessary packages
|
||||||
import { StyleSheet, View } from "react-native";
|
import React, { useState, useEffect } from "react";
|
||||||
import { NavigationContainer } from "@react-navigation/native";
|
import { View, Text, TextInput, StyleSheet, Pressable } from "react-native"; // Add Picker import
|
||||||
|
import { getDb } from "@/app/lib/db";
|
||||||
import {
|
import { Settings } from "@/app/lib/settings";
|
||||||
default as ReactNativeSettings,
|
import { LanguageServer, fetchWithTimeout } from "@/app/i18n/api";
|
||||||
SettingsElement,
|
import { Picker } from "@react-native-picker/picker";
|
||||||
} from "@mmomtchev/react-native-settings";
|
|
||||||
import { longLang } from "@/app/i18n/lang";
|
import { longLang } from "@/app/i18n/lang";
|
||||||
import { Translator, language_matrix } from "@/app/i18n/api";
|
import FileSystem, { DownloadResumable } from "expo-file-system";
|
||||||
|
import { LIBRETRANSLATE_BASE_URL } from "@/constants/api";
|
||||||
|
import {
|
||||||
|
WHISPER_MODELS,
|
||||||
|
WHISPER_MODEL_DIR,
|
||||||
|
initiateWhisperDownload,
|
||||||
|
download_status,
|
||||||
|
getWhisperDownloadStatus,
|
||||||
|
getWhisperTarget,
|
||||||
|
whisper_model_tag_t,
|
||||||
|
} from "@/app/lib/whisper";
|
||||||
|
import { Paths } from "expo-file-system/next";
|
||||||
|
|
||||||
// We will store the config here
|
type Language = {
|
||||||
const configData: Record<string, string> = {};
|
code: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type LanguageMatrix = {
|
||||||
|
[key: string]: Language;
|
||||||
|
};
|
||||||
|
|
||||||
|
type connection_test_t =
|
||||||
|
| {
|
||||||
|
success: true;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
success: false;
|
||||||
|
error: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SettingsComponent: React.FC = () => {
|
||||||
|
const [hostLanguage, setHostLanguage] = useState<string | null>(null);
|
||||||
|
const [libretranslateBaseUrl, setLibretranslateBaseUrl] = useState<
|
||||||
|
string | null
|
||||||
|
>(null);
|
||||||
|
const [languages, setLanguages] = useState<undefined | LanguageMatrix>();
|
||||||
|
const [isLoaded, setIsLoaded] = useState<boolean>(false);
|
||||||
|
const [whisperModel, setWhisperModel] = useState<
|
||||||
|
undefined | whisper_model_tag_t
|
||||||
|
>();
|
||||||
|
const [downloadStatus, setDownloadStatus] = useState<
|
||||||
|
undefined | download_status
|
||||||
|
>();
|
||||||
|
|
||||||
|
const [langServerConn, setLangServerConn] = useState<
|
||||||
|
undefined | connection_test_t
|
||||||
|
>();
|
||||||
|
const [whisperDownloadProgress, setWhisperDownloadProgress] = useState<
|
||||||
|
FileSystem.DownloadProgressData | undefined
|
||||||
|
>();
|
||||||
|
const [downloader, setDownloader] = useState<DownloadResumable | undefined>();
|
||||||
|
|
||||||
|
const fillHostLanguageOptions = async () => {
|
||||||
|
const settings = await Settings.getDefault();
|
||||||
|
const hostLang = await settings.getHostLanguage();
|
||||||
|
setHostLanguage(hostLang || "en");
|
||||||
|
const langServer = new LanguageServer(
|
||||||
|
libretranslateBaseUrl || LIBRETRANSLATE_BASE_URL
|
||||||
|
);
|
||||||
|
// Fetch languages from API
|
||||||
|
try {
|
||||||
|
const langData = await langServer.fetchLanguages();
|
||||||
|
setLanguages(langData);
|
||||||
|
setLangServerConn({ success: true });
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Got an error fetching: %s", err);
|
||||||
|
setLangServerConn({
|
||||||
|
success: false,
|
||||||
|
error: `Could not connect to ${libretranslateBaseUrl}: ${err}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
// Fetch the database connection
|
||||||
|
// const db = await getDb("down");
|
||||||
|
const settings = await Settings.getDefault();
|
||||||
|
|
||||||
|
await fillHostLanguageOptions();
|
||||||
|
|
||||||
|
console.log("Fetched settings");
|
||||||
|
|
||||||
|
// Get the current settings values
|
||||||
|
const libretranslateUrl =
|
||||||
|
(await settings.getLibretranslateBaseUrl()) || LIBRETRANSLATE_BASE_URL;
|
||||||
|
setLibretranslateBaseUrl(libretranslateUrl);
|
||||||
|
console.log("libretranslate url = %s", libretranslateUrl);
|
||||||
|
try {
|
||||||
|
const wModel = await settings.getWhisperModel();
|
||||||
|
setWhisperModel(wModel || "small");
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// setWhisperModel(wModel);
|
||||||
|
setIsLoaded(true);
|
||||||
|
// console.log("Set is loaded: %s", isLoaded);
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Check for whether a model is currently downloading and set the status.
|
||||||
|
setInterval(async () => {
|
||||||
|
if (!whisperModel) return null;
|
||||||
|
const dlStatus = await getWhisperDownloadStatus(whisperModel);
|
||||||
|
setDownloadStatus(dlStatus);
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
setInterval(async () => {
|
||||||
|
if (!libretranslateBaseUrl) return;
|
||||||
|
try {
|
||||||
|
const resp = await fetchWithTimeout(
|
||||||
|
libretranslateBaseUrl + "/languages",
|
||||||
|
{
|
||||||
|
method: "HEAD",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
5000
|
||||||
|
);
|
||||||
|
if (resp.status !== 200) {
|
||||||
|
throw new Error(resp.statusText);
|
||||||
|
}
|
||||||
|
setLangServerConn({ success: true });
|
||||||
|
} catch (err) {
|
||||||
|
setLangServerConn({
|
||||||
|
success: false,
|
||||||
|
error: `Could not connect to ${libretranslateBaseUrl}: ${err}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
setInterval(async () => {
|
||||||
|
const settings = await Settings.getDefault();
|
||||||
|
await settings.setHostLanguage(hostLanguage || "en");
|
||||||
|
await settings.setLibretranslateBaseUrl(
|
||||||
|
libretranslateBaseUrl || LIBRETRANSLATE_BASE_URL
|
||||||
|
);
|
||||||
|
await settings.setWhisperModel(whisperModel || "small");
|
||||||
|
}, 1000);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const doReadownload = async () => {
|
||||||
|
if (!whisperModel) return;
|
||||||
|
await initiateWhisperDownload(whisperModel, {
|
||||||
|
force_redownload: true,
|
||||||
|
onDownload: setWhisperDownloadProgress,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const doDownload = async () => {
|
||||||
|
if (!whisperModel) return;
|
||||||
|
try {
|
||||||
|
setDownloader(
|
||||||
|
await initiateWhisperDownload(whisperModel, {
|
||||||
|
onDownload: setWhisperDownloadProgress,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await downloader?.downloadAsync();
|
||||||
|
console.log("completed download");
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHostLanguageChange = async (value: string) => {
|
||||||
|
setHostLanguage(value);
|
||||||
|
|
||||||
|
// Fetch the database connection
|
||||||
|
const db = await getDb();
|
||||||
|
const settings = new Settings(db);
|
||||||
|
|
||||||
|
// Save the updated setting value
|
||||||
|
await settings.setHostLanguage(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLibretranslateBaseUrlChange = async (value: string) => {
|
||||||
|
setLibretranslateBaseUrl(value);
|
||||||
|
|
||||||
|
const settings = await Settings.getDefault();
|
||||||
|
|
||||||
|
// Save the updated setting value
|
||||||
|
await settings.setLibretranslateBaseUrl(value);
|
||||||
|
|
||||||
|
await fillHostLanguageOptions();
|
||||||
|
};
|
||||||
|
|
||||||
|
return isLoaded ? (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Text style={styles.label}>Host Language:</Text>
|
||||||
|
<Picker
|
||||||
|
selectedValue={hostLanguage || ""}
|
||||||
|
style={{ height: 50, width: "100%" }}
|
||||||
|
onValueChange={handleHostLanguageChange}
|
||||||
|
accessibilityHint="hostLanguage"
|
||||||
|
>
|
||||||
|
{languages &&
|
||||||
|
Object.entries(languages).map((lang) => (
|
||||||
|
<Picker.Item key={lang[0]} label={lang[1].name} value={lang[0]} />
|
||||||
|
))}
|
||||||
|
</Picker>
|
||||||
|
|
||||||
|
<Text style={styles.label}>LibreTranslate Base URL:</Text>
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
value={libretranslateBaseUrl || LIBRETRANSLATE_BASE_URL}
|
||||||
|
onChangeText={handleLibretranslateBaseUrlChange}
|
||||||
|
accessibilityHint="libretranslate base url"
|
||||||
|
/>
|
||||||
|
{langServerConn &&
|
||||||
|
(langServerConn.success ? (
|
||||||
|
<Text>Success connecting to {libretranslateBaseUrl}</Text>
|
||||||
|
) : (
|
||||||
|
<Text>
|
||||||
|
Error connecting to {libretranslateBaseUrl}: {langServerConn.error}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
<Picker
|
||||||
|
selectedValue={whisperModel || ""}
|
||||||
|
style={{ height: 50, width: "100%" }}
|
||||||
|
onValueChange={setWhisperModel}
|
||||||
|
accessibilityHint="language"
|
||||||
|
>
|
||||||
|
{Object.entries(WHISPER_MODELS).map(([key, { label }]) => (
|
||||||
|
<Picker.Item key={key} label={label} value={key} />
|
||||||
|
))}
|
||||||
|
</Picker>
|
||||||
|
<View>
|
||||||
|
{ /* If there's a downloader, that means we're in the middle of a download */}
|
||||||
|
{downloader && whisperDownloadProgress && (
|
||||||
|
<Text>
|
||||||
|
{whisperDownloadProgress.totalBytesWritten} of {whisperDownloadProgress.totalBytesExpectedToWrite}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<Pressable onPress={doDownload} style={styles.button}>
|
||||||
|
<Text style={styles.buttonText}>Download</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View>
|
||||||
|
<Text>Loading ...</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create styles for the component
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
button: {
|
||||||
|
backgroundColor: "blue",
|
||||||
|
flexDirection: "row",
|
||||||
|
display: "flex",
|
||||||
|
flexShrink: 1,
|
||||||
|
padding: 20,
|
||||||
|
alignItems: "center",
|
||||||
|
alignContent: "center",
|
||||||
|
},
|
||||||
|
buttonText: {
|
||||||
|
color: "white",
|
||||||
|
alignSelf: "center",
|
||||||
|
},
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: "#fff",
|
padding: 20,
|
||||||
justifyContent: "center",
|
},
|
||||||
padding: "1.5%",
|
label: {
|
||||||
|
fontSize: 16,
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
height: 40,
|
||||||
|
borderColor: "gray",
|
||||||
|
borderWidth: 1,
|
||||||
|
marginBottom: 20,
|
||||||
|
paddingHorizontal: 8,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Retrieve a conf item or return the default
|
export default SettingsComponent;
|
||||||
const confGet = (key: string, def: string): string => configData[key] || def;
|
|
||||||
|
|
||||||
// Store a conf item
|
|
||||||
const confSet = (key: string, value: string): void => {
|
|
||||||
configData[key] = value;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Choose from a list item
|
|
||||||
const intelligence: Record<string, string> = {
|
|
||||||
L: "Low",
|
|
||||||
M: "Medium",
|
|
||||||
H: "High",
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export default function Settings() {
|
|
||||||
// Simply pass the schema here
|
|
||||||
// It integrates in your existing `NavigationContainer` or `Screen`
|
|
||||||
const [translator, setTranslator] = useState<Translator>(new Translator("en"))
|
|
||||||
const [languages, setLanguages] = useState<language_matrix | undefined>();
|
|
||||||
const [languagesLoaded, setLanguagesLoaded] = useState<boolean>(false);
|
|
||||||
|
|
||||||
|
|
||||||
// This is the configuration schema
|
|
||||||
const settings: SettingsElement[] = [
|
|
||||||
{
|
|
||||||
label: "LibreTranslate server",
|
|
||||||
type: "string",
|
|
||||||
get: confGet.bind(null, "@ltServer", "http://localhost:5000"),
|
|
||||||
set: confSet.bind(null, "@ltServer"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Host Language",
|
|
||||||
type: "enum",
|
|
||||||
// You can override the way the value is displayed
|
|
||||||
values: ["en", "es", "fr"],
|
|
||||||
display: (v : string) => {
|
|
||||||
return longLang(v);
|
|
||||||
},
|
|
||||||
get: confGet.bind(null, "@hostLanguage", ""),
|
|
||||||
set: confSet.bind(null, "@hostLanguage"),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
// Replace with your actual async data fetching logic
|
|
||||||
const languages = await translator.fetchLanguages();
|
|
||||||
setLanguages(languages);
|
|
||||||
setLanguagesLoaded(true);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching data:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NavigationContainer>
|
|
||||||
<View style={styles.container}>
|
|
||||||
<ReactNativeSettings settings={settings} />
|
|
||||||
</View>
|
|
||||||
</NavigationContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
42
components/TTNavStack.tsx
Normal file
42
components/TTNavStack.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import SettingsComponent from "@/components/Settings";
|
||||||
|
import { LanguageSelection } from "@/components/LanguageSelection";
|
||||||
|
import {
|
||||||
|
createNativeStackNavigator,
|
||||||
|
NativeStackNavigationProp,
|
||||||
|
} from "@react-navigation/native-stack";
|
||||||
|
import { useNavigation } from "@react-navigation/native";
|
||||||
|
import ConversationThread from "@/components/ConversationThread";
|
||||||
|
import { language_matrix_entry, Translator } from "@/app/i18n/api";
|
||||||
|
import { Conversation } from "@/app/lib/conversation";
|
||||||
|
import { Settings } from "@/app/lib/settings";
|
||||||
|
import { RootStackParamList } from "@/navigation.types";
|
||||||
|
|
||||||
|
const Stack = createNativeStackNavigator();
|
||||||
|
|
||||||
|
export default function TTNavStack() {
|
||||||
|
const nav = useNavigation<NativeStackNavigationProp<RootStackParamList, "Conversation">>()
|
||||||
|
|
||||||
|
async function onLangSelected(lang: language_matrix_entry) {
|
||||||
|
const settings = await Settings.getDefault();
|
||||||
|
const hostLanguage = await settings.getHostLanguage();
|
||||||
|
const conversation = new Conversation(
|
||||||
|
await Translator.getDefault(lang.code),
|
||||||
|
{ id: "host", language: hostLanguage },
|
||||||
|
{ id: "guest", language: lang.code }
|
||||||
|
);
|
||||||
|
nav.navigate("ConversationThread", { conversation });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack.Navigator initialRouteName="LanguageSelection">
|
||||||
|
<Stack.Screen name="LanguageSelection">
|
||||||
|
{(props) => (
|
||||||
|
<LanguageSelection {...props} onLangSelected={onLangSelected} />
|
||||||
|
)}
|
||||||
|
</Stack.Screen>
|
||||||
|
<Stack.Screen name="ConversationThread" component={ConversationThread} />
|
||||||
|
<Stack.Screen name="Settings" component={SettingsComponent} />
|
||||||
|
</Stack.Navigator>
|
||||||
|
);
|
||||||
|
}
|
61
components/__tests__/index.spec.tsx
Normal file
61
components/__tests__/index.spec.tsx
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
jest.mock("@/app/i18n/api", () => require("../../__mocks__/api.ts"));
|
||||||
|
import { renderRouter} from 'expo-router/testing-library';
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
act,
|
||||||
|
fireEvent,
|
||||||
|
render,
|
||||||
|
screen,
|
||||||
|
waitFor,
|
||||||
|
} from "@testing-library/react-native";
|
||||||
|
import {
|
||||||
|
NavigationContainer,
|
||||||
|
createNavigationContainerRef,
|
||||||
|
} from "@react-navigation/native";
|
||||||
|
import TTNavStack from "../TTNavStack";
|
||||||
|
|
||||||
|
describe("Navigation", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Reset the navigation state before each test
|
||||||
|
jest.clearAllMocks();
|
||||||
|
jest.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Navigates to ConversationThread on language selection", async () => {
|
||||||
|
const MockComponent = jest.fn(() => <TTNavStack />);
|
||||||
|
renderRouter(
|
||||||
|
{
|
||||||
|
index: MockComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
initialUrl: '/',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const languageSelectionText = await waitFor(() =>
|
||||||
|
screen.getByText(/.*I Speak French.*/i)
|
||||||
|
);
|
||||||
|
act(() => {
|
||||||
|
fireEvent.press(languageSelectionText);
|
||||||
|
});
|
||||||
|
expect(await screen.findByText("Conversation Thread")).toBeOnTheScreen();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Navigates to Settings on settings selection", async () => {
|
||||||
|
const MockComponent = jest.fn(() => <TTNavStack />);
|
||||||
|
renderRouter(
|
||||||
|
{
|
||||||
|
index: MockComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
initialUrl: '/',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const settingsButton = await waitFor(() =>
|
||||||
|
screen.getByText(/.*Settings.*/i)
|
||||||
|
);
|
||||||
|
fireEvent.press(settingsButton);
|
||||||
|
expect(await waitFor(() => screen.getByText(/Settings/i))).toBeOnTheScreen();
|
||||||
|
// expect(waitFor(() => screen.getByText(/Settings/i))).toBeTruthy()
|
||||||
|
expect(screen.getByText("Settings")).toBeOnTheScreen();
|
||||||
|
});
|
||||||
|
});
|
@ -1,121 +1,142 @@
|
|||||||
// import AsyncStorage from '@react-native-async-storage/async-storage';
|
// import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import { CachedTranslator, Translator, language_matrix_entry } from "@/app/i18n/api"
|
import {
|
||||||
import { longLang } from "@/app/i18n/lang"
|
CachedTranslator,
|
||||||
import React, { useEffect, useRef, useState } from "react"
|
Translator,
|
||||||
import { Button, Image, ImageBackground, Pressable, StyleSheet, TouchableOpacity, View } from "react-native"
|
language_matrix_entry,
|
||||||
import { Text } from 'react-native';
|
} from "@/app/i18n/api";
|
||||||
|
import { longLang } from "@/app/i18n/lang";
|
||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Image,
|
||||||
|
ImageBackground,
|
||||||
|
Pressable,
|
||||||
|
StyleSheet,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from "react-native";
|
||||||
|
import { Text } from "react-native";
|
||||||
import CountryFlag from "react-native-country-flag";
|
import CountryFlag from "react-native-country-flag";
|
||||||
import { chooseCountry } from '@/app/i18n/countries';
|
import { chooseCountry } from "@/app/i18n/countries";
|
||||||
|
|
||||||
type ISpeakButtonProps = {
|
type ISpeakButtonProps = {
|
||||||
language: language_matrix_entry,
|
language: language_matrix_entry;
|
||||||
translator?: Translator,
|
translator?: Translator;
|
||||||
onLangSelected?: (lang : language_matrix_entry) => any | Promise<any>,
|
onLangSelected?: (lang: language_matrix_entry) => any | Promise<any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function iSpeak(language: language_matrix_entry) {
|
||||||
|
return `I speak ${language.name}.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function iSpeak(language : language_matrix_entry) {
|
async function iSpeakTr(
|
||||||
return `I speak ${language.name}.`
|
translator: CachedTranslator,
|
||||||
}
|
targetLang: language_matrix_entry
|
||||||
|
) {
|
||||||
async function iSpeakTr(translator : CachedTranslator, targetLang : language_matrix_entry) {
|
const sourceStr = iSpeak(targetLang);
|
||||||
const sourceStr = iSpeak(targetLang)
|
return await translator.translate(sourceStr, targetLang.code);
|
||||||
return await translator.translate(sourceStr, targetLang.code);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_FLAGS = {
|
const DEFAULT_FLAGS = {
|
||||||
"en": ["us", "gb"],
|
en: ["us", "gb"],
|
||||||
// "sq": ["al"],
|
// "sq": ["al"],
|
||||||
"ar": ["ae"],
|
ar: ["ae"],
|
||||||
"es": ["es"],
|
es: ["es"],
|
||||||
"pt": ["pt"],
|
pt: ["pt"],
|
||||||
"ru": ["ru"],
|
ru: ["ru"],
|
||||||
"it": ["it"],
|
it: ["it"],
|
||||||
"ir": ["ie"],
|
ir: ["ie"],
|
||||||
"sk": ["sk"],
|
sk: ["sk"],
|
||||||
"ro": ["ro"],
|
ro: ["ro"],
|
||||||
"ja": ["jp"],
|
ja: ["jp"],
|
||||||
"ko": ["kp", "kr"],
|
ko: ["kp", "kr"],
|
||||||
"el": ["gr"],
|
el: ["gr"],
|
||||||
"fr": ["fr"],
|
fr: ["fr"],
|
||||||
"de": ["de"],
|
de: ["de"],
|
||||||
"nl": ["nl"],
|
nl: ["nl"],
|
||||||
"cz": ["cz"],
|
cz: ["cz"],
|
||||||
"uk": ["ua"],
|
uk: ["ua"],
|
||||||
"he": ["il"],
|
he: ["il"],
|
||||||
"hi": ["in"],
|
hi: ["in"],
|
||||||
"gl": ["es"],
|
gl: ["es"],
|
||||||
"fa": ["ir"],
|
fa: ["ir"],
|
||||||
"ur": ["pk"],
|
ur: ["pk"],
|
||||||
"ga": ["ie"],
|
ga: ["ie"],
|
||||||
"eo": ["es"]
|
eo: ["es"],
|
||||||
}
|
};
|
||||||
|
|
||||||
const ISpeakButton = (props : ISpeakButtonProps) => {
|
const ISpeakButton = (props: ISpeakButtonProps) => {
|
||||||
|
const [title, setTitle] = useState<string | undefined>();
|
||||||
|
const [titleLoaded, setTitleLoaded] = useState<boolean>(false);
|
||||||
|
const [translator, setTranslator] = useState<Translator | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
const [title, setTitle] = useState<string | undefined>();
|
useEffect(() => {
|
||||||
const [titleLoaded, setTitleLoaded] = useState<boolean>(false);
|
(async function () {
|
||||||
const translator = props.translator || new CachedTranslator("en");
|
const tr = props.translator || (await CachedTranslator.getDefault());
|
||||||
|
if (!tr) {
|
||||||
|
console.error("Failed to construct cachedTranslator");
|
||||||
|
}
|
||||||
|
setTranslator(tr);
|
||||||
|
try {
|
||||||
|
// Replace with your actual async data fetching logic
|
||||||
|
const title2 = await iSpeakTr(tr, props.language);
|
||||||
|
setTitle(title2);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching data from %s: %s", tr.languageServer.baseUrl, error);
|
||||||
|
} finally {
|
||||||
|
setTitleLoaded(true);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
const countries =
|
||||||
const fetchData = async () => {
|
// @ts-ignore
|
||||||
try {
|
DEFAULT_FLAGS[props.language.code] || chooseCountry(props.language.code);
|
||||||
// Replace with your actual async data fetching logic
|
|
||||||
const title = await iSpeakTr(translator, props.language);
|
|
||||||
setTitle(title);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching data:', error);
|
|
||||||
} finally {
|
|
||||||
setTitleLoaded(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const countries = DEFAULT_FLAGS[props.language.code] || chooseCountry(props.language.code);
|
|
||||||
|
|
||||||
return (
|
|
||||||
title ? (
|
|
||||||
<TouchableOpacity style={styles.button} onPress={() => props.onLangSelected && props.onLangSelected(props.language)}>
|
|
||||||
<View>
|
|
||||||
<View style={styles.flag}>
|
|
||||||
{countries &&
|
|
||||||
countries.map( c => {
|
|
||||||
return <CountryFlag isoCode={c} size={25} key={c}/> }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</View>
|
|
||||||
<View style={styles.iSpeak}>
|
|
||||||
<Text style={styles.iSpeakText}>{ title }</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
) : (
|
|
||||||
<Text>Loading...</Text>
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
return title ? (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.button}
|
||||||
|
onPress={() =>
|
||||||
|
props.onLangSelected && props.onLangSelected(props.language)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<View>
|
||||||
|
<View style={styles.flag}>
|
||||||
|
{countries &&
|
||||||
|
countries.map((c) => {
|
||||||
|
return <CountryFlag isoCode={c} size={25} key={c} />;
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<Text style={styles.iSpeakText}>{title}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
) : (
|
||||||
|
<Text>Loading...</Text>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
button: {
|
button: {
|
||||||
width: "20%",
|
width: "20%",
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
borderColor: "white",
|
borderColor: "white",
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderStyle: "solid",
|
borderStyle: "solid",
|
||||||
height: 110,
|
height: 110,
|
||||||
alignSelf: "flex-start",
|
alignSelf: "flex-start",
|
||||||
margin: 8,
|
margin: 8,
|
||||||
},
|
},
|
||||||
flag: {
|
flag: {},
|
||||||
},
|
iSpeak: {
|
||||||
iSpeak: {
|
textAlign: "center",
|
||||||
textAlign: "center",
|
},
|
||||||
},
|
iSpeakText: {
|
||||||
iSpeakText: {
|
textAlign: "center",
|
||||||
textAlign: "center"
|
},
|
||||||
}
|
});
|
||||||
})
|
|
||||||
|
|
||||||
export default ISpeakButton;
|
export default ISpeakButton;
|
||||||
|
@ -9,58 +9,35 @@ type MessageProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MessageBubble = (props: MessageProps) => {
|
const MessageBubble = (props: MessageProps) => {
|
||||||
const [text, setText] = useState(props.message.text);
|
|
||||||
const [translatedText, setTranslatedText] = useState<string|undefined>();
|
|
||||||
const [isTranslating, setIsTranslating] = useState<boolean>(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
|
|
||||||
props.message.onTextUpdate = (message: Message) => {
|
|
||||||
setText(message.text);
|
|
||||||
}
|
|
||||||
|
|
||||||
props.message.onTextDone = async (message: Message) => {
|
|
||||||
setIsTranslating(true);
|
|
||||||
await props.message.translate()
|
|
||||||
}
|
|
||||||
|
|
||||||
props.message.onTranslationDone = (message: Message) => {
|
|
||||||
if (!message.translation) throw new Error("Missing translation");
|
|
||||||
setTranslatedText(message.translation);
|
|
||||||
setIsTranslating(false);
|
|
||||||
}
|
|
||||||
}, [props.message])
|
|
||||||
|
|
||||||
const spId = props.message.speaker.id
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView>
|
||||||
{text && (
|
{props.message.text && (
|
||||||
<Text>{text}</Text>
|
<Text>{props.message.text}</Text>
|
||||||
)}
|
)}
|
||||||
{translatedText &&
|
{props.message.translation &&
|
||||||
<Text>{translatedText}</Text>
|
<Text accessibilityHint="translation">{props.message.translation}</Text>
|
||||||
}
|
}
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// const bubbleStyle = StyleSheet.create({
|
const bubbleStyle = StyleSheet.create({
|
||||||
// host: {
|
host: {
|
||||||
|
|
||||||
// },
|
},
|
||||||
// guest: {
|
guest: {
|
||||||
|
|
||||||
// },
|
},
|
||||||
// })
|
})
|
||||||
|
|
||||||
// const textStyles = StyleSheet.create({
|
const textStyles = StyleSheet.create({
|
||||||
// native: {
|
native: {
|
||||||
|
|
||||||
// },
|
},
|
||||||
// translation: {
|
translation: {
|
||||||
|
|
||||||
// },
|
},
|
||||||
// });
|
});
|
||||||
|
|
||||||
export default MessageBubble;
|
export default MessageBubble;
|
0
components/ui/WhisperDownloadButton.tsx
Normal file
0
components/ui/WhisperDownloadButton.tsx
Normal file
33
components/ui/WhisperDownloader.tsx
Normal file
33
components/ui/WhisperDownloader.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { Settings } from "@/app/lib/settings"
|
||||||
|
import { WHISPER_MODELS } from "@/app/lib/whisper"
|
||||||
|
import { Picker } from "@react-native-picker/picker"
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { Pressable, View } from "react-native"
|
||||||
|
|
||||||
|
const WhisperDownloader = () => {
|
||||||
|
|
||||||
|
const [whisperModel, setWhisperModel] = useState<string|undefined>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const settings = await Settings.getDefault();
|
||||||
|
setWhisperModel((await settings.getWhisperModel()) || "small");
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<Picker
|
||||||
|
selectedValue={whisperModel || ""}
|
||||||
|
style={{ height: 50, width: "100%" }}
|
||||||
|
onValueChange={setWhisperModel}
|
||||||
|
accessibilityHint="whisper model"
|
||||||
|
>
|
||||||
|
{Object.entries(WHISPER_MODELS).map(([key, { label }]) => (
|
||||||
|
<Picker.Item key={key} label={label} value={key} />
|
||||||
|
))}
|
||||||
|
</Picker>
|
||||||
|
|
||||||
|
<WhisperDownloadButton whisperModel={whisperModel} />
|
||||||
|
|
||||||
|
<WhisperDownloadInfo whisperModel={whisperModel} />
|
||||||
|
</View>
|
||||||
|
}
|
@ -1,12 +1,59 @@
|
|||||||
import React, { act } from 'react';
|
import React, { act } from 'react';
|
||||||
import { render, screen } from '@testing-library/react-native'
|
import { render, screen, waitFor } from '@testing-library/react-native'
|
||||||
import MessageBubble from '@/components/ui/MessageBubble';
|
import MessageBubble from '@/components/ui/MessageBubble';
|
||||||
import { Conversation, Speaker } from '@/app/lib/conversation';
|
import { Conversation, Speaker } from '@/app/lib/conversation';
|
||||||
import {Translator} from '@/app/i18n/api';
|
import {LanguageServer, Translator, language_matrix} from '@/app/i18n/api';
|
||||||
import { View } from 'react-native';
|
import { View } from 'react-native';
|
||||||
|
import { LIBRETRANSLATE_BASE_URL } from '@/constants/api';
|
||||||
|
|
||||||
|
|
||||||
|
jest.mock("@/app/i18n/api", () => {
|
||||||
|
class LanguageServer {
|
||||||
|
fetchLanguages = () => {
|
||||||
|
return {
|
||||||
|
"en": {
|
||||||
|
code: "en",
|
||||||
|
name: "English",
|
||||||
|
targets: [
|
||||||
|
"fr",
|
||||||
|
"es"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fr": {
|
||||||
|
code: "fr",
|
||||||
|
name: "French",
|
||||||
|
targets: [
|
||||||
|
"en",
|
||||||
|
"es"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"es": {
|
||||||
|
code: "es",
|
||||||
|
name: "Spanish",
|
||||||
|
targets: [
|
||||||
|
"en",
|
||||||
|
"fr"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
} as language_matrix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Translator {
|
||||||
|
translate = jest.fn((text : string, target : string) => {
|
||||||
|
if (text.match(/Hello, how are you\?/i)) {
|
||||||
|
return "Hola, ¿cómo estás?"
|
||||||
|
}
|
||||||
|
return "??? Huh ???"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
LanguageServer,
|
||||||
|
Translator,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
describe('Message Component', () => {
|
describe('Message Component', () => {
|
||||||
const translator = new Translator('en', 'es');
|
const translator = new Translator('en', 'es', new LanguageServer(LIBRETRANSLATE_BASE_URL));
|
||||||
|
|
||||||
const host : Speaker = {id : "host", language : "en"}
|
const host : Speaker = {id : "host", language : "en"}
|
||||||
const guest : Speaker = {id : "guest", language: "es"}
|
const guest : Speaker = {id : "guest", language: "es"}
|
||||||
@ -21,8 +68,8 @@ describe('Message Component', () => {
|
|||||||
it('renders the message text correctly', async () => {
|
it('renders the message text correctly', async () => {
|
||||||
conversation.addMessage(host, "Hello, World!");
|
conversation.addMessage(host, "Hello, World!");
|
||||||
const message = conversation[0];
|
const message = conversation[0];
|
||||||
render(<View></View>);
|
// render(<View></View>);
|
||||||
// render(<MessageBubble message={message} />);
|
render(<MessageBubble message={message} />);
|
||||||
expect(await screen.findByText(message.text as string)).toBeOnTheScreen();
|
expect(await screen.findByText(message.text as string)).toBeOnTheScreen();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -32,7 +79,7 @@ describe('Message Component', () => {
|
|||||||
await conversation.translateLast();
|
await conversation.translateLast();
|
||||||
|
|
||||||
render(<MessageBubble message={conversation[0]} />);
|
render(<MessageBubble message={conversation[0]} />);
|
||||||
expect(await screen.findByText(translatedText)).toBeOnTheScreen();
|
expect(screen.getByAccessibilityHint("translation")).toBeOnTheScreen();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('widget still renders pre-translation', async () => {
|
it('widget still renders pre-translation', async () => {
|
||||||
@ -42,10 +89,8 @@ describe('Message Component', () => {
|
|||||||
render(<MessageBubble message={conversation[0]} />);
|
render(<MessageBubble message={conversation[0]} />);
|
||||||
expect(screen.getByText(text)).toBeOnTheScreen();
|
expect(screen.getByText(text)).toBeOnTheScreen();
|
||||||
// expect(screen.getByText(translatedText)).not.toBeOnTheScreen();
|
// expect(screen.getByText(translatedText)).not.toBeOnTheScreen();
|
||||||
await act(async () => {
|
// await conversation.translateLast();
|
||||||
await conversation.translateLast();
|
// expect(await screen.findByText(text)).toBeOnTheScreen();
|
||||||
});
|
// expect(await screen.findByText(translatedText)).toBeOnTheScreen();
|
||||||
expect(await screen.findByText(text)).toBeOnTheScreen();
|
|
||||||
expect(await screen.findByText(translatedText)).toBeOnTheScreen();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
110
components/ui/__tests__/Settings.spec.tsx
Normal file
110
components/ui/__tests__/Settings.spec.tsx
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import React, { Dispatch } from "react";
|
||||||
|
import { render, screen, fireEvent, act } from "@testing-library/react-native";
|
||||||
|
import SettingsComponent from "@/components/Settings";
|
||||||
|
import { language_matrix } from "@/app/i18n/api";
|
||||||
|
import { Settings } from "@/app/lib/settings";
|
||||||
|
import { getDb, migrateDb } from "@/app/lib/db";
|
||||||
|
|
||||||
|
const RENDER_TIME = 1000;
|
||||||
|
|
||||||
|
jest.mock("@/app/i18n/api", () => {
|
||||||
|
class LanguageServer {
|
||||||
|
fetchLanguages = () => {
|
||||||
|
return {
|
||||||
|
"en": {
|
||||||
|
code: "en",
|
||||||
|
name: "English",
|
||||||
|
targets: [
|
||||||
|
"fr",
|
||||||
|
"es"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fr": {
|
||||||
|
code: "fr",
|
||||||
|
name: "French",
|
||||||
|
targets: [
|
||||||
|
"en",
|
||||||
|
"es"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"es": {
|
||||||
|
code: "es",
|
||||||
|
name: "Spanish",
|
||||||
|
targets: [
|
||||||
|
"en",
|
||||||
|
"fr"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
} as language_matrix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Translator {
|
||||||
|
translate = jest.fn((text : string, target : string) => {
|
||||||
|
return "Hola, como estas?"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
LanguageServer,
|
||||||
|
Translator,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("SettingsComponent", () => {
|
||||||
|
beforeEach(async() => {
|
||||||
|
await migrateDb();
|
||||||
|
const settings = await Settings.getDefault();
|
||||||
|
await settings.setHostLanguage("en");
|
||||||
|
await settings.setLibretranslateBaseUrl("https://example.com");
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
jest.useRealTimers()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("renders correctly with initial settings", async () => {
|
||||||
|
render(<SettingsComponent />);
|
||||||
|
jest.advanceTimersByTime(RENDER_TIME);
|
||||||
|
screen.debug();
|
||||||
|
|
||||||
|
// Wait for the component to fetch and display the initial settings
|
||||||
|
await screen.findByText(/Host Language:/i);
|
||||||
|
await screen.findByText(/LibreTranslate Base URL:/i);
|
||||||
|
|
||||||
|
// expect(screen.getByDisplayValue("English")).toBeTruthy();
|
||||||
|
expect(screen.getByAccessibilityHint("libretranslate base url")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("updates host language setting when input changes", async () => {
|
||||||
|
render(<SettingsComponent />);
|
||||||
|
|
||||||
|
// Wait for the component to fetch and display the initial settings
|
||||||
|
await screen.findByText(/Host Language:/i);
|
||||||
|
await screen.findByText(/LibreTranslate Base URL:/i);
|
||||||
|
|
||||||
|
// Change the host language input value
|
||||||
|
const picker = screen.getByAccessibilityHint("hostLanguage");
|
||||||
|
fireEvent(picker, "onvalueChange", "es");
|
||||||
|
expect(picker.props.selectedIndex).toStrictEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("updates LibreTranslate base URL setting when input changes", async () => {
|
||||||
|
render(<SettingsComponent />);
|
||||||
|
|
||||||
|
jest.advanceTimersByTime(RENDER_TIME)
|
||||||
|
screen.debug();
|
||||||
|
|
||||||
|
// Wait for the component to fetch and display the initial settings
|
||||||
|
await screen.findByText(/Host Language:/i);
|
||||||
|
await screen.findByText(/LibreTranslate Base URL:/i);
|
||||||
|
|
||||||
|
// Change the LibreTranslate base URL input value
|
||||||
|
fireEvent.changeText(screen.getByAccessibilityHint("libretranslate base url"), "http://new-example.com");
|
||||||
|
jest.advanceTimersByTime(RENDER_TIME);
|
||||||
|
|
||||||
|
expect(screen.getByAccessibilityHint("libretranslate base url")).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
87
jestSetup.ts
87
jestSetup.ts
@ -1,24 +1,71 @@
|
|||||||
// jestSetup.ts
|
// jestSetup.ts
|
||||||
|
|
||||||
jest.mock('expo-sqlite', () => {
|
// include this line for mocking react-native-gesture-handler
|
||||||
return {
|
import 'react-native-gesture-handler/jestSetup';
|
||||||
openDatabaseAsync: async (name: string) => {
|
|
||||||
const {DatabaseSync} = require("node:sqlite")
|
|
||||||
const db = new DatabaseSync(':memory:');
|
|
||||||
|
|
||||||
return {
|
jest.mock("expo-sqlite", () => {
|
||||||
closeAsync: jest.fn(() => db.close()),
|
const { DatabaseSync } = require("node:sqlite");
|
||||||
executeSql: jest.fn((sql: string) => db.exec(sql)),
|
const db = new DatabaseSync(":memory:");
|
||||||
runAsync: jest.fn(async (sql: string, params = []) => {
|
|
||||||
const stmt = db.prepare(sql);
|
const { MIGRATE_UP } = jest.requireActual("./app/lib/migrations");
|
||||||
stmt.run(params)
|
|
||||||
}),
|
const openDatabaseAsync = async (name: string) => {
|
||||||
getFirstAsync: jest.fn(async (sql : string, params = []) => {
|
return {
|
||||||
const stmt = db.prepare(sql)
|
closeAsync: jest.fn(() => db.close()),
|
||||||
const result = stmt.run(params);
|
executeSql: jest.fn((sql: string) => db.exec(sql)),
|
||||||
return stmt.all(params)[0]
|
runAsync: jest.fn(async (sql: string, params = []) => {
|
||||||
})
|
for (let m of Object.values(MIGRATE_UP)) {
|
||||||
};
|
for (let stmt of m) {
|
||||||
},
|
const s = db.prepare(stmt);
|
||||||
|
s.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const stmt = db.prepare(sql);
|
||||||
|
// console.log("Running %s with %s", sql, params);
|
||||||
|
try {
|
||||||
|
stmt.run(params);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(
|
||||||
|
`running ${sql} with params ${JSON.stringify(params)}: ${e}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
getFirstAsync: jest.fn(async (sql: string, params = []) => {
|
||||||
|
for (let m of Object.values(MIGRATE_UP)) {
|
||||||
|
for (let stmt of m) {
|
||||||
|
const s = db.prepare(stmt);
|
||||||
|
s.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const stmt = db.prepare(sql);
|
||||||
|
// const result = stmt.run(...params);
|
||||||
|
return stmt.get(params);
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
});
|
};
|
||||||
|
return {
|
||||||
|
migrateDb: async (direction: "up" | "down" = "up") => {
|
||||||
|
const db = await openDatabaseAsync("translation_terrace");
|
||||||
|
for (let m of Object.values(MIGRATE_UP)) {
|
||||||
|
for (let stmt of m) {
|
||||||
|
await db.executeSql(stmt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openDatabaseAsync,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// include this section and the NativeAnimatedHelper section for mocking react-native-reanimated
|
||||||
|
jest.mock('react-native-reanimated', () => {
|
||||||
|
const Reanimated = require('react-native-reanimated/mock');
|
||||||
|
|
||||||
|
// The mock for `call` immediately calls the callback which is incorrect
|
||||||
|
// So we override it with a no-op
|
||||||
|
Reanimated.default.call = () => {};
|
||||||
|
|
||||||
|
return Reanimated;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Silence the warning: Animated: `useNativeDriver` is not supported because the native animated module is missing
|
||||||
|
// jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper');
|
11
navigation.types.ts
Normal file
11
navigation.types.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// navigation.types.ts
|
||||||
|
|
||||||
|
import { ParamListBase } from '@react-navigation/native';
|
||||||
|
import { Conversation } from '@/app/lib/conversation';
|
||||||
|
|
||||||
|
export type RootStackParamList = {
|
||||||
|
LanguageSelection: undefined;
|
||||||
|
ConversationThread: undefined;
|
||||||
|
Settings: undefined;
|
||||||
|
Conversation: { conversation?: Conversation };
|
||||||
|
};
|
316
package-lock.json
generated
316
package-lock.json
generated
@ -12,17 +12,22 @@
|
|||||||
"@expo/vector-icons": "^14.0.4",
|
"@expo/vector-icons": "^14.0.4",
|
||||||
"@mmomtchev/react-native-settings": "^1.1.0",
|
"@mmomtchev/react-native-settings": "^1.1.0",
|
||||||
"@react-native-async-storage/async-storage": "^2.1.0",
|
"@react-native-async-storage/async-storage": "^2.1.0",
|
||||||
|
"@react-native-picker/picker": "^2.11.0",
|
||||||
"@react-navigation/bottom-tabs": "^7.2.0",
|
"@react-navigation/bottom-tabs": "^7.2.0",
|
||||||
"@react-navigation/native": "^7.0.14",
|
|
||||||
"@react-navigation/native-stack": "^7.2.0",
|
"@react-navigation/native-stack": "^7.2.0",
|
||||||
"expo": "~52.0.28",
|
"expo": "~52.0.28",
|
||||||
|
"expo-background-fetch": "~13.0.5",
|
||||||
"expo-blur": "~14.0.3",
|
"expo-blur": "~14.0.3",
|
||||||
"expo-constants": "~17.0.5",
|
"expo-constants": "~17.0.6",
|
||||||
|
"expo-device": "~7.0.2",
|
||||||
|
"expo-file-system": "^18.0.10",
|
||||||
"expo-font": "~13.0.3",
|
"expo-font": "~13.0.3",
|
||||||
"expo-haptics": "~14.0.1",
|
"expo-haptics": "~14.0.1",
|
||||||
"expo-linking": "~7.0.5",
|
"expo-linking": "~7.0.5",
|
||||||
|
"expo-notifications": "~0.29.13",
|
||||||
"expo-router": "~4.0.17",
|
"expo-router": "~4.0.17",
|
||||||
"expo-screen-orientation": "~8.0.4",
|
"expo-screen-orientation": "~8.0.4",
|
||||||
|
"expo-sharing": "^13.0.1",
|
||||||
"expo-splash-screen": "~0.29.21",
|
"expo-splash-screen": "~0.29.21",
|
||||||
"expo-sqlite": "~15.1.2",
|
"expo-sqlite": "~15.1.2",
|
||||||
"expo-status-bar": "~2.0.1",
|
"expo-status-bar": "~2.0.1",
|
||||||
@ -48,10 +53,13 @@
|
|||||||
"@babel/core": "^7.26.7",
|
"@babel/core": "^7.26.7",
|
||||||
"@babel/preset-typescript": "^7.26.0",
|
"@babel/preset-typescript": "^7.26.0",
|
||||||
"@jest/globals": "^29.7.0",
|
"@jest/globals": "^29.7.0",
|
||||||
|
"@react-navigation/native": "^7.0.14",
|
||||||
|
"@react-navigation/stack": "^7.1.1",
|
||||||
"@testing-library/react-native": "^13.0.1",
|
"@testing-library/react-native": "^13.0.1",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/react": "~18.3.18",
|
"@types/react": "~18.3.18",
|
||||||
"@types/react-native-sqlite-storage": "^6.0.5",
|
"@types/react-native-sqlite-storage": "^6.0.5",
|
||||||
|
"@types/react-navigation": "^3.0.8",
|
||||||
"@types/react-test-renderer": "^18.3.1",
|
"@types/react-test-renderer": "^18.3.1",
|
||||||
"babel-jest": "^29.7.0",
|
"babel-jest": "^29.7.0",
|
||||||
"babel-plugin-module-resolver": "^5.0.2",
|
"babel-plugin-module-resolver": "^5.0.2",
|
||||||
@ -2518,15 +2526,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@expo/config": {
|
"node_modules/@expo/config": {
|
||||||
"version": "10.0.8",
|
"version": "10.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@expo/config/-/config-10.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@expo/config/-/config-10.0.10.tgz",
|
||||||
"integrity": "sha512-RaKwi8e6PbkMilRexdsxObLMdQwxhY6mlgel+l/eW+IfIw8HEydSU0ERlzYUjlGJxHLHUXe4rC2vw8FEvaowyQ==",
|
"integrity": "sha512-wI9/iam3Irk99ADGM/FyD7YrrEibIZXR4huSZiU5zt9o3dASOKhqepiNJex4YPiktLfKhYrpSEJtwno1g0SrgA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "~7.10.4",
|
"@babel/code-frame": "~7.10.4",
|
||||||
"@expo/config-plugins": "~9.0.14",
|
"@expo/config-plugins": "~9.0.15",
|
||||||
"@expo/config-types": "^52.0.3",
|
"@expo/config-types": "^52.0.4",
|
||||||
"@expo/json-file": "^9.0.1",
|
"@expo/json-file": "^9.0.2",
|
||||||
"deepmerge": "^4.3.1",
|
"deepmerge": "^4.3.1",
|
||||||
"getenv": "^1.0.0",
|
"getenv": "^1.0.0",
|
||||||
"glob": "^10.4.2",
|
"glob": "^10.4.2",
|
||||||
@ -2928,9 +2936,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@expo/json-file": {
|
"node_modules/@expo/json-file": {
|
||||||
"version": "9.0.1",
|
"version": "9.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-9.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-9.0.2.tgz",
|
||||||
"integrity": "sha512-ZVPhbbEBEwafPCJ0+kI25O2Iivt3XKHEKAADCml1q2cmOIbQnKgLyn8DpOJXqWEyRQr/VWS+hflBh8DU2YFSqg==",
|
"integrity": "sha512-yAznIUrybOIWp3Uax7yRflB0xsEpvIwIEqIjao9SGi2Gaa+N0OamWfe0fnXBSWF+2zzF4VvqwT4W5zwelchfgw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "~7.10.4",
|
"@babel/code-frame": "~7.10.4",
|
||||||
@ -3387,6 +3395,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@ide/backoff": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ide/backoff/-/backoff-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@isaacs/cliui": {
|
"node_modules/@isaacs/cliui": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||||
@ -4045,6 +4059,19 @@
|
|||||||
"react-native": "^0.0.0-0 || >=0.65 <1.0"
|
"react-native": "^0.0.0-0 || >=0.65 <1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@react-native-picker/picker": {
|
||||||
|
"version": "2.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-2.11.0.tgz",
|
||||||
|
"integrity": "sha512-QuZU6gbxmOID5zZgd/H90NgBnbJ3VV6qVzp6c7/dDrmWdX8S0X5YFYgDcQFjE3dRen9wB9FWnj2VVdPU64adSg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"workspaces": [
|
||||||
|
"example"
|
||||||
|
],
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@react-native/assets-registry": {
|
"node_modules/@react-native/assets-registry": {
|
||||||
"version": "0.76.6",
|
"version": "0.76.6",
|
||||||
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.76.6.tgz",
|
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.76.6.tgz",
|
||||||
@ -4682,6 +4709,25 @@
|
|||||||
"nanoid": "3.3.8"
|
"nanoid": "3.3.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@react-navigation/stack": {
|
||||||
|
"version": "7.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-navigation/stack/-/stack-7.1.1.tgz",
|
||||||
|
"integrity": "sha512-CBTKQlIkELp05zRiTAv5Pa7OMuCpKyBXcdB3PGMN2Mm55/5MkDsA1IaZorp/6TsVCdllITD6aTbGX/HA/88A6w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-navigation/elements": "^2.2.5",
|
||||||
|
"color": "^4.2.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@react-navigation/native": "^7.0.14",
|
||||||
|
"react": ">= 18.2.0",
|
||||||
|
"react-native": "*",
|
||||||
|
"react-native-gesture-handler": ">= 2.0.0",
|
||||||
|
"react-native-safe-area-context": ">= 4.0.0",
|
||||||
|
"react-native-screens": ">= 4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@remix-run/node": {
|
"node_modules/@remix-run/node": {
|
||||||
"version": "2.15.3",
|
"version": "2.15.3",
|
||||||
"resolved": "https://registry.npmjs.org/@remix-run/node/-/node-2.15.3.tgz",
|
"resolved": "https://registry.npmjs.org/@remix-run/node/-/node-2.15.3.tgz",
|
||||||
@ -5056,6 +5102,17 @@
|
|||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/react-native": {
|
||||||
|
"version": "0.72.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.72.8.tgz",
|
||||||
|
"integrity": "sha512-St6xA7+EoHN5mEYfdWnfYt0e8u6k2FR0P9s2arYgakQGFgU1f9FlPrIEcj0X24pLCF5c5i3WVuLCUdiCYHmOoA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-native/virtualized-lists": "^0.72.4",
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/react-native-sqlite-storage": {
|
"node_modules/@types/react-native-sqlite-storage": {
|
||||||
"version": "6.0.5",
|
"version": "6.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-native-sqlite-storage/-/react-native-sqlite-storage-6.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-native-sqlite-storage/-/react-native-sqlite-storage-6.0.5.tgz",
|
||||||
@ -5063,6 +5120,31 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/react-native/node_modules/@react-native/virtualized-lists": {
|
||||||
|
"version": "0.72.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.72.8.tgz",
|
||||||
|
"integrity": "sha512-J3Q4Bkuo99k7mu+jPS9gSUSgq+lLRSI/+ahXNwV92XgJ/8UgOTxu2LPwhJnBk/sQKxq7E8WkZBnBiozukQMqrw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"invariant": "^2.2.4",
|
||||||
|
"nullthrows": "^1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/react-navigation": {
|
||||||
|
"version": "3.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-navigation/-/react-navigation-3.0.8.tgz",
|
||||||
|
"integrity": "sha512-r8UQvBmOz7XjPE8AHTHh0SThGqModhQtSsntkmob7rczhueJIqDwBOgsEn54SJa25XzD/KBlelAWeVZ7+Ggm8A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/react-test-renderer": {
|
"node_modules/@types/react-test-renderer": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-18.3.1.tgz",
|
||||||
@ -5604,6 +5686,19 @@
|
|||||||
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
|
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/assert": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.2",
|
||||||
|
"is-nan": "^1.3.2",
|
||||||
|
"object-is": "^1.1.5",
|
||||||
|
"object.assign": "^4.1.4",
|
||||||
|
"util": "^0.12.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ast-types": {
|
"node_modules/ast-types": {
|
||||||
"version": "0.15.2",
|
"version": "0.15.2",
|
||||||
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.15.2.tgz",
|
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.15.2.tgz",
|
||||||
@ -5883,6 +5978,12 @@
|
|||||||
"@babel/core": "^7.0.0"
|
"@babel/core": "^7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/badgin": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/badgin/-/badgin-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-NQGA7LcfCpSzIbGRbkgjgdWkjy7HI+Th5VLxTJfW5EeaAf3fnS+xWQaQOCYiny+q6QSvxqoSO04vCx+4u++EJw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
@ -7050,6 +7151,23 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/define-properties": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"define-data-property": "^1.0.1",
|
||||||
|
"has-property-descriptors": "^1.0.0",
|
||||||
|
"object-keys": "^1.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/del": {
|
"node_modules/del": {
|
||||||
"version": "6.1.1",
|
"version": "6.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz",
|
||||||
@ -7716,6 +7834,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-application": {
|
||||||
|
"version": "6.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-application/-/expo-application-6.0.2.tgz",
|
||||||
|
"integrity": "sha512-qcj6kGq3mc7x5yIb5KxESurFTJCoEKwNEL34RdPEvTB/xhl7SeVZlu05sZBqxB1V4Ryzq/LsCb7NHNfBbb3L7A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-asset": {
|
"node_modules/expo-asset": {
|
||||||
"version": "11.0.3",
|
"version": "11.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-11.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-11.0.3.tgz",
|
||||||
@ -7733,6 +7860,18 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-background-fetch": {
|
||||||
|
"version": "13.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-background-fetch/-/expo-background-fetch-13.0.5.tgz",
|
||||||
|
"integrity": "sha512-rLRM+rYDRT0fA0Oaet5ibJK3nKVRkfdjXjISHxjUvIE4ktD9pE+UjAPPdjTXZ5CkNb3JyNNhQGJEGpdJC2HLKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"expo-task-manager": "~12.0.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-blur": {
|
"node_modules/expo-blur": {
|
||||||
"version": "14.0.3",
|
"version": "14.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/expo-blur/-/expo-blur-14.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/expo-blur/-/expo-blur-14.0.3.tgz",
|
||||||
@ -7745,12 +7884,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo-constants": {
|
"node_modules/expo-constants": {
|
||||||
"version": "17.0.5",
|
"version": "17.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-17.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-17.0.6.tgz",
|
||||||
"integrity": "sha512-6SHXh32jCB+vrp2TRDNkoGoM421eOBPZIXX9ixI0hKKz71tIjD+LMr/P+rGUd/ks312MP3WK3j5vcYYPkCD8tQ==",
|
"integrity": "sha512-rl3/hBIIkh4XDkCEMzGpmY6kWj2G1TA4Mq2joeyzoFBepJuGjqnGl7phf/71sTTgamQ1hmhKCLRNXMpRqzzqxw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/config": "~10.0.8",
|
"@expo/config": "~10.0.9",
|
||||||
"@expo/env": "~0.4.1"
|
"@expo/env": "~0.4.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@ -7758,6 +7897,44 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-device": {
|
||||||
|
"version": "7.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-device/-/expo-device-7.0.2.tgz",
|
||||||
|
"integrity": "sha512-0PkTixE4Qi8VQBjixnj4aw2f6vE4tUZH7GK8zHROGKlBypZKcWmsA+W/Vp3RC5AyREjX71pO/hjKTSo/vF0E2w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ua-parser-js": "^0.7.33"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/expo-device/node_modules/ua-parser-js": {
|
||||||
|
"version": "0.7.40",
|
||||||
|
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.40.tgz",
|
||||||
|
"integrity": "sha512-us1E3K+3jJppDBa3Tl0L3MOJiGhe1C6P0+nIvQAFYbxlMAx0h81eOwLmU57xgqToduDDPx3y5QsdjPfDu+FgOQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/ua-parser-js"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paypal",
|
||||||
|
"url": "https://paypal.me/faisalman"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/faisalman"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"ua-parser-js": "script/cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-file-system": {
|
"node_modules/expo-file-system": {
|
||||||
"version": "18.0.10",
|
"version": "18.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-18.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-18.0.10.tgz",
|
||||||
@ -7927,6 +8104,26 @@
|
|||||||
"invariant": "^2.2.4"
|
"invariant": "^2.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-notifications": {
|
||||||
|
"version": "0.29.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-0.29.13.tgz",
|
||||||
|
"integrity": "sha512-GHye6XeI1uEeVttJO/hGwUyA5cgQsxR3mi5q37yOE7cZN3cMj36pIfEEmjXEr0nWIWSzoJ0w8c2QxNj5xfP1pA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@expo/image-utils": "^0.6.4",
|
||||||
|
"@ide/backoff": "^1.0.0",
|
||||||
|
"abort-controller": "^3.0.0",
|
||||||
|
"assert": "^2.0.0",
|
||||||
|
"badgin": "^1.1.5",
|
||||||
|
"expo-application": "~6.0.2",
|
||||||
|
"expo-constants": "~17.0.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*",
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-router": {
|
"node_modules/expo-router": {
|
||||||
"version": "4.0.17",
|
"version": "4.0.17",
|
||||||
"resolved": "https://registry.npmjs.org/expo-router/-/expo-router-4.0.17.tgz",
|
"resolved": "https://registry.npmjs.org/expo-router/-/expo-router-4.0.17.tgz",
|
||||||
@ -7990,6 +8187,15 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-sharing": {
|
||||||
|
"version": "13.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-sharing/-/expo-sharing-13.0.1.tgz",
|
||||||
|
"integrity": "sha512-qych3Nw65wlFcnzE/gRrsdtvmdV0uF4U4qVMZBJYPG90vYyWh2QM9rp1gVu0KWOBc7N8CC2dSVYn4/BXqJy6Xw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-splash-screen": {
|
"node_modules/expo-splash-screen": {
|
||||||
"version": "0.29.21",
|
"version": "0.29.21",
|
||||||
"resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-0.29.21.tgz",
|
"resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-0.29.21.tgz",
|
||||||
@ -8068,6 +8274,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-task-manager": {
|
||||||
|
"version": "12.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-task-manager/-/expo-task-manager-12.0.5.tgz",
|
||||||
|
"integrity": "sha512-tDHOBYORA6wuO32NWwz/Egrvn+N6aANHAa0DFs+01VK/IJZfU9D05ZN6M5XYIlZv5ll4GSX1wJZyTCY0HZGapw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"unimodules-app-loader": "~5.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-web-browser": {
|
"node_modules/expo-web-browser": {
|
||||||
"version": "14.0.2",
|
"version": "14.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-14.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-14.0.2.tgz",
|
||||||
@ -9322,6 +9541,22 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-nan": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz",
|
||||||
|
"integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.0",
|
||||||
|
"define-properties": "^1.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-number": {
|
"node_modules/is-number": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||||
@ -12338,6 +12573,51 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/object-is": {
|
||||||
|
"version": "1.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
|
||||||
|
"integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.7",
|
||||||
|
"define-properties": "^1.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/object-keys": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/object.assign": {
|
||||||
|
"version": "4.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
|
||||||
|
"integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.8",
|
||||||
|
"call-bound": "^1.0.3",
|
||||||
|
"define-properties": "^1.2.1",
|
||||||
|
"es-object-atoms": "^1.0.0",
|
||||||
|
"has-symbols": "^1.1.0",
|
||||||
|
"object-keys": "^1.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/on-finished": {
|
"node_modules/on-finished": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||||
@ -15722,6 +16002,12 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/unimodules-app-loader": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/unimodules-app-loader/-/unimodules-app-loader-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-JI4dUMOovvLrZ1U/mrQrR73cxGH26H7NpfBxwE0hk59CBOyHO4YYpliI3hPSGgZzt+YEy2VZR6nrspSUXY8jyw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/unique-filename": {
|
"node_modules/unique-filename": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz",
|
||||||
|
29
package.json
29
package.json
@ -4,7 +4,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "jest --watch --coverage=false --changedSince=origin/main",
|
"test": "jest --watch --coverage=false --changedSince=origin/main",
|
||||||
"testDebug": "jest -o --watch --coverage=false",
|
"testDebug": "jest -o --watch --bail --coverage=false",
|
||||||
"testFinal": "jest",
|
"testFinal": "jest",
|
||||||
"updateSnapshots": "jest -u --coverage=false",
|
"updateSnapshots": "jest -u --coverage=false",
|
||||||
"start": "expo start",
|
"start": "expo start",
|
||||||
@ -19,17 +19,22 @@
|
|||||||
"@expo/vector-icons": "^14.0.4",
|
"@expo/vector-icons": "^14.0.4",
|
||||||
"@mmomtchev/react-native-settings": "^1.1.0",
|
"@mmomtchev/react-native-settings": "^1.1.0",
|
||||||
"@react-native-async-storage/async-storage": "^2.1.0",
|
"@react-native-async-storage/async-storage": "^2.1.0",
|
||||||
|
"@react-native-picker/picker": "^2.11.0",
|
||||||
"@react-navigation/bottom-tabs": "^7.2.0",
|
"@react-navigation/bottom-tabs": "^7.2.0",
|
||||||
"@react-navigation/native": "^7.0.14",
|
|
||||||
"@react-navigation/native-stack": "^7.2.0",
|
"@react-navigation/native-stack": "^7.2.0",
|
||||||
"expo": "~52.0.28",
|
"expo": "~52.0.28",
|
||||||
|
"expo-background-fetch": "~13.0.5",
|
||||||
"expo-blur": "~14.0.3",
|
"expo-blur": "~14.0.3",
|
||||||
"expo-constants": "~17.0.5",
|
"expo-constants": "~17.0.6",
|
||||||
|
"expo-device": "~7.0.2",
|
||||||
|
"expo-file-system": "^18.0.10",
|
||||||
"expo-font": "~13.0.3",
|
"expo-font": "~13.0.3",
|
||||||
"expo-haptics": "~14.0.1",
|
"expo-haptics": "~14.0.1",
|
||||||
"expo-linking": "~7.0.5",
|
"expo-linking": "~7.0.5",
|
||||||
|
"expo-notifications": "~0.29.13",
|
||||||
"expo-router": "~4.0.17",
|
"expo-router": "~4.0.17",
|
||||||
"expo-screen-orientation": "~8.0.4",
|
"expo-screen-orientation": "~8.0.4",
|
||||||
|
"expo-sharing": "^13.0.1",
|
||||||
"expo-splash-screen": "~0.29.21",
|
"expo-splash-screen": "~0.29.21",
|
||||||
"expo-sqlite": "~15.1.2",
|
"expo-sqlite": "~15.1.2",
|
||||||
"expo-status-bar": "~2.0.1",
|
"expo-status-bar": "~2.0.1",
|
||||||
@ -53,32 +58,28 @@
|
|||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"preset": "jest-expo",
|
"preset": "jest-expo",
|
||||||
|
"testPathIgnorePatterns": [
|
||||||
|
".ollama"
|
||||||
|
],
|
||||||
"transformIgnorePatterns": [
|
"transformIgnorePatterns": [
|
||||||
"node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@sentry/react-native|native-base|react-native-svg)"
|
"node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@sentry/react-native|native-base|react-native-svg)"
|
||||||
],
|
],
|
||||||
"collectCoverage": true,
|
|
||||||
"collectCoverageFrom": [
|
|
||||||
"**/*.{ts,tsx,js,jsx}",
|
|
||||||
"!**/coverage/**",
|
|
||||||
"!**/node_modules/**",
|
|
||||||
"!**/babel.config.js",
|
|
||||||
"!**/expo-env.d.ts",
|
|
||||||
"!**/.expo/**"
|
|
||||||
],
|
|
||||||
"automock": false,
|
"automock": false,
|
||||||
"setupFilesAfterEnv": [
|
"setupFilesAfterEnv": [
|
||||||
"<rootDir>/jestSetup.ts"
|
"<rootDir>/jestSetup.ts"
|
||||||
],
|
]
|
||||||
"testTimeout": 10000
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.26.7",
|
"@babel/core": "^7.26.7",
|
||||||
"@babel/preset-typescript": "^7.26.0",
|
"@babel/preset-typescript": "^7.26.0",
|
||||||
"@jest/globals": "^29.7.0",
|
"@jest/globals": "^29.7.0",
|
||||||
|
"@react-navigation/native": "^7.0.14",
|
||||||
|
"@react-navigation/stack": "^7.1.1",
|
||||||
"@testing-library/react-native": "^13.0.1",
|
"@testing-library/react-native": "^13.0.1",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/react": "~18.3.18",
|
"@types/react": "~18.3.18",
|
||||||
"@types/react-native-sqlite-storage": "^6.0.5",
|
"@types/react-native-sqlite-storage": "^6.0.5",
|
||||||
|
"@types/react-navigation": "^3.0.8",
|
||||||
"@types/react-test-renderer": "^18.3.1",
|
"@types/react-test-renderer": "^18.3.1",
|
||||||
"babel-jest": "^29.7.0",
|
"babel-jest": "^29.7.0",
|
||||||
"babel-plugin-module-resolver": "^5.0.2",
|
"babel-plugin-module-resolver": "^5.0.2",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user