updating code using knex.

This commit is contained in:
Jordan
2025-03-02 20:15:27 -08:00
parent d00e6d62ff
commit a9b5ccf84f
15 changed files with 2557 additions and 605 deletions

View File

@ -1,240 +1,136 @@
// 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 { View, Text, TextInput, Pressable, StyleSheet } from "react-native";
import { WhisperFile } from "@/app/lib/whisper";
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;
import { LanguageServer, language_matrix, language_matrix_entry } from "@/app/i18n/api";
const WHISPER_MODELS = {
small: new WhisperFile("small"),
medium: new WhisperFile("medium"),
large: new WhisperFile("large"),
};
type LanguageMatrix = {
[key: string]: Language;
};
const LIBRETRANSLATE_BASE_URL = "https://translate.argosopentech.com/translate";
type connection_test_t =
| {
success: true;
}
| {
success: false;
error: string;
};
const SettingsComponent: React.FC = () => {
const SettingsComponent = () => {
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 [languageOptions, setLanguageOptions] = useState<language_matrix | undefined>();
const [langServerConn, setLangServerConn] = useState<{
success: boolean;
error?: string;
} | null>(null);
const [whisperModel, setWhisperModel] =
useState<keyof typeof WHISPER_MODELS>("small");
const [downloader, setDownloader] = useState<any>(null);
const [whisperDownloadProgress, setWhisperDownloadProgress] = useState<
FileSystem.DownloadProgressData | undefined
>();
const [downloader, setDownloader] = useState<DownloadResumable | undefined>();
const [whisperModelTarget, setWhisperModelTarget] = useState<File | undefined>()
any | null
>(null);
const fillHostLanguageOptions = async () => {
useEffect(() => {
loadSettings();
}, []);
useEffect(() => {
checkDownloadStatus(whisperModel);
}, [whisperModel]);
const getLanguageOptions = async () => {
const languageServer = await LanguageServer.getDefault();
setLanguageOptions(await languageServer.fetchLanguages());
}
const loadSettings = 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
const hostLanguage = await settings.getHostLanguage();
setHostLanguage(hostLanguage);
const libretranslateBaseUrl = await settings.getLibretranslateBaseUrl();
setLibretranslateBaseUrl(libretranslateBaseUrl);
const whisperModel = await settings.getWhisperModel();
setWhisperModel(whisperModel as keyof typeof WHISPER_MODELS);
};
const handleHostLanguageChange = async (lang: string) => {
const settings = await Settings.getDefault();
setHostLanguage(lang);
await settings.setHostLanguage(lang);
};
const handleLibretranslateBaseUrlChange = async (url: string) => {
const settings = await Settings.getDefault();
setLibretranslateBaseUrl(url);
await settings.setLibretranslateBaseUrl(url);
checkLangServerConnection(url);
};
const checkLangServerConnection = async (baseUrl: string) => {
try {
const langData = await langServer.fetchLanguages();
setLanguages(langData);
// Replace with actual connection check logic
setLangServerConn({ success: true });
} catch (err) {
console.warn("Got an error fetching: %s", err);
setLangServerConn({
success: false,
error: `Could not connect to ${libretranslateBaseUrl}: ${err}`,
});
} catch (error) {
setLangServerConn({ success: false, error: `${error}` });
}
};
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 handleWhisperModelChange = async (
model: keyof typeof WHISPER_MODELS
) => {
const settings = await Settings.getDefault();
setWhisperModel(model);
await settings.setWhisperModel(model);
checkDownloadStatus(model);
};
const doDownload = async () => {
if (!whisperModel) return;
const whisperFile = WHISPER_MODELS[whisperModel];
const resumable = await whisperFile.createDownloadResumable({
onData: (progress) => setWhisperDownloadProgress(progress),
});
setDownloader(resumable);
try {
setDownloader(
await initiateWhisperDownload(whisperModel, {
onDownload: setWhisperDownloadProgress,
force_redownload: true,
})
);
await downloader?.downloadAsync();
console.log("completed download");
} catch (err) {
console.error(err);
await resumable.downloadAsync();
checkDownloadStatus(whisperModel);
} catch (error) {
console.error("Failed to download whisper model:", error);
}
};
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()
}
downloader.cancelAsync();
setDownloader(null);
};
return isLoaded ? (
const doDelete = async () => {
const whisperFile = WHISPER_MODELS[whisperModel];
whisperFile.delete();
checkDownloadStatus(whisperModel);
};
const checkDownloadStatus = async (model: keyof typeof WHISPER_MODELS) => {
const whisperFile = WHISPER_MODELS[model];
const status = await whisperFile.getDownloadStatus();
if (
!status.isDownloadComplete &&
(!status.doesTargetExist || !status.hasDownloadStarted)
) {
setDownloader(null);
}
};
return hostLanguage && libretranslateBaseUrl ? (
<View style={styles.container}>
<Text style={styles.label}>Host Language:</Text>
<Picker
selectedValue={hostLanguage || ""}
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]} />
))}
{ languag }
<Picker.Item label="English" value="en" />
<Picker.Item label="Spanish" value="es" />
</Picker>
<Text style={styles.label}>LibreTranslate Base URL:</Text>
@ -253,40 +149,59 @@ const SettingsComponent: React.FC = () => {
</Text>
))}
<Picker
selectedValue={whisperModel || "small"}
selectedValue={whisperModel}
style={{ height: 50, width: "100%" }}
onValueChange={handleWhisperModelChange}
accessibilityHint="language"
>
{Object.entries(WHISPER_MODELS).map(([key, { label }]) => (
<Picker.Item key={key} label={label} value={key} />
{Object.entries(WHISPER_MODELS).map(([key, whisperFile]) => (
<Picker.Item
key={whisperFile.tag}
label={whisperFile.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)} %)
{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}>
{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>)
}
</Pressable>
) : (
<Pressable onPress={doDownload} style={styles.downloadButton}>
<Text style={styles.buttonText}>DOWNLOAD</Text>
</Pressable>
)}
{whisperModel &&
WHISPER_MODELS[whisperModel] &&
WHISPER_MODELS[whisperModel].doesTargetExist && (
<Pressable
onPress={doDelete}
style={styles.deleteButton}
aria-label="Delete"
>
<Text style={styles.buttonText}>Delete</Text>
</Pressable>
)}
</View>
</View>
</View>
@ -300,29 +215,36 @@ const SettingsComponent: React.FC = () => {
// Create styles for the component
const styles = StyleSheet.create({
downloadButtonWrapper: {
flex: 1,
flexDirection: "row",
alignItems: "center",
verticalAlign: "middle",
},
downloadButton: {
backgroundColor: "blue",
padding: 10,
backgroundColor: "darkblue",
padding: 20,
margin: 10,
flex: 3,
flexDirection: "column",
},
deleteButton: {
backgroundColor: "darkred",
flex: 1,
flexDirection: "column",
padding: 10,
margin: 10,
height: 50,
},
pauseDownloadButton: {
backgroundColor: "#444444",
padding: 10,
margin: 10,
height: 50,
},
buttonText: {
color: "white",
color: "#fff",
flex: 1,
fontSize: 16,
alignSelf: "center",
textAlign: "center",
textAlignVertical: "top",
},
container: {
flex: 1,

View File

@ -3,108 +3,115 @@ 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";
import { getDb } from "@/app/lib/db";
import { Knex } from "knex";
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,
}
})
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");
})
let db: Knex;
let settings: Settings;
beforeAll(() => {
jest.useFakeTimers();
})
beforeEach(async () => {
db = await getDb("development");
settings = new Settings(db);
await settings.setHostLanguage("en");
await settings.setLibretranslateBaseUrl("https://example.com");
});
afterAll(() => {
jest.useRealTimers()
})
afterEach(async () => {
await db.migrate.down();
await db.destroy();
});
test("renders correctly with initial settings", async () => {
render(<SettingsComponent />);
jest.advanceTimersByTime(RENDER_TIME);
screen.debug();
beforeAll(() => {
jest.useFakeTimers();
});
// Wait for the component to fetch and display the initial settings
await screen.findByText(/Host Language:/i);
await screen.findByText(/LibreTranslate Base URL:/i);
afterAll(() => {
jest.useRealTimers();
});
// expect(screen.getByDisplayValue("English")).toBeTruthy();
expect(screen.getByAccessibilityHint("libretranslate base url")).toBeTruthy();
});
test("renders correctly with initial settings", async () => {
render(<SettingsComponent />);
jest.advanceTimersByTime(RENDER_TIME);
screen.debug();
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);
// 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();
});
// Change the host language input value
const picker = screen.getByAccessibilityHint("hostLanguage");
fireEvent(picker, "onvalueChange", "es");
expect(picker.props.selectedIndex).toStrictEqual(0);
});
test("updates host language setting when input changes", async () => {
render(<SettingsComponent />);
test("updates LibreTranslate base URL 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);
jest.advanceTimersByTime(RENDER_TIME)
screen.debug();
// Change the host language input value
const picker = screen.getByAccessibilityHint("hostLanguage");
fireEvent(picker, "onvalueChange", "es");
expect(picker.props.selectedIndex).toStrictEqual(0);
});
// Wait for the component to fetch and display the initial settings
await screen.findByText(/Host Language:/i);
await screen.findByText(/LibreTranslate Base URL:/i);
test("updates LibreTranslate base URL setting when input changes", async () => {
render(<SettingsComponent />);
// Change the LibreTranslate base URL input value
fireEvent.changeText(screen.getByAccessibilityHint("libretranslate base url"), "http://new-example.com");
jest.advanceTimersByTime(RENDER_TIME);
jest.advanceTimersByTime(RENDER_TIME);
screen.debug();
expect(screen.getByAccessibilityHint("libretranslate base url")).toBeTruthy();
});
});
// 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();
});
});