345 lines
10 KiB
TypeScript
345 lines
10 KiB
TypeScript
// Import necessary packages
|
|
import React, { useState, useEffect } from "react";
|
|
import { View, Text, TextInput, StyleSheet, Pressable } from "react-native"; // Add Picker import
|
|
import { getDb } from "@/app/lib/db";
|
|
import { Settings } from "@/app/lib/settings";
|
|
import { LanguageServer, fetchWithTimeout } from "@/app/i18n/api";
|
|
import { Picker } from "@react-native-picker/picker";
|
|
import { longLang } from "@/app/i18n/lang";
|
|
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 { File, Paths } from "expo-file-system/next";
|
|
|
|
type Language = {
|
|
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 [whisperModelTarget, setWhisperModelTarget] = useState<File | 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() || "small";
|
|
setWhisperModel(wModel);
|
|
setWhisperModelTarget(new File(WHISPER_MODELS[wModel].target))
|
|
} catch (err) {
|
|
console.warn("Could not set whisper model: %s", 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);
|
|
setWhisperModelTarget(new File(WHISPER_MODELS[whisperModel].target))
|
|
console.log("Setting whisper model target to %s", whisperModelTarget);
|
|
}, 1000);
|
|
|
|
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 fileExists = async (file: File) => {
|
|
const info = await FileSystem.getInfoAsync(file.uri);
|
|
return info.exists;
|
|
}
|
|
|
|
const doDelete = async () => {
|
|
if (!whisperModelTarget) return;
|
|
whisperModelTarget.delete();
|
|
}
|
|
|
|
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,
|
|
force_redownload: true,
|
|
})
|
|
);
|
|
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();
|
|
};
|
|
|
|
const handleWhisperModelChange = async (value: string) => {
|
|
const settings = await Settings.getDefault();
|
|
setWhisperModel(value);
|
|
await settings.setWhisperModel(value);
|
|
setWhisperModelTarget(getWhisperTarget(value));
|
|
}
|
|
|
|
const doStopDownload = async () => {
|
|
if (!downloader) return;
|
|
await downloader.pauseAsync()
|
|
}
|
|
|
|
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 || "small"}
|
|
style={{ height: 50, width: "100%" }}
|
|
onValueChange={handleWhisperModelChange}
|
|
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} bytes of {whisperDownloadProgress.totalBytesExpectedToWrite} bytes
|
|
|
|
({Math.round((whisperDownloadProgress.totalBytesWritten / whisperDownloadProgress.totalBytesExpectedToWrite) * 100)} %)
|
|
</Text>
|
|
)
|
|
}
|
|
<View style={styles.downloadButtonWrapper}>
|
|
{downloader && whisperDownloadProgress && (whisperDownloadProgress.totalBytesWritten !== whisperDownloadProgress.totalBytesExpectedToWrite) ? (
|
|
(<Pressable onPress={doStopDownload} style={styles.pauseDownloadButton}>
|
|
<Text style={styles.buttonText}>Pause Download</Text>
|
|
</Pressable>)
|
|
) :
|
|
(<Pressable onPress={doDownload} style={styles.downloadButton}>
|
|
<Text style={styles.buttonText}>Download</Text>
|
|
</Pressable>)
|
|
}
|
|
{whisperModelTarget && fileExists(whisperModelTarget) &&
|
|
(<Pressable onPress={doDelete} style={styles.deleteButton}>
|
|
<Text style={styles.buttonText}>Delete</Text>
|
|
</Pressable>)
|
|
}
|
|
</View>
|
|
</View>
|
|
</View>
|
|
) : (
|
|
<View>
|
|
<Text>Loading ...</Text>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
// Create styles for the component
|
|
const styles = StyleSheet.create({
|
|
downloadButtonWrapper: {
|
|
flex: 1,
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
verticalAlign: "middle",
|
|
},
|
|
downloadButton: {
|
|
backgroundColor: "blue",
|
|
padding: 10,
|
|
margin: 10,
|
|
},
|
|
deleteButton: {
|
|
backgroundColor: "darkred",
|
|
padding: 10,
|
|
margin: 10,
|
|
},
|
|
pauseDownloadButton: {
|
|
backgroundColor: "#444444",
|
|
padding: 10,
|
|
margin: 10,
|
|
},
|
|
buttonText: {
|
|
color: "white",
|
|
alignSelf: "center",
|
|
},
|
|
container: {
|
|
flex: 1,
|
|
padding: 20,
|
|
},
|
|
label: {
|
|
fontSize: 16,
|
|
marginBottom: 8,
|
|
},
|
|
input: {
|
|
height: 40,
|
|
borderColor: "gray",
|
|
borderWidth: 1,
|
|
marginBottom: 20,
|
|
paddingHorizontal: 8,
|
|
},
|
|
});
|
|
|
|
export default SettingsComponent;
|