give up on downloader idea. Use file from assets.
This commit is contained in:
parent
123933d459
commit
e61fb43ee3
2
android/app/proguard-rules.pro
vendored
2
android/app/proguard-rules.pro
vendored
@ -12,3 +12,5 @@
|
||||
-keep class com.facebook.react.turbomodule.** { *; }
|
||||
|
||||
# Add any project specific keep options here:
|
||||
# whisper.rn
|
||||
-keep class com.rnwhisper.** { *; }
|
@ -138,7 +138,7 @@ export class WhisperFile {
|
||||
return Paths.join(WHISPER_MODEL_PATH, this.targetFileName as string);
|
||||
}
|
||||
|
||||
get targetPartPath () {
|
||||
get targetPartPath() {
|
||||
return this.targetPath + ".part";
|
||||
}
|
||||
|
||||
@ -146,6 +146,10 @@ export class WhisperFile {
|
||||
return new File(this.targetPath);
|
||||
}
|
||||
|
||||
get targetPartFile() {
|
||||
return new File(this.targetPartPath);
|
||||
}
|
||||
|
||||
async getTargetInfo() {
|
||||
return await FileSystem.getInfoAsync(this.targetPath);
|
||||
}
|
||||
@ -156,7 +160,9 @@ export class WhisperFile {
|
||||
|
||||
async updateTargetExistence() {
|
||||
this.does_target_exist = (await this.getTargetInfo()).exists;
|
||||
console.log("Determining if %s exists: %s", this.targetPath, this.does_target_exist)
|
||||
this.does_part_target_exist = (await this.getTargetPartInfo()).exists;
|
||||
console.log("Determining if %s exists: %s", this.targetPartPath, this.does_part_target_exist)
|
||||
}
|
||||
|
||||
public async getTargetSha() {
|
||||
@ -171,7 +177,6 @@ export class WhisperFile {
|
||||
});
|
||||
const data = strToArrBuf(strData);
|
||||
|
||||
|
||||
const digest = await Crypto.digest(
|
||||
Crypto.CryptoDigestAlgorithm.SHA256,
|
||||
data
|
||||
@ -192,14 +197,15 @@ export class WhisperFile {
|
||||
|
||||
delete(ignoreErrors = true) {
|
||||
try {
|
||||
this.targetFile.delete();
|
||||
this.does_target_exist && this.targetFile.delete();
|
||||
this.does_part_target_exist && this.targetPartFile.delete();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
if (!ignoreErrors) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
console.debug("Created %s", WHISPER_MODEL_DIR);
|
||||
console.debug("Successfully deleted %s and %s", this.targetPartPath, this.targetPath);
|
||||
}
|
||||
|
||||
get modelUrl() {
|
||||
@ -260,9 +266,9 @@ export class WhisperFile {
|
||||
onData?: DownloadCallback | undefined;
|
||||
onComplete?: CompletionCallback | undefined;
|
||||
} = {
|
||||
onData: undefined,
|
||||
onComplete: undefined,
|
||||
}
|
||||
onData: undefined,
|
||||
onComplete: undefined,
|
||||
}
|
||||
) {
|
||||
await this.syncHfMetadata();
|
||||
|
||||
@ -277,23 +283,26 @@ export class WhisperFile {
|
||||
// If it exists, load the existing data.
|
||||
await this.updateTargetExistence();
|
||||
|
||||
if (this.does_part_target_exist) {
|
||||
options.onComplete && options.onComplete(this)
|
||||
}
|
||||
|
||||
try {
|
||||
const existingData = this.does_target_exist
|
||||
? await FileSystem.readAsStringAsync(this.targetPath, {
|
||||
encoding: FileSystem.EncodingType.Base64,
|
||||
})
|
||||
: undefined;
|
||||
// const existingData = this.does_target_exist
|
||||
// ? await FileSystem.readAsStringAsync(this.targetPath, {
|
||||
// encoding: FileSystem.EncodingType.Base64,
|
||||
// })
|
||||
// : undefined;
|
||||
|
||||
// Create the resumable.
|
||||
return FileSystem.createDownloadResumable(
|
||||
this.modelUrl,
|
||||
this.targetPath,
|
||||
this.targetPartPath,
|
||||
{},
|
||||
async (data: FileSystem.DownloadProgressData) => {
|
||||
console.log(
|
||||
"Downloading %s: %d of %d",
|
||||
this.targetPartPath,
|
||||
data.totalBytesExpectedToWrite,
|
||||
data.totalBytesWritten
|
||||
);
|
||||
|
||||
// console.debug("yes, I'm still downloading");
|
||||
try {
|
||||
this.download_data = data;
|
||||
@ -304,7 +313,7 @@ export class WhisperFile {
|
||||
try {
|
||||
await this.syncHfMetadata();
|
||||
} catch (err) {
|
||||
console.error("Failed to update HuggingFace metadata: %s", err)
|
||||
console.error("Failed to update HuggingFace metadata: %s", err);
|
||||
}
|
||||
|
||||
// try {
|
||||
@ -316,13 +325,17 @@ export class WhisperFile {
|
||||
try {
|
||||
await this.updateTargetExistence();
|
||||
} catch (err) {
|
||||
console.error("Failed to update target existence: %s", err)
|
||||
console.error("Failed to update target existence: %s", err);
|
||||
}
|
||||
if (options.onData) await options.onData(this);
|
||||
|
||||
if (data.totalBytesExpectedToWrite === 0) {
|
||||
console.debug("Finalizing; copying from %s -> %s", this.targetPartPath, this.targetPath);
|
||||
await FileSystem.copyAsync({
|
||||
if (data.totalBytesExpectedToWrite === data.totalBytesWritten) {
|
||||
console.debug(
|
||||
"Finalizing; copying from %s -> %s",
|
||||
this.targetPartPath,
|
||||
this.targetPath
|
||||
);
|
||||
await FileSystem.moveAsync({
|
||||
from: this.targetPartPath,
|
||||
to: this.targetPath,
|
||||
});
|
||||
@ -330,7 +343,7 @@ export class WhisperFile {
|
||||
options.onComplete && options.onComplete(this);
|
||||
}
|
||||
},
|
||||
existingData ? existingData : undefined
|
||||
// existingData ? existingData : undefined
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("Could not read %s: %s", this.targetPath, err);
|
||||
|
@ -1,14 +1,24 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Alert, ScrollView, StyleSheet, Text, TouchableHighlight, View } from "react-native";
|
||||
import {
|
||||
Alert,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableHighlight,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useNavigation, Route } from "@react-navigation/native";
|
||||
import { Conversation, Message } from "@/app/lib/conversation";
|
||||
import MessageBubble from "@/components/ui/MessageBubble";
|
||||
import { CachedTranslator, LanguageServer, language_matrix_entry } from "@/app/i18n/api";
|
||||
import {
|
||||
CachedTranslator,
|
||||
LanguageServer,
|
||||
language_matrix_entry,
|
||||
} from "@/app/i18n/api";
|
||||
import { Settings } from "@/app/lib/settings";
|
||||
import { WHISPER_FILES } from "@/app/lib/whisper";
|
||||
import { initWhisper, WhisperContext } from 'whisper.rn'
|
||||
import { useAudioRecorder, AudioModule, RecordingPresets } from 'expo-audio';
|
||||
|
||||
import { initWhisper, WhisperContext } from "whisper.rn";
|
||||
import { useAudioRecorder, AudioModule, RecordingPresets } from "expo-audio";
|
||||
|
||||
const lasOptions = {
|
||||
sampleRate: 32000, // default is 44100 but 32000 is adequate for accurate voice recognition
|
||||
@ -19,11 +29,19 @@ const lasOptions = {
|
||||
};
|
||||
// LiveAudioStream.init(lasOptions as any);
|
||||
|
||||
const ConversationThread = ({ route }: { route?: Route<"Conversation", { conversation: Conversation }> }) => {
|
||||
const ConversationThread = ({
|
||||
route,
|
||||
}: {
|
||||
route?: Route<"Conversation", { conversation: Conversation }>;
|
||||
}) => {
|
||||
const navigation = useNavigation();
|
||||
|
||||
if (!route) {
|
||||
return (<View><Text>Missing Params!</Text></View>)
|
||||
return (
|
||||
<View>
|
||||
<Text>Missing Params!</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/* 2. Get the param */
|
||||
@ -32,21 +50,26 @@ const ConversationThread = ({ route }: { route?: Route<"Conversation", { convers
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [guestSpeak, setGuestSpeak] = useState<string | undefined>();
|
||||
const [guestSpeakLoaded, setGuestSpeakLoaded] = useState<boolean>(false);
|
||||
const [whisperContext, setWhisperContext] = useState<WhisperContext | undefined>();
|
||||
const [whisperContext, setWhisperContext] = useState<
|
||||
WhisperContext | undefined
|
||||
>();
|
||||
const [cachedTranslator, setCachedTranslator] = useState<
|
||||
undefined | CachedTranslator
|
||||
>();
|
||||
|
||||
const [languageLabels, setLanguageLabels] = useState<undefined | {
|
||||
hostNative: {
|
||||
host: string,
|
||||
guest: string,
|
||||
},
|
||||
guestNative: {
|
||||
host: string,
|
||||
guest: string,
|
||||
}
|
||||
}>()
|
||||
const [languageLabels, setLanguageLabels] = useState<
|
||||
| undefined
|
||||
| {
|
||||
hostNative: {
|
||||
host: string;
|
||||
guest: string;
|
||||
};
|
||||
guestNative: {
|
||||
host: string;
|
||||
guest: string;
|
||||
};
|
||||
}
|
||||
>();
|
||||
|
||||
// recorder settings
|
||||
const audioRecorder = useAudioRecorder(RecordingPresets.HIGH_QUALITY);
|
||||
@ -63,50 +86,70 @@ const ConversationThread = ({ route }: { route?: Route<"Conversation", { convers
|
||||
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
|
||||
const languageServer = await LanguageServer.getDefault();
|
||||
try {
|
||||
const languages = await languageServer.fetchLanguages(5000);
|
||||
const cc = new CachedTranslator(
|
||||
const cachedTranslator = new CachedTranslator(
|
||||
"en",
|
||||
conversation.guest.language,
|
||||
languageServer,
|
||||
)
|
||||
console.log("Set cached translator from %s", languageServer.baseUrl)
|
||||
setCachedTranslator(cc);
|
||||
languageServer
|
||||
);
|
||||
console.log("Set cached translator from %s", languageServer.baseUrl);
|
||||
setCachedTranslator(cachedTranslator);
|
||||
|
||||
const settings = await Settings.getDefault();
|
||||
const whisperFileLabel = (await settings.getWhisperModel()) || "small";
|
||||
const whisperFile = WHISPER_FILES[whisperFileLabel];
|
||||
|
||||
if (!whisperFile) {
|
||||
throw new Error(`Could not find the whisper file with the label ${whisperFileLabel}`);
|
||||
}
|
||||
|
||||
try {
|
||||
setWhisperContext(
|
||||
await initWhisper({
|
||||
filePath: whisperFile.targetPath,
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
throw err;
|
||||
}
|
||||
|
||||
// recorder settings
|
||||
|
||||
(async () => {
|
||||
const status = await AudioModule.requestRecordingPermissionsAsync();
|
||||
if (!status.granted) {
|
||||
Alert.alert("Permission to access microphone was denied");
|
||||
}
|
||||
})();
|
||||
setGuestSpeak(await cachedTranslator.translate("Speak"));
|
||||
const hostLang1 = languages[conversation.host.language].name;
|
||||
const guestLang1 = languages[conversation.host.language].name;
|
||||
const hostLang2 = await cachedTranslator.translate(
|
||||
languages[conversation.host.language].name
|
||||
);
|
||||
const guestLang2 = await cachedTranslator.translate(
|
||||
languages[conversation.host.language].name
|
||||
);
|
||||
setLanguageLabels({
|
||||
hostNative: {
|
||||
host: hostLang1,
|
||||
guest: guestLang1,
|
||||
},
|
||||
guestNative: {
|
||||
host: hostLang2,
|
||||
guest: guestLang2,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Could not set translator from %s: %s", languageServer.baseUrl, err)
|
||||
console.error(
|
||||
"Could not set translator from %s: %s",
|
||||
languageServer.baseUrl,
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
const settings = await Settings.getDefault();
|
||||
const whisperFile = WHISPER_FILES[await settings.getWhisperModel() || "en"];
|
||||
setWhisperContext(await initWhisper({
|
||||
filePath: whisperFile.targetPath,
|
||||
}));
|
||||
|
||||
// recorder settings
|
||||
|
||||
(async () => {
|
||||
const status = await AudioModule.requestRecordingPermissionsAsync();
|
||||
if (!status.granted) {
|
||||
Alert.alert('Permission to access microphone was denied');
|
||||
}
|
||||
})();
|
||||
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) => {
|
||||
@ -114,7 +157,7 @@ const ConversationThread = ({ route }: { route?: Route<"Conversation", { convers
|
||||
};
|
||||
|
||||
if (!conversation) {
|
||||
console.warn("Conversation is null or undefined.")
|
||||
console.warn("Conversation is null or undefined.");
|
||||
}
|
||||
|
||||
conversation.on("add_message", updateMessages);
|
||||
@ -133,11 +176,17 @@ const ConversationThread = ({ route }: { route?: Route<"Conversation", { convers
|
||||
|
||||
return cachedTranslator ? (
|
||||
<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>)
|
||||
}
|
||||
{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
|
||||
style={{
|
||||
borderColor: "black",
|
||||
@ -177,15 +226,9 @@ const ConversationThread = ({ route }: { route?: Route<"Conversation", { convers
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
languageLabels: {
|
||||
|
||||
},
|
||||
nativeHostLabel: {
|
||||
|
||||
},
|
||||
nativeGuestLabel: {
|
||||
|
||||
},
|
||||
})
|
||||
languageLabels: {},
|
||||
nativeHostLabel: {},
|
||||
nativeGuestLabel: {},
|
||||
});
|
||||
|
||||
export default ConversationThread;
|
||||
|
@ -36,8 +36,6 @@ const SettingsComponent = () => {
|
||||
const [whisperModel, setWhisperModel] =
|
||||
useState<keyof typeof WHISPER_MODELS>("small");
|
||||
const [whisperFile, setWhisperFile] = useState<WhisperFile | undefined>();
|
||||
const [whisperFileExists, setWhisperFileExists] = useState<boolean>(false);
|
||||
const [isWhisperHashValid, setIsWhisperHashValid] = useState<boolean>(false);
|
||||
const [downloader, setDownloader] = useState<any>(null);
|
||||
const [bytesDone, setBytesDone] = useState<number | undefined>();
|
||||
const [bytesRemaining, setBytesRemaining] = useState<number | undefined>();
|
||||
@ -54,12 +52,13 @@ const SettingsComponent = () => {
|
||||
);
|
||||
setWhisperModel((await settings.getWhisperModel()) || "small");
|
||||
setWhisperFile(WHISPER_FILES[whisperModel]);
|
||||
if (whisperFile) {
|
||||
await whisperFile.syncHfMetadata();
|
||||
await whisperFile.updateTargetExistence();
|
||||
await whisperFile.updateTargetHash();
|
||||
setWhisperFileExists(whisperFile.does_target_exist)
|
||||
if (!whisperFile) {
|
||||
throw new Error("Invalid Whisper file!");
|
||||
}
|
||||
await whisperFile.syncHfMetadata();
|
||||
await whisperFile.updateTargetExistence();
|
||||
await whisperFile.updateTargetHash();
|
||||
console.log("Does %s exist? part=%s, target=%s", whisperFile.label, whisperFile.does_part_target_exist, whisperFile.does_target_exist)
|
||||
})();
|
||||
}, [whisperFile]);
|
||||
|
||||
@ -100,19 +99,21 @@ const SettingsComponent = () => {
|
||||
// await wFile.updateTargetHash();
|
||||
// setIsWhisperHashValid(wFile.isHashValid);
|
||||
setWhisperFile(wFile);
|
||||
setWhisperFileExists(wFile.does_target_exist);
|
||||
};
|
||||
|
||||
const doSetDownloadStatus = (arg0: WhisperFile) => {
|
||||
console.log("Downloading ....")
|
||||
setIsWhisperHashValid(arg0.isHashValid);
|
||||
// console.log("Downloading ....");
|
||||
setBytesDone(arg0.download_data?.totalBytesWritten);
|
||||
setBytesRemaining(arg0.download_data?.totalBytesExpectedToWrite);
|
||||
};
|
||||
|
||||
const doOnComplete = (arg0: WhisperFile) => {
|
||||
const doOnComplete = async (arg0: WhisperFile) => {
|
||||
console.log("✅ Download complete.");
|
||||
setDownloader(undefined);
|
||||
await arg0.updateTargetExistence();
|
||||
setWhisperFile(arg0);
|
||||
}
|
||||
await whisperFile?.updateTargetExistence();
|
||||
};
|
||||
|
||||
const doDownload = async () => {
|
||||
if (!whisperModel) {
|
||||
@ -144,7 +145,7 @@ const SettingsComponent = () => {
|
||||
const doDelete = async () => {
|
||||
const whisperFile = WHISPER_MODELS[whisperModel];
|
||||
whisperFile.delete();
|
||||
setStatusTimeout(undefined);
|
||||
await whisperFile.updateTargetExistence();
|
||||
};
|
||||
|
||||
return hostLanguage && libretranslateBaseUrl ? (
|
||||
@ -194,36 +195,82 @@ const SettingsComponent = () => {
|
||||
))}
|
||||
</Picker>
|
||||
<View style={styles.downloadButtonWrapper}>
|
||||
{((!downloader) && whisperFile) &&
|
||||
(whisperFile.does_target_exist && (<Pressable onPress={doDelete} style={styles.deleteButton}>
|
||||
<Text style={styles.buttonText}>DELETE {whisperModel.toUpperCase()}</Text>
|
||||
</Pressable>))
|
||||
}
|
||||
<Pressable onPress={doDownload} style={styles.downloadButton}>
|
||||
<Text style={styles.buttonText}>DOWNLOAD {whisperModel.toUpperCase()}</Text>
|
||||
</Pressable>
|
||||
))
|
||||
|
||||
{
|
||||
downloader && (
|
||||
<Pressable onPress={doStopDownload} style={styles.pauseDownloadButton}>
|
||||
<Text style={styles.buttonText}>STOP DOWNLOAD</Text>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
{bytesDone && bytesRemaining && whisperFile?.does_part_target_exist && (
|
||||
<View>
|
||||
{whisperFile &&
|
||||
(<Text>
|
||||
Downloading to {whisperFile.targetPath}
|
||||
</Text>)}
|
||||
<Text>
|
||||
{bytesDone} of{" "}
|
||||
{bytesRemaining} (
|
||||
{bytesDone / bytesRemaining * 100} %){" "}
|
||||
{/* The target is completely downloaded */}
|
||||
{!downloader && whisperFile?.does_target_exist && (
|
||||
<Pressable onPress={doDelete} style={styles.deleteButton}>
|
||||
<Text style={styles.buttonText}>
|
||||
DELETE {whisperModel.toUpperCase()}
|
||||
</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
)}
|
||||
|
||||
{/* The target "part" is present and is downloading */}
|
||||
|
||||
{downloader && whisperFile?.does_part_target_exist && (
|
||||
<Pressable
|
||||
onPress={doStopDownload}
|
||||
style={styles.pauseDownloadButton}
|
||||
>
|
||||
<Text style={styles.buttonText}>STOP DOWNLOAD</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
|
||||
{/* The target "part" is present and we are NOT downloading */}
|
||||
|
||||
{!downloader && whisperFile?.does_part_target_exist && (
|
||||
<>
|
||||
<Pressable onPress={doDownload} style={styles.downloadButton}>
|
||||
<Text style={styles.buttonText}>RESUME DOWNLOAD</Text>
|
||||
</Pressable>
|
||||
<Pressable onPress={doDelete} style={styles.deleteButton}>
|
||||
<Text style={styles.buttonText}>
|
||||
DELETE {whisperModel.toUpperCase()}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Anything else -- usually if the file has not yet been downloaded */}
|
||||
|
||||
{!(
|
||||
downloader ||
|
||||
whisperFile?.does_target_exist ||
|
||||
whisperFile?.does_part_target_exist
|
||||
) && (
|
||||
<Pressable onPress={doDownload} style={styles.downloadButton}>
|
||||
<Text style={styles.buttonText}>
|
||||
DOWNLOAD {whisperModel.toUpperCase()}
|
||||
</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
|
||||
{downloader &&
|
||||
bytesDone &&
|
||||
bytesRemaining &&
|
||||
whisperFile?.does_part_target_exist && (
|
||||
<View>
|
||||
{whisperFile && (
|
||||
<Text>Downloading to {whisperFile.targetPath}</Text>
|
||||
)}
|
||||
<Text>
|
||||
{bytesDone} of {bytesRemaining} (
|
||||
{(bytesDone / bytesRemaining) * 100} %){" "}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View>
|
||||
<Text>Debug Panel</Text>
|
||||
<Text>downloader is null? {downloader ? "no" : "yes"}</Text>
|
||||
<Text>
|
||||
whisperFile.does_target_exist{" "}
|
||||
{whisperFile?.does_target_exist ? "yes" : "no"}
|
||||
</Text>
|
||||
<Text>
|
||||
whisperFile.does_part_target_exist{" "}
|
||||
{whisperFile?.does_part_target_exist ? "yes" : "no"}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
@ -239,11 +286,12 @@ const styles = StyleSheet.create({
|
||||
flexDirection: "row",
|
||||
},
|
||||
downloadButton: {
|
||||
backgroundColor: "darkblue",
|
||||
backgroundColor: "#236b9f",
|
||||
padding: 20,
|
||||
margin: 10,
|
||||
flex: 3,
|
||||
flex: 1,
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
},
|
||||
deleteButton: {
|
||||
backgroundColor: "darkred",
|
||||
@ -261,11 +309,11 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
buttonText: {
|
||||
color: "#fff",
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
alignSelf: "center",
|
||||
textAlign: "center",
|
||||
textAlignVertical: "top",
|
||||
// flex: 1,
|
||||
// fontSize: 16,
|
||||
// alignSelf: "center",
|
||||
// textAlign: "center",
|
||||
// textAlignVertical: "top",
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
|
Loading…
x
Reference in New Issue
Block a user