You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
692 lines
20 KiB
692 lines
20 KiB
import { app, BrowserWindow, ipcMain } from "electron";
|
|
import { Capacitor } from "@capacitor/core";
|
|
import fs from "fs";
|
|
import path from "path";
|
|
import { fileURLToPath } from "url";
|
|
|
|
// Get __dirname equivalent in ES modules
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
// Set global variables that the plugin needs
|
|
global.__dirname = __dirname;
|
|
global.__filename = __filename;
|
|
|
|
// Now import the plugin after setting up globals
|
|
import { CapacitorSQLite } from "@capacitor-community/sqlite/electron/dist/plugin.js";
|
|
|
|
// Simple logger implementation
|
|
const logger = {
|
|
// eslint-disable-next-line no-console
|
|
log: (...args: unknown[]) => console.log("[Main]", ...args),
|
|
// eslint-disable-next-line no-console
|
|
error: (...args: unknown[]) => console.error("[Main]", ...args),
|
|
// eslint-disable-next-line no-console
|
|
info: (...args: unknown[]) => console.info("[Main]", ...args),
|
|
// eslint-disable-next-line no-console
|
|
warn: (...args: unknown[]) => console.warn("[Main]", ...args),
|
|
// eslint-disable-next-line no-console
|
|
debug: (...args: unknown[]) => console.debug("[Main]", ...args),
|
|
};
|
|
|
|
logger.info("Starting main process initialization...");
|
|
|
|
// Initialize Capacitor for Electron in main process
|
|
try {
|
|
logger.info("About to initialize Capacitor...");
|
|
logger.info("Capacitor before init:", {
|
|
hasPlatform: "platform" in Capacitor,
|
|
hasIsNativePlatform: "isNativePlatform" in Capacitor,
|
|
platformType: typeof Capacitor.platform,
|
|
isNativePlatformType: typeof Capacitor.isNativePlatform,
|
|
});
|
|
|
|
// Try direct assignment first
|
|
try {
|
|
(Capacitor as unknown as { platform: string }).platform = "electron";
|
|
(Capacitor as unknown as { isNativePlatform: boolean }).isNativePlatform =
|
|
true;
|
|
logger.info("Direct assignment successful");
|
|
} catch (e) {
|
|
logger.warn("Direct assignment failed, trying defineProperty:", e);
|
|
Object.defineProperty(Capacitor, "isNativePlatform", {
|
|
get: () => true,
|
|
configurable: true,
|
|
});
|
|
|
|
Object.defineProperty(Capacitor, "platform", {
|
|
get: () => "electron",
|
|
configurable: true,
|
|
});
|
|
}
|
|
|
|
logger.info("Capacitor after init:", {
|
|
platform: Capacitor.platform,
|
|
isNativePlatform: Capacitor.isNativePlatform,
|
|
platformType: typeof Capacitor.platform,
|
|
isNativePlatformType: typeof Capacitor.isNativePlatform,
|
|
});
|
|
} catch (error) {
|
|
logger.error("Failed to initialize Capacitor:", error);
|
|
throw error;
|
|
}
|
|
|
|
// Database path resolution utilities
|
|
const getAppDataPath = async (): Promise<string> => {
|
|
try {
|
|
// Read config file directly
|
|
const configPath = path.join(__dirname, "..", "capacitor.config.json");
|
|
const configContent = await fs.promises.readFile(configPath, "utf-8");
|
|
const config = JSON.parse(configContent);
|
|
const linuxPath = config?.plugins?.CapacitorSQLite?.electronLinuxLocation;
|
|
|
|
if (linuxPath) {
|
|
// Expand ~ to home directory
|
|
const expandedPath = linuxPath.replace(/^~/, process.env.HOME || "");
|
|
logger.info("[Electron] Using configured database path:", expandedPath);
|
|
return expandedPath;
|
|
}
|
|
|
|
// Fallback to app.getPath if config path is not available
|
|
const userDataPath = app.getPath("userData");
|
|
logger.info("[Electron] Using fallback user data path:", userDataPath);
|
|
return userDataPath;
|
|
} catch (error) {
|
|
logger.error("[Electron] Error getting app data path:", error);
|
|
// Fallback to app.getPath if anything fails
|
|
const userDataPath = app.getPath("userData");
|
|
logger.info(
|
|
"[Electron] Using fallback user data path after error:",
|
|
userDataPath,
|
|
);
|
|
return userDataPath;
|
|
}
|
|
};
|
|
|
|
const validateAndNormalizePath = async (filePath: string): Promise<string> => {
|
|
// Resolve any relative paths
|
|
const resolvedPath = path.resolve(filePath);
|
|
|
|
// Ensure it's an absolute path
|
|
if (!path.isAbsolute(resolvedPath)) {
|
|
throw new Error(`Database path must be absolute: ${resolvedPath}`);
|
|
}
|
|
|
|
// Ensure it's within the app data directory
|
|
const appDataPath = await getAppDataPath();
|
|
if (!resolvedPath.startsWith(appDataPath)) {
|
|
throw new Error(
|
|
`Database path must be within app data directory: ${resolvedPath}`,
|
|
);
|
|
}
|
|
|
|
// Normalize the path
|
|
const normalizedPath = path.normalize(resolvedPath);
|
|
|
|
logger.info("[Electron] Validated database path:", {
|
|
original: filePath,
|
|
resolved: resolvedPath,
|
|
normalized: normalizedPath,
|
|
appDataPath,
|
|
isAbsolute: path.isAbsolute(normalizedPath),
|
|
isWithinAppData: normalizedPath.startsWith(appDataPath),
|
|
});
|
|
|
|
return normalizedPath;
|
|
};
|
|
|
|
const ensureDirectoryExists = async (dirPath: string): Promise<void> => {
|
|
try {
|
|
// Normalize the path first
|
|
const normalizedPath = path.normalize(dirPath);
|
|
|
|
// Check if directory exists
|
|
if (!fs.existsSync(normalizedPath)) {
|
|
logger.info("[Electron] Creating database directory:", normalizedPath);
|
|
await fs.promises.mkdir(normalizedPath, { recursive: true });
|
|
}
|
|
|
|
// Verify directory permissions
|
|
try {
|
|
await fs.promises.access(
|
|
normalizedPath,
|
|
fs.constants.R_OK | fs.constants.W_OK,
|
|
);
|
|
logger.info(
|
|
"[Electron] Database directory permissions verified:",
|
|
normalizedPath,
|
|
);
|
|
} catch (error) {
|
|
logger.error("[Electron] Database directory permission error:", error);
|
|
throw new Error(`Database directory not accessible: ${normalizedPath}`);
|
|
}
|
|
|
|
// Test write permissions
|
|
const testFile = path.join(normalizedPath, ".write-test");
|
|
try {
|
|
await fs.promises.writeFile(testFile, "test");
|
|
await fs.promises.unlink(testFile);
|
|
logger.info(
|
|
"[Electron] Database directory write test passed:",
|
|
normalizedPath,
|
|
);
|
|
} catch (error) {
|
|
logger.error("[Electron] Database directory write test failed:", error);
|
|
throw new Error(`Database directory not writable: ${normalizedPath}`);
|
|
}
|
|
} catch (error) {
|
|
logger.error(
|
|
"[Electron] Failed to ensure database directory exists:",
|
|
error,
|
|
);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
// Initialize database paths
|
|
let dbPath: string | undefined;
|
|
let dbDir: string | undefined;
|
|
let dbPathInitialized = false;
|
|
let dbPathInitializationPromise: Promise<void> | null = null;
|
|
|
|
const initializeDatabasePaths = async (): Promise<void> => {
|
|
// Prevent multiple simultaneous initializations
|
|
if (dbPathInitializationPromise) {
|
|
return dbPathInitializationPromise;
|
|
}
|
|
|
|
if (dbPathInitialized) {
|
|
return;
|
|
}
|
|
|
|
dbPathInitializationPromise = (async () => {
|
|
try {
|
|
// Get the base directory from config
|
|
dbDir = await getAppDataPath();
|
|
logger.info("[Electron] Database directory:", dbDir);
|
|
|
|
// Ensure the directory exists and is writable
|
|
await ensureDirectoryExists(dbDir);
|
|
|
|
// Construct the database path
|
|
dbPath = await validateAndNormalizePath(
|
|
path.join(dbDir, "timesafari.db"),
|
|
);
|
|
logger.info("[Electron] Database path initialized:", dbPath);
|
|
|
|
// Verify the database file if it exists
|
|
if (fs.existsSync(dbPath)) {
|
|
try {
|
|
await fs.promises.access(
|
|
dbPath,
|
|
fs.constants.R_OK | fs.constants.W_OK,
|
|
);
|
|
logger.info(
|
|
"[Electron] Existing database file permissions verified:",
|
|
dbPath,
|
|
);
|
|
} catch (error) {
|
|
logger.error("[Electron] Database file permission error:", error);
|
|
throw new Error(`Database file not accessible: ${dbPath}`);
|
|
}
|
|
}
|
|
|
|
dbPathInitialized = true;
|
|
} catch (error) {
|
|
logger.error("[Electron] Failed to initialize database paths:", error);
|
|
throw error;
|
|
} finally {
|
|
dbPathInitializationPromise = null;
|
|
}
|
|
})();
|
|
|
|
return dbPathInitializationPromise;
|
|
};
|
|
|
|
// Initialize SQLite plugin
|
|
let sqlitePlugin: any = null;
|
|
let sqliteInitialized = false;
|
|
let sqliteInitializationPromise: Promise<void> | null = null;
|
|
|
|
async function initializeSQLite() {
|
|
// Prevent multiple simultaneous initializations
|
|
if (sqliteInitializationPromise) {
|
|
return sqliteInitializationPromise;
|
|
}
|
|
|
|
if (sqliteInitialized) {
|
|
return;
|
|
}
|
|
|
|
sqliteInitializationPromise = (async () => {
|
|
try {
|
|
logger.info("[Electron] Initializing SQLite plugin...");
|
|
sqlitePlugin = new CapacitorSQLite();
|
|
|
|
// Initialize database paths first
|
|
await initializeDatabasePaths();
|
|
|
|
if (!dbPath) {
|
|
throw new Error("Database path not initialized");
|
|
}
|
|
|
|
// Test the plugin
|
|
const echoResult = await sqlitePlugin.echo({ value: "test" });
|
|
logger.info("[Electron] SQLite plugin echo test:", echoResult);
|
|
|
|
// Initialize database connection using validated dbPath
|
|
const connectionOptions = {
|
|
database: dbPath,
|
|
version: 1,
|
|
readOnly: false,
|
|
encryption: "no-encryption",
|
|
useNative: true,
|
|
mode: "rwc", // Force read-write-create mode
|
|
};
|
|
|
|
logger.info(
|
|
"[Electron] Creating initial connection with options:",
|
|
connectionOptions,
|
|
);
|
|
|
|
// Log the actual path being used
|
|
logger.info("[Electron] Using database path:", dbPath);
|
|
logger.info("[Electron] Path exists:", fs.existsSync(dbPath));
|
|
logger.info("[Electron] Path is absolute:", path.isAbsolute(dbPath));
|
|
|
|
const db = await sqlitePlugin.createConnection(connectionOptions);
|
|
|
|
if (!db || typeof db !== "object") {
|
|
throw new Error(
|
|
`Failed to create database connection - invalid response. Path used: ${dbPath}`,
|
|
);
|
|
}
|
|
|
|
// Wait a moment for the connection to be fully established
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
|
|
// Verify the connection is working
|
|
try {
|
|
const result = await db.query("PRAGMA journal_mode;");
|
|
logger.info("[Electron] Database connection verified:", result);
|
|
} catch (error) {
|
|
logger.error(
|
|
"[Electron] Database connection verification failed:",
|
|
error,
|
|
);
|
|
throw error;
|
|
}
|
|
|
|
sqliteInitialized = true;
|
|
logger.info("[Electron] SQLite plugin initialized successfully");
|
|
} catch (error) {
|
|
logger.error("[Electron] Failed to initialize SQLite plugin:", error);
|
|
throw error;
|
|
} finally {
|
|
sqliteInitializationPromise = null;
|
|
}
|
|
})();
|
|
|
|
return sqliteInitializationPromise;
|
|
}
|
|
|
|
// Initialize app when ready
|
|
app.whenReady().then(async () => {
|
|
logger.info("App is ready, starting initialization...");
|
|
|
|
// Create window first
|
|
const mainWindow = createWindow();
|
|
|
|
// Initialize database in background
|
|
initializeSQLite().catch((error) => {
|
|
logger.error(
|
|
"[Electron] Database initialization failed, but continuing:",
|
|
error,
|
|
);
|
|
// Notify renderer about database status
|
|
if (!mainWindow.isDestroyed()) {
|
|
mainWindow.webContents.send("database-status", {
|
|
status: "error",
|
|
error: error.message,
|
|
});
|
|
}
|
|
});
|
|
|
|
// Handle window close
|
|
mainWindow.on("closed", () => {
|
|
logger.info("[Electron] Main window closed");
|
|
});
|
|
|
|
// Handle window close request
|
|
mainWindow.on("close", (event) => {
|
|
logger.info("[Electron] Window close requested");
|
|
// Prevent immediate close if we're in the middle of something
|
|
if (mainWindow.webContents.isLoading()) {
|
|
event.preventDefault();
|
|
logger.info("[Electron] Deferring window close due to loading state");
|
|
mainWindow.webContents.once("did-finish-load", () => {
|
|
mainWindow.close();
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
// Check if running in dev mode
|
|
// const isDev = process.argv.includes("--inspect");
|
|
|
|
function createWindow(): BrowserWindow {
|
|
// Resolve preload path based on environment
|
|
const preloadPath = app.isPackaged
|
|
? path.join(process.resourcesPath, "preload.js")
|
|
: path.join(__dirname, "preload.js");
|
|
|
|
logger.log("[Electron] Preload path:", preloadPath);
|
|
logger.log("[Electron] Preload exists:", fs.existsSync(preloadPath));
|
|
|
|
// Log environment and paths
|
|
logger.log("[Electron] process.cwd():", process.cwd());
|
|
logger.log("[Electron] __dirname:", __dirname);
|
|
logger.log("[Electron] app.getAppPath():", app.getAppPath());
|
|
logger.log("[Electron] app.isPackaged:", app.isPackaged);
|
|
logger.log("[Electron] process.resourcesPath:", process.resourcesPath);
|
|
|
|
// List files in __dirname and __dirname/www
|
|
try {
|
|
logger.log("Files in __dirname:", fs.readdirSync(__dirname));
|
|
const wwwDir = path.join(__dirname, "www");
|
|
if (fs.existsSync(wwwDir)) {
|
|
logger.log("Files in www:", fs.readdirSync(wwwDir));
|
|
} else {
|
|
logger.log("www directory does not exist in __dirname");
|
|
}
|
|
} catch (e) {
|
|
logger.error("Error reading directories:", e);
|
|
}
|
|
|
|
// Create the browser window with proper error handling
|
|
const mainWindow = new BrowserWindow({
|
|
width: 1200,
|
|
height: 800,
|
|
show: false, // Don't show until ready
|
|
webPreferences: {
|
|
nodeIntegration: false,
|
|
contextIsolation: true,
|
|
sandbox: false,
|
|
preload: preloadPath,
|
|
webSecurity: true,
|
|
allowRunningInsecureContent: false,
|
|
},
|
|
});
|
|
|
|
// Show window when ready
|
|
mainWindow.once("ready-to-show", () => {
|
|
logger.info("[Electron] Window ready to show");
|
|
mainWindow.show();
|
|
});
|
|
|
|
// Handle window errors
|
|
mainWindow.webContents.on("render-process-gone", (_event, details) => {
|
|
logger.error("[Electron] Render process gone:", details);
|
|
});
|
|
|
|
mainWindow.webContents.on(
|
|
"did-fail-load",
|
|
(_event, errorCode, errorDescription) => {
|
|
logger.error(
|
|
"[Electron] Page failed to load:",
|
|
errorCode,
|
|
errorDescription,
|
|
);
|
|
logger.error("[Electron] Failed URL:", mainWindow.webContents.getURL());
|
|
},
|
|
);
|
|
|
|
// Load the index.html
|
|
let indexPath: string;
|
|
let fileUrl: string;
|
|
|
|
if (app.isPackaged) {
|
|
indexPath = path.join(process.resourcesPath, "www", "index.html");
|
|
fileUrl = `file://${indexPath}`;
|
|
logger.info(
|
|
"[Electron] App is packaged. Using process.resourcesPath for index.html",
|
|
);
|
|
} else {
|
|
indexPath = path.resolve(__dirname, "www", "index.html");
|
|
fileUrl = `file://${indexPath}`;
|
|
logger.info(
|
|
"[Electron] App is not packaged. Using __dirname for index.html",
|
|
);
|
|
}
|
|
|
|
logger.info("[Electron] Resolved index.html path:", indexPath);
|
|
logger.info("[Electron] Using file URL:", fileUrl);
|
|
|
|
// Load the index.html with retry logic
|
|
const loadIndexHtml = async (retryCount = 0): Promise<void> => {
|
|
try {
|
|
if (mainWindow.isDestroyed()) {
|
|
logger.error(
|
|
"[Electron] Window was destroyed before loading index.html",
|
|
);
|
|
return;
|
|
}
|
|
|
|
const exists = fs.existsSync(indexPath);
|
|
logger.info(`[Electron] fs.existsSync for index.html: ${exists}`);
|
|
|
|
if (!exists) {
|
|
throw new Error(`index.html not found at path: ${indexPath}`);
|
|
}
|
|
|
|
// Try to read the file to verify it's accessible
|
|
const stats = fs.statSync(indexPath);
|
|
logger.info("[Electron] index.html stats:", {
|
|
size: stats.size,
|
|
mode: stats.mode,
|
|
uid: stats.uid,
|
|
gid: stats.gid,
|
|
});
|
|
|
|
// Try loadURL first
|
|
try {
|
|
logger.info("[Electron] Attempting to load index.html via loadURL");
|
|
await mainWindow.loadURL(fileUrl);
|
|
logger.info("[Electron] Successfully loaded index.html via loadURL");
|
|
} catch (loadUrlError) {
|
|
logger.warn(
|
|
"[Electron] loadURL failed, trying loadFile:",
|
|
loadUrlError,
|
|
);
|
|
// Fallback to loadFile
|
|
await mainWindow.loadFile(indexPath);
|
|
logger.info("[Electron] Successfully loaded index.html via loadFile");
|
|
}
|
|
} catch (error: unknown) {
|
|
const errorMessage =
|
|
error instanceof Error ? error.message : "Unknown error occurred";
|
|
logger.error("[Electron] Error loading index.html:", errorMessage);
|
|
|
|
// Retry logic
|
|
if (retryCount < 3 && !mainWindow.isDestroyed()) {
|
|
logger.info(
|
|
`[Electron] Retrying index.html load (attempt ${retryCount + 1})`,
|
|
);
|
|
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second
|
|
return loadIndexHtml(retryCount + 1);
|
|
}
|
|
|
|
// If we've exhausted retries, show error in window
|
|
if (!mainWindow.isDestroyed()) {
|
|
const errorHtml = `
|
|
<html>
|
|
<body style="font-family: sans-serif; padding: 20px;">
|
|
<h1>Error Loading Application</h1>
|
|
<p>Failed to load the application after multiple attempts.</p>
|
|
<pre style="background: #f0f0f0; padding: 10px; border-radius: 4px;">${errorMessage}</pre>
|
|
</body>
|
|
</html>
|
|
`;
|
|
await mainWindow.loadURL(
|
|
`data:text/html,${encodeURIComponent(errorHtml)}`,
|
|
);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Start loading the index.html
|
|
loadIndexHtml().catch((error: unknown) => {
|
|
logger.error("[Electron] Fatal error loading index.html:", error);
|
|
});
|
|
|
|
// Only open DevTools if not in production
|
|
if (!app.isPackaged) {
|
|
mainWindow.webContents.openDevTools({ mode: "detach" });
|
|
}
|
|
|
|
return mainWindow;
|
|
}
|
|
|
|
// Handle app ready
|
|
app.whenReady().then(createWindow);
|
|
|
|
// Handle all windows closed
|
|
app.on("window-all-closed", () => {
|
|
if (process.platform !== "darwin") {
|
|
app.quit();
|
|
}
|
|
});
|
|
|
|
app.on("activate", () => {
|
|
if (BrowserWindow.getAllWindows().length === 0) {
|
|
createWindow();
|
|
}
|
|
});
|
|
|
|
// Handle any errors
|
|
process.on("uncaughtException", (error) => {
|
|
logger.error("Uncaught Exception:", error);
|
|
});
|
|
|
|
// Set up IPC handlers for SQLite operations
|
|
ipcMain.handle("check-sqlite-availability", () => {
|
|
return sqlitePlugin !== null;
|
|
});
|
|
|
|
ipcMain.handle("sqlite-echo", async (_event, value) => {
|
|
try {
|
|
return await sqlitePlugin.echo({ value });
|
|
} catch (error) {
|
|
logger.error(
|
|
"Error in sqlite-echo:",
|
|
error,
|
|
JSON.stringify(error),
|
|
(error as any)?.stack,
|
|
);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
ipcMain.handle("sqlite-create-connection", async (_event, options) => {
|
|
try {
|
|
// Ensure database is initialized
|
|
await initializeSQLite();
|
|
|
|
if (!dbPath) {
|
|
throw new Error("Database path not initialized");
|
|
}
|
|
|
|
// Override any provided database path with our resolved path
|
|
const connectionOptions = {
|
|
...options,
|
|
database: dbPath,
|
|
readOnly: false,
|
|
mode: "rwc", // Force read-write-create mode
|
|
encryption: "no-encryption",
|
|
useNative: true,
|
|
};
|
|
|
|
logger.info(
|
|
"[Electron] Creating database connection with options:",
|
|
connectionOptions,
|
|
);
|
|
const result = await sqlitePlugin.createConnection(connectionOptions);
|
|
|
|
if (!result || typeof result !== "object") {
|
|
throw new Error(
|
|
"Failed to create database connection - invalid response",
|
|
);
|
|
}
|
|
|
|
// Wait a moment for the connection to be fully established
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
|
|
try {
|
|
// Verify connection is not read-only
|
|
const testResult = await result.query({
|
|
statement: "PRAGMA journal_mode;",
|
|
});
|
|
if (testResult?.values?.[0]?.journal_mode === "off") {
|
|
logger.error(
|
|
"[Electron] Connection opened in read-only mode despite options",
|
|
);
|
|
throw new Error("Database connection opened in read-only mode");
|
|
}
|
|
} catch (queryError) {
|
|
logger.error("[Electron] Error verifying connection:", queryError);
|
|
throw queryError;
|
|
}
|
|
|
|
logger.info("[Electron] Database connection created successfully");
|
|
return result;
|
|
} catch (error) {
|
|
logger.error("[Electron] Error in sqlite-create-connection:", error);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
ipcMain.handle("sqlite-execute", async (_event, options) => {
|
|
try {
|
|
return await sqlitePlugin.execute(options);
|
|
} catch (error) {
|
|
logger.error(
|
|
"Error in sqlite-execute:",
|
|
error,
|
|
JSON.stringify(error),
|
|
(error as any)?.stack,
|
|
);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
ipcMain.handle("sqlite-query", async (_event, options) => {
|
|
try {
|
|
return await sqlitePlugin.query(options);
|
|
} catch (error) {
|
|
logger.error(
|
|
"Error in sqlite-query:",
|
|
error,
|
|
JSON.stringify(error),
|
|
(error as any)?.stack,
|
|
);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
ipcMain.handle("sqlite-close-connection", async (_event, options) => {
|
|
try {
|
|
return await sqlitePlugin.closeConnection(options);
|
|
} catch (error) {
|
|
logger.error(
|
|
"Error in sqlite-close-connection:",
|
|
error,
|
|
JSON.stringify(error),
|
|
(error as any)?.stack,
|
|
);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
ipcMain.handle("sqlite-is-available", async () => {
|
|
return sqlitePlugin !== null;
|
|
});
|
|
|