diff --git a/.changeset/neat-crabs-lie.md b/.changeset/neat-crabs-lie.md new file mode 100644 index 000000000..c848811a2 --- /dev/null +++ b/.changeset/neat-crabs-lie.md @@ -0,0 +1,5 @@ +--- +'@powersync/web': patch +--- + +Testing diff --git a/demos/example-capacitor/android/app/capacitor.build.gradle b/demos/example-capacitor/android/app/capacitor.build.gradle index 259821da2..d46a7a3d6 100644 --- a/demos/example-capacitor/android/app/capacitor.build.gradle +++ b/demos/example-capacitor/android/app/capacitor.build.gradle @@ -9,6 +9,8 @@ android { apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" dependencies { + implementation project(':capacitor-filesystem') + implementation project(':capacitor-share') implementation project(':capacitor-splash-screen') } diff --git a/demos/example-capacitor/android/capacitor.settings.gradle b/demos/example-capacitor/android/capacitor.settings.gradle index 68ddb413e..a765310e6 100644 --- a/demos/example-capacitor/android/capacitor.settings.gradle +++ b/demos/example-capacitor/android/capacitor.settings.gradle @@ -2,5 +2,11 @@ include ':capacitor-android' project(':capacitor-android').projectDir = new File('../../../node_modules/@capacitor/android/capacitor') +include ':capacitor-filesystem' +project(':capacitor-filesystem').projectDir = new File('../../../node_modules/@capacitor/filesystem/android') + +include ':capacitor-share' +project(':capacitor-share').projectDir = new File('../../../node_modules/@capacitor/share/android') + include ':capacitor-splash-screen' project(':capacitor-splash-screen').projectDir = new File('../../../node_modules/@capacitor/splash-screen/android') diff --git a/demos/example-capacitor/ios/App/App.xcodeproj/project.pbxproj b/demos/example-capacitor/ios/App/App.xcodeproj/project.pbxproj index e9273628f..ba5de84a2 100644 --- a/demos/example-capacitor/ios/App/App.xcodeproj/project.pbxproj +++ b/demos/example-capacitor/ios/App/App.xcodeproj/project.pbxproj @@ -350,7 +350,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; INFOPLIST_FILE = App/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 13; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MARKETING_VERSION = 1.0; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; @@ -370,7 +370,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; INFOPLIST_FILE = App/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 13; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.powersync.example; diff --git a/demos/example-capacitor/ios/App/App/Info.plist b/demos/example-capacitor/ios/App/App/Info.plist index 966c88263..f7c470487 100644 --- a/demos/example-capacitor/ios/App/App/Info.plist +++ b/demos/example-capacitor/ios/App/App/Info.plist @@ -1,49 +1,53 @@ - - CFBundleDevelopmentRegion - en - CFBundleDisplayName - powersync-capacitor - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - $(MARKETING_VERSION) - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIRequiredDeviceCapabilities - - armv7 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - - + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + powersync-capacitor + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + NSDocumentsFolderUsageDescription + This app needs access to documents folder to save files + NSFileProviderDomainUsageDescription + This app needs access to manage files + + \ No newline at end of file diff --git a/demos/example-capacitor/ios/App/Podfile b/demos/example-capacitor/ios/App/Podfile index 70b65a21b..9e2944c4f 100644 --- a/demos/example-capacitor/ios/App/Podfile +++ b/demos/example-capacitor/ios/App/Podfile @@ -11,6 +11,8 @@ install! 'cocoapods', :disable_input_output_paths => true def capacitor_pods pod 'Capacitor', :path => '../../../../node_modules/@capacitor/ios' pod 'CapacitorCordova', :path => '../../../../node_modules/@capacitor/ios' + pod 'CapacitorFilesystem', :path => '../../../../node_modules/@capacitor/filesystem' + pod 'CapacitorShare', :path => '../../../../node_modules/@capacitor/share' pod 'CapacitorSplashScreen', :path => '../../../../node_modules/@capacitor/splash-screen' end diff --git a/demos/example-capacitor/ios/App/Podfile.lock b/demos/example-capacitor/ios/App/Podfile.lock index 850243219..d8e4f0f29 100644 --- a/demos/example-capacitor/ios/App/Podfile.lock +++ b/demos/example-capacitor/ios/App/Podfile.lock @@ -1,13 +1,19 @@ PODS: - - Capacitor (6.0.0): + - Capacitor (6.2.1): - CapacitorCordova - - CapacitorCordova (6.0.0) - - CapacitorSplashScreen (6.0.0): + - CapacitorCordova (6.2.1) + - CapacitorFilesystem (6.0.3): + - Capacitor + - CapacitorShare (6.0.3): + - Capacitor + - CapacitorSplashScreen (7.0.2): - Capacitor DEPENDENCIES: - "Capacitor (from `../../../../node_modules/@capacitor/ios`)" - "CapacitorCordova (from `../../../../node_modules/@capacitor/ios`)" + - "CapacitorFilesystem (from `../../../../node_modules/@capacitor/filesystem`)" + - "CapacitorShare (from `../../../../node_modules/@capacitor/share`)" - "CapacitorSplashScreen (from `../../../../node_modules/@capacitor/splash-screen`)" EXTERNAL SOURCES: @@ -15,14 +21,20 @@ EXTERNAL SOURCES: :path: "../../../../node_modules/@capacitor/ios" CapacitorCordova: :path: "../../../../node_modules/@capacitor/ios" + CapacitorFilesystem: + :path: "../../../../node_modules/@capacitor/filesystem" + CapacitorShare: + :path: "../../../../node_modules/@capacitor/share" CapacitorSplashScreen: :path: "../../../../node_modules/@capacitor/splash-screen" SPEC CHECKSUMS: - Capacitor: 559d073c4ca6c27f8e7002c807eea94c3ba435a9 - CapacitorCordova: 8c4bfdf69368512e85b1d8b724dd7546abeb30af - CapacitorSplashScreen: 5431ab8d19c1c6e95777d53bfaa7a36a6c3d94c7 + Capacitor: 1e0d0e7330dea9f983b50da737d8918abcf273f8 + CapacitorCordova: 8d93e14982f440181be7304aa9559ca631d77fff + CapacitorFilesystem: fa3099b3c3aa43a1b51362d0c999301ab1a9a752 + CapacitorShare: 7af6ca761ce62030e8e9fbd2eb82416f5ceced38 + CapacitorSplashScreen: 8d6c8cb0542a8e81585c593815db8785ed8ce454 -PODFILE CHECKSUM: 30a5df536d5e7830e635f84e1fe35fa438802eaa +PODFILE CHECKSUM: c1703336f990a4728e25eafbdd9992a7afc2008e -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/demos/example-capacitor/package.json b/demos/example-capacitor/package.json index b9609d0e9..fa973a2fe 100644 --- a/demos/example-capacitor/package.json +++ b/demos/example-capacitor/package.json @@ -21,14 +21,19 @@ "dependencies": { "@capacitor/android": "^6.0.0", "@capacitor/core": "latest", + "@capacitor/filesystem": "^6.0.0", "@capacitor/ios": "^6.0.0", + "@capacitor/share": "^6.0.0", "@capacitor/splash-screen": "latest", "@journeyapps/wa-sqlite": "^1.2.0", + "@mui/icons-material": "^7.3.1", "@powersync/react": "workspace:*", "@powersync/web": "workspace:*", + "@types/react-router-dom": "^5.3.3", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.23.0" + "react-router-dom": "^6.30.1", + "react-window": "^1.8.11" }, "devDependencies": { "@capacitor/cli": "^6.0.0", @@ -36,6 +41,7 @@ "@types/node": "^20.12.12", "@types/react": "^18.3.2", "@types/react-dom": "^18.3.0", + "@types/react-window": "^1.8.8", "vite": "^5.2.11", "vite-plugin-require": "^1.2.14", "vite-plugin-top-level-await": "^1.4.1", diff --git a/demos/example-capacitor/src/app/LogsPage.tsx b/demos/example-capacitor/src/app/LogsPage.tsx new file mode 100644 index 000000000..fadafdb3a --- /dev/null +++ b/demos/example-capacitor/src/app/LogsPage.tsx @@ -0,0 +1,216 @@ +import { Capacitor } from '@capacitor/core'; +import { Share } from '@capacitor/share'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import { + Avatar, + Button, + ButtonGroup, + Checkbox, + Divider, + FormControlLabel, + FormGroup, + Grid, + IconButton, + ListItem, + ListItemAvatar, + ListItemText, + Paper, + styled, + Typography +} from '@mui/material'; +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { FixedSizeList, ListChildComponentProps } from 'react-window'; +import { LOG_STORAGE, LogRecord } from '../components/providers/Logging.js'; + +function downloadTextFile(filename: string, content: string) { + const blob = new Blob([content], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + setTimeout(() => { + document.body.removeChild(a); + URL.revokeObjectURL(url); + }, 0); +} + +async function shareFile(content: string) { + await Share.share({ + title: 'logs', + text: content, + dialogTitle: 'Share your log file' + }); +} + +export const LogDisplay: React.FC<{ logs: ReadonlyArray> }> = React.memo((props) => { + const { logs } = props; + + const Row = ({ index, style }: ListChildComponentProps) => { + const log = logs[index]; + return ( +
+ + + + {log.level[0].toUpperCase()} + + + + + {log.timestamp} + + + {log.level.toUpperCase()} + + + } + secondary={ + + {log.message.slice(0, 100)} + {log.message.length > 100 ? '...' : ''} + + } + /> + + {index < logs.length - 1 && } +
+ ); + }; + + return ( + + + {logs.length === 0 ? ( + No logs + ) : ( + + {Row} + + )} + + + ); +}); + +const LOG_LEVELS = [ + { label: 'Error', value: 'ERROR', color: '#d32f2f' }, + { label: 'Warn', value: 'WARN', color: '#fbc02d' }, + { label: 'Debug', value: 'DEBUG', color: '#1976d2' }, + { label: 'Info', value: 'INFO', color: '#1976d2' } +]; + +const LogsPage = () => { + const [logs, setLogs] = React.useState(LOG_STORAGE.logs); + const [selectedLevels, setSelectedLevels] = React.useState(LOG_LEVELS.map((l) => l.value)); + const navigate = useNavigate(); + + React.useEffect(() => { + return LOG_STORAGE.registerListener({ + logsUpdated: (logs) => setLogs([...logs]) + }); + }, []); + + const handleLevelChange = (level: string) => { + setSelectedLevels((prev) => (prev.includes(level) ? prev.filter((l) => l !== level) : [...prev, level])); + }; + + const filteredLogs = logs.filter((log) => selectedLevels.includes(log.level)); + + return ( + + + navigate(-1)} aria-label="Back"> + + + + Logs + + + + + {LOG_LEVELS.map(({ label, value, color }) => ( + handleLevelChange(value)} + sx={{ color, '&.Mui-checked': { color } }} + /> + } + label={label} + /> + ))} + + + + + + + + + + + + + ); +}; + +namespace S { + export const MainGrid = styled(Grid)` + width: 100vw; + `; + + export const LogsListContainer = styled('div')` + width: 100%; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.07); + overflow-x: auto; + overflow-y: auto; + max-height: 1000px; + padding: 0.5rem 0; + `; +} + +export default LogsPage; diff --git a/demos/example-capacitor/src/app/index.tsx b/demos/example-capacitor/src/app/index.tsx index c8a2e01f8..8bc064d98 100644 --- a/demos/example-capacitor/src/app/index.tsx +++ b/demos/example-capacitor/src/app/index.tsx @@ -1,15 +1,32 @@ -import React from 'react'; import { createRoot } from 'react-dom/client'; -import EntryPage from './page.jsx'; + +import { createTheme, CssBaseline, ThemeProvider } from '@mui/material'; +import { BrowserRouter, Route, Routes } from 'react-router-dom'; import SystemProvider from '../components/providers/SystemProvider.jsx'; +import LogsPage from './LogsPage.jsx'; +import EntryPage from './page.jsx'; + +const theme = createTheme({ + palette: { + mode: 'dark' + } +}); const root = createRoot(document.getElementById('app')!); root.render(); export function App() { return ( - - - + + + + + + } /> + } /> + + + + ); } diff --git a/demos/example-capacitor/src/app/page.tsx b/demos/example-capacitor/src/app/page.tsx index b5f0049f5..35e125025 100644 --- a/demos/example-capacitor/src/app/page.tsx +++ b/demos/example-capacitor/src/app/page.tsx @@ -1,34 +1,15 @@ +import { Button, ButtonGroup, CircularProgress, Grid, ListItem, Paper, styled } from '@mui/material'; +import { usePowerSync, useQuery, useStatus } from '@powersync/react'; import React from 'react'; -import { CircularProgress, Grid, ListItem, styled } from '@mui/material'; -import { useQuery, useStatus } from '@powersync/react'; - -const EntryPage = () => { - const status = useStatus(); - const { data: customers } = useQuery('SELECT id, name FROM customers'); - - const areVariablesSet = import.meta.env.VITE_POWERSYNC_URL && import.meta.env.VITE_PUBLIC_POWERSYNC_TOKEN; - - if (areVariablesSet && !status.hasSynced) { - return ( - -

- Syncing down from the backend. This will load indefinitely if you have not set the connection up correctly. -

- -
- ); - } - - if (!areVariablesSet) { - return ( - -

You have not set up a connection to the backend, please connect your backend.

-
- ); - } +import { useNavigate } from 'react-router-dom'; +import LocalEditWidget from '../components/LocalEditWidget.js'; +import { Customer } from '../library/powersync/AppSchema.js'; +import { BackendConnector } from '../library/powersync/BackendConnector.js'; +const CustomersWidget: React.FC<{ customers: Customer[] }> = (props) => { + const { customers } = props; return ( - +

Customers

@@ -47,21 +28,74 @@ const EntryPage = () => { )} -
+ + ); +}; + +const EntryPage = () => { + const status = useStatus(); + const powerSync = usePowerSync(); + + const { data: customers } = useQuery('SELECT id, name FROM customers'); + const navigate = useNavigate(); + + const areVariablesSet = import.meta.env.VITE_POWERSYNC_URL && import.meta.env.VITE_PUBLIC_POWERSYNC_TOKEN; + + return ( + + + + + + + + + + + + {areVariablesSet && !status.hasSynced && ( + <> +

+ Syncing down from the backend. This will load indefinitely if you have not set the connection up + correctly. +

+ + + )} + {!areVariablesSet && ( +

+ You have not set up a connection to the backend, please connect your backend. +

+ )} + {areVariablesSet && status.hasSynced && } +
+
+ + + + +
); }; namespace S { - export const CenteredGrid = styled(Grid)` + export const FlexGrid = styled(Grid)` display: flex; - justify-content: center; - align-items: center; + flex-direction: column; + margin: 10px; `; - export const MainGrid = styled(CenteredGrid)` - min-height: 100vh; - display: flex; - flex-direction: column; + export const CenteredGrid = styled(FlexGrid)` + justify-content: center; + align-items: center; `; } diff --git a/demos/example-capacitor/src/components/LocalEditWidget.tsx b/demos/example-capacitor/src/components/LocalEditWidget.tsx new file mode 100644 index 000000000..864aac62e --- /dev/null +++ b/demos/example-capacitor/src/components/LocalEditWidget.tsx @@ -0,0 +1,61 @@ +import DeleteIcon from '@mui/icons-material/Delete'; +import { Button, IconButton, List, ListItem, ListItemText, Paper, Typography } from '@mui/material'; +import { usePowerSync, useQuery } from '@powersync/react'; +import React from 'react'; +import { Product } from '../library/powersync/AppSchema.js'; + +function getRandomProductName() { + const adjectives = ['Cool', 'Amazing', 'Fresh', 'New', 'Shiny', 'Super', 'Eco', 'Smart']; + const nouns = ['Widget', 'Gadget', 'Device', 'Item', 'Thing', 'Product', 'Tool', 'Object']; + return ( + adjectives[Math.floor(Math.random() * adjectives.length)] + + ' ' + + nouns[Math.floor(Math.random() * nouns.length)] + + ' #' + + Math.floor(Math.random() * 10000) + ); +} + +export default function LocalEditWidget() { + const powerSync = usePowerSync(); + const { data: products } = useQuery('SELECT * FROM products'); + + const addProduct = React.useCallback(() => { + return powerSync.execute('INSERT INTO products (id, name) VALUES (uuid(), ?)', [getRandomProductName()]); + }, []); + + const deleteProduct = React.useCallback((product: Product) => { + return powerSync.execute('DELETE FROM products WHERE id = ?', [product.id]); + }, []); + + return ( + + + Products + + Perform local only edits to Products. These won't be synced. + + + {products.length === 0 ? ( + + + + ) : ( + products.map((product) => ( + deleteProduct(product)}> + + + }> + + + )) + )} + + + ); +} diff --git a/demos/example-capacitor/src/components/providers/Logging.ts b/demos/example-capacitor/src/components/providers/Logging.ts new file mode 100644 index 000000000..e31d97b71 --- /dev/null +++ b/demos/example-capacitor/src/components/providers/Logging.ts @@ -0,0 +1,120 @@ +import { BaseObserver, ControlledExecutor, createBaseLogger, LogLevel } from '@powersync/web'; + +export type LogRecord = { + level: string; + timestamp: string; + message: string; +}; + +export type LogListener = { + logsUpdated: (logs: ReadonlyArray>) => void; +}; + +export class LogStorage extends BaseObserver { + protected _logs: LogRecord[]; + protected writeExecutor: ControlledExecutor; + protected dbPromise: Promise; + + static readonly LOGS_DB_NAME = '_ps_logs_db'; + static readonly LOGS_STORE_NAME = 'logs'; + + get logs(): ReadonlyArray> { + return this._logs; + } + + constructor() { + super(); + this._logs = []; + this.writeExecutor = new ControlledExecutor(() => this.saveLogsToIndexedDB()); + this.dbPromise = this.openLogsDB(); + this.readLogsFromIndexedDB(); + } + + clearLogs() { + this._logs = []; + this.iterateListeners((l) => l.logsUpdated?.(this.logs)); + } + + writeLog(record: LogRecord) { + this._logs.unshift(record); + this.iterateListeners((l) => l.logsUpdated?.(this.logs)); + this.writeExecutor.schedule(); + } + + private async saveLogsToIndexedDB() { + const db = await this.dbPromise; + const tx = db.transaction(LogStorage.LOGS_STORE_NAME, 'readwrite'); + const store = tx.objectStore(LogStorage.LOGS_STORE_NAME); + await new Promise((resolve, reject) => { + const clearReq = store.clear(); + clearReq.onsuccess = () => { + this.logs.forEach((log) => store.add(log)); + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); + }; + clearReq.onerror = () => reject(clearReq.error); + }); + } + + // Read all LogRecord entries from IndexedDB + private async readLogsFromIndexedDB(): Promise { + const db = await this.dbPromise; + const tx = db.transaction(LogStorage.LOGS_STORE_NAME, 'readwrite'); + const store = tx.objectStore(LogStorage.LOGS_STORE_NAME); + return new Promise((resolve, reject) => { + const req = store.getAll(); + req.onsuccess = () => { + this._logs = (req.result as any[]) + .map(({ id, ...rest }) => rest) + // sort in descending order + .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); + resolve(this._logs); + this.iterateListeners((l) => l.logsUpdated?.(this.logs)); + }; + req.onerror = () => reject(req.error); + }); + } + + // Helper to open the IndexedDB database + private async openLogsDB(): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open(LogStorage.LOGS_DB_NAME, 1); + request.onupgradeneeded = () => { + const db = request.result; + if (!db.objectStoreNames.contains(LogStorage.LOGS_STORE_NAME)) { + db.createObjectStore(LogStorage.LOGS_STORE_NAME, { keyPath: 'id', autoIncrement: true }); + } + }; + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + } +} + +export const LOG_STORAGE = new LogStorage(); + +const mapLogParam = (param: any) => { + if (param instanceof Error) { + return ` +Error +Name: ${param.name} +Message: ${param.message} +Cause: ${param.cause} +Stack: ${param.stack} + `.trim(); + } + return JSON.stringify(param); +}; + +// Configure base logger for global settings +const logger = createBaseLogger(); +logger.useDefaults({ + defaultLevel: LogLevel.DEBUG, + formatter: (messages, context) => { + LOG_STORAGE.writeLog({ + level: context.level.name, + message: messages.map(mapLogParam).join(' -- '), + timestamp: new Date().toISOString() + }); + } +}); diff --git a/demos/example-capacitor/src/components/providers/SystemProvider.tsx b/demos/example-capacitor/src/components/providers/SystemProvider.tsx index fc3b9e050..e4a2ad124 100644 --- a/demos/example-capacitor/src/components/providers/SystemProvider.tsx +++ b/demos/example-capacitor/src/components/providers/SystemProvider.tsx @@ -1,30 +1,25 @@ -import { PowerSyncContext } from '@powersync/react'; -import { PowerSyncDatabase, createBaseLogger, LogLevel } from '@powersync/web'; +import { Capacitor } from '@capacitor/core'; import { CircularProgress } from '@mui/material'; +import { PowerSyncContext } from '@powersync/react'; +import { PowerSyncDatabase, WASQLiteOpenFactory, WASQLiteVFS } from '@powersync/web'; import React, { Suspense } from 'react'; import { AppSchema } from '../../library/powersync/AppSchema.js'; import { BackendConnector } from '../../library/powersync/BackendConnector.js'; -import { Capacitor } from '@capacitor/core'; - -const logger = createBaseLogger(); -logger.useDefaults(); -logger.setLevel(LogLevel.DEBUG); const platform = Capacitor.getPlatform(); -const isIOs = platform === 'ios'; -// Web worker implementation does not work on iOS -const useWebWorker = !isIOs; const powerSync = new PowerSyncDatabase({ - database: { dbFilename: 'powersync2.db' }, + database: new WASQLiteOpenFactory({ + dbFilename: 'ps.db', + vfs: platform == 'ios' ? WASQLiteVFS.AccessHandlePoolVFS : WASQLiteVFS.OPFSCoopSyncVFS, + debugMode: true + }), schema: AppSchema, flags: { - enableMultiTabs: false, - useWebWorker + enableMultiTabs: false } }); const connector = new BackendConnector(); - powerSync.connect(connector); export const SystemProvider = ({ children }: { children: React.ReactNode }) => { diff --git a/demos/example-capacitor/src/index.css b/demos/example-capacitor/src/index.css index 8856f90b3..a89143ed2 100644 --- a/demos/example-capacitor/src/index.css +++ b/demos/example-capacitor/src/index.css @@ -1,7 +1,10 @@ body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, - Arial, sans-serif; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; margin: auto; max-width: 38rem; padding: 2rem; + padding-top: env(safe-area-inset-top); + padding-bottom: env(safe-area-inset-bottom); + padding-left: env(safe-area-inset-left); + padding-right: env(safe-area-inset-right); } diff --git a/demos/example-capacitor/src/index.html b/demos/example-capacitor/src/index.html index de7a97c29..27e149188 100644 --- a/demos/example-capacitor/src/index.html +++ b/demos/example-capacitor/src/index.html @@ -1,11 +1,15 @@ - - - - - - - -
- - + + + + + + + + + + +
+ + + \ No newline at end of file diff --git a/demos/example-capacitor/src/library/powersync/AppSchema.ts b/demos/example-capacitor/src/library/powersync/AppSchema.ts index 3569f78fc..f24887f76 100644 --- a/demos/example-capacitor/src/library/powersync/AppSchema.ts +++ b/demos/example-capacitor/src/library/powersync/AppSchema.ts @@ -5,8 +5,18 @@ const customers = new Table({ created_at: column.text }); +const products = new Table( + { + name: column.text + }, + { + localOnly: true + } +); + export const AppSchema = new Schema({ - customers + customers, + products }); export type Database = (typeof AppSchema)['types']; diff --git a/demos/example-capacitor/tsconfig.json b/demos/example-capacitor/tsconfig.json index 98800e2e9..59dfe0221 100644 --- a/demos/example-capacitor/tsconfig.json +++ b/demos/example-capacitor/tsconfig.json @@ -11,7 +11,7 @@ "outDir": "dist", "moduleResolution": "NodeNext", "resolveJsonModule": true, - "jsx": "preserve" + "jsx": "react-jsx" }, "references": [ { diff --git a/demos/example-capacitor/vite.config.ts b/demos/example-capacitor/vite.config.ts index e36be6a57..f0bcf31dc 100644 --- a/demos/example-capacitor/vite.config.ts +++ b/demos/example-capacitor/vite.config.ts @@ -1,5 +1,5 @@ -import wasm from 'vite-plugin-wasm'; import topLevelAwait from 'vite-plugin-top-level-await'; +import wasm from 'vite-plugin-wasm'; import { defineConfig } from 'vite'; diff --git a/packages/common/src/client/SQLOpenFactory.ts b/packages/common/src/client/SQLOpenFactory.ts index 44d5b21e4..3c3b34779 100644 --- a/packages/common/src/client/SQLOpenFactory.ts +++ b/packages/common/src/client/SQLOpenFactory.ts @@ -1,3 +1,4 @@ +import { ILogger } from 'js-logger'; import { DBAdapter } from '../db/DBAdapter.js'; export interface SQLOpenOptions { @@ -23,6 +24,8 @@ export interface SQLOpenOptions { * debugMode: process.env.NODE_ENV !== 'production' */ debugMode?: boolean; + + logger?: ILogger; } export interface SQLOpenFactory { diff --git a/packages/web/src/db/adapters/AsyncDatabaseConnection.ts b/packages/web/src/db/adapters/AsyncDatabaseConnection.ts index 183d5210b..f78d80096 100644 --- a/packages/web/src/db/adapters/AsyncDatabaseConnection.ts +++ b/packages/web/src/db/adapters/AsyncDatabaseConnection.ts @@ -32,9 +32,24 @@ export interface AsyncDatabaseConnection; } +/** + * @internal + */ +export interface DBWorkerLogEvent { + loggerName: string; + logLevel: string; + messages: string[]; +} + +/** + * @internal + */ +export type WorkerLogHandler = (event: DBWorkerLogEvent) => void; + /** * @internal */ export type OpenAsyncDatabaseConnection = ( - config: Config + config: Config, + logger?: WorkerLogHandler ) => AsyncDatabaseConnection; diff --git a/packages/web/src/db/adapters/wa-sqlite/WASQLiteConnection.ts b/packages/web/src/db/adapters/wa-sqlite/WASQLiteConnection.ts index 091c85da4..57b16ddc0 100644 --- a/packages/web/src/db/adapters/wa-sqlite/WASQLiteConnection.ts +++ b/packages/web/src/db/adapters/wa-sqlite/WASQLiteConnection.ts @@ -1,5 +1,5 @@ import * as SQLite from '@journeyapps/wa-sqlite'; -import { BaseObserver, BatchedUpdateNotification } from '@powersync/common'; +import { BaseObserver, BatchedUpdateNotification, ILogger } from '@powersync/common'; import { Mutex } from 'async-mutex'; import { AsyncDatabaseConnection, OnTableChangeCallback, ProxiedQueryResult } from '../AsyncDatabaseConnection'; import { ResolvedWASQLiteOpenFactoryOptions } from './WASQLiteOpenFactory'; @@ -36,7 +36,12 @@ export type SQLiteModule = Parameters[0]; /** * @internal */ -export type WASQLiteModuleFactoryOptions = { dbFileName: string; encryptionKey?: string }; +export type WASQLiteModuleFactoryOptions = { + dbFileName: string; + logger?: ILogger; + encryptionKey?: string; + debugMode?: boolean; +}; /** * @internal @@ -104,9 +109,17 @@ export const DEFAULT_MODULE_FACTORIES = { } // @ts-expect-error The types for this static method are missing upstream const { AccessHandlePoolVFS } = await import('@journeyapps/wa-sqlite/src/examples/AccessHandlePoolVFS.js'); + const vfs = await AccessHandlePoolVFS.create(options.dbFileName, module); + + // TODO, maybe use a different flag for this (if we want to actually expose this) + if (options.debugMode && options.logger) { + // Enable VFS logs + vfs.log = (...params: any[]) => options.logger?.debug(...params); + } + return { module, - vfs: await AccessHandlePoolVFS.create(options.dbFileName, module) + vfs }; }, [WASQLiteVFS.OPFSCoopSyncVFS]: async (options: WASQLiteModuleFactoryOptions) => { @@ -187,7 +200,9 @@ export class WASqliteConnection protected async openSQLiteAPI(): Promise { const { module, vfs } = await this._moduleFactory({ dbFileName: this.options.dbFilename, - encryptionKey: this.options.encryptionKey + encryptionKey: this.options.encryptionKey, + debugMode: this.options.debugMode, + logger: this.options.logger }); const sqlite3 = SQLite.Factory(module); sqlite3.vfs_register(vfs, true); diff --git a/packages/web/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts b/packages/web/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts index 129a3b130..45d7c4719 100644 --- a/packages/web/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts +++ b/packages/web/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts @@ -2,7 +2,7 @@ import { type ILogLevel, DBAdapter } from '@powersync/common'; import * as Comlink from 'comlink'; import { openWorkerDatabasePort, resolveWorkerDatabasePortFactory } from '../../../worker/db/open-worker-database'; import { AbstractWebSQLOpenFactory } from '../AbstractWebSQLOpenFactory'; -import { AsyncDatabaseConnection, OpenAsyncDatabaseConnection } from '../AsyncDatabaseConnection'; +import { AsyncDatabaseConnection, DBWorkerLogEvent, OpenAsyncDatabaseConnection } from '../AsyncDatabaseConnection'; import { LockedAsyncDatabaseAdapter } from '../LockedAsyncDatabaseAdapter'; import { DEFAULT_CACHE_SIZE_KB, @@ -21,7 +21,7 @@ export interface ResolvedWASQLiteOpenFactoryOptions extends ResolvedWebSQLOpenOp vfs: WASQLiteVFS; } -export interface WorkerDBOpenerOptions extends ResolvedWASQLiteOpenFactoryOptions { +export interface WorkerDBOpenerOptions extends Omit { logLevel: ILogLevel; } @@ -82,15 +82,22 @@ export class WASQLiteOpenFactory extends AbstractWebSQLOpenFactory { return new WorkerWrappedAsyncDatabaseConnection({ remote: workerDBOpener, - baseConnection: await workerDBOpener({ - dbFilename: this.options.dbFilename, - vfs, - temporaryStorage, - cacheSizeKb, - flags: this.resolvedFlags, - encryptionKey: encryptionKey, - logLevel: this.logger.getLevel() - }), + baseConnection: await workerDBOpener( + { + dbFilename: this.options.dbFilename, + vfs, + temporaryStorage, + cacheSizeKb, + flags: this.resolvedFlags, + encryptionKey: encryptionKey, + logLevel: this.logger.getLevel(), + debugMode: this.options.debugMode + }, + Comlink.proxy((event: DBWorkerLogEvent) => { + // TODO + (this.logger as any)[event.logLevel.toLocaleLowerCase()]('[DB Worker] ', ...event.messages); + }) + ), identifier: this.options.dbFilename, onClose: () => { if (workerPort instanceof Worker) { diff --git a/packages/web/src/worker/db/LogHandler.ts b/packages/web/src/worker/db/LogHandler.ts new file mode 100644 index 000000000..990500b84 --- /dev/null +++ b/packages/web/src/worker/db/LogHandler.ts @@ -0,0 +1,17 @@ +import { BaseListener, BaseObserver } from '@powersync/common'; + +export type LogEvent = { + loggerName: string; + logLevel: string; + messages: string[]; +}; + +export interface LogHandlerListener extends BaseListener { + onLog: (event: LogEvent) => void; +} + +export class LogHandler extends BaseObserver { + pushLog(entry: LogEvent) { + this.iterateListeners((l) => l.onLog?.(entry)); + } +} diff --git a/packages/web/src/worker/db/WASQLiteDB.worker.ts b/packages/web/src/worker/db/WASQLiteDB.worker.ts index 3db4bdc49..ed5dfb817 100644 --- a/packages/web/src/worker/db/WASQLiteDB.worker.ts +++ b/packages/web/src/worker/db/WASQLiteDB.worker.ts @@ -3,19 +3,16 @@ */ import '@journeyapps/wa-sqlite'; -import { createBaseLogger, createLogger } from '@powersync/common'; +import { createBaseLogger, createLogger, ILogHandler, LogLevel } from '@powersync/common'; import * as Comlink from 'comlink'; -import { AsyncDatabaseConnection } from '../../db/adapters/AsyncDatabaseConnection'; +import { AsyncDatabaseConnection, WorkerLogHandler } from '../../db/adapters/AsyncDatabaseConnection'; import { WASqliteConnection } from '../../db/adapters/wa-sqlite/WASQLiteConnection'; import { ResolvedWASQLiteOpenFactoryOptions, WorkerDBOpenerOptions } from '../../db/adapters/wa-sqlite/WASQLiteOpenFactory'; import { getNavigatorLocks } from '../../shared/navigator'; - -const baseLogger = createBaseLogger(); -baseLogger.useDefaults(); -const logger = createLogger('db-worker'); +import { LogHandler } from './LogHandler'; /** * Keeps track of open DB connections and the clients which @@ -31,11 +28,60 @@ const OPEN_DB_LOCK = 'open-wasqlite-db'; let nextClientId = 1; +function logUnhandledException(error: Error) { + const errorMessage = ` + Name: ${error.name} + Cause: ${error.cause} + Message: ${error.message} + Stack: ${error.stack}`.trim(); + + for (const dbFilename of DBMap.keys()) { + logBroadcaster.pushLog({ + loggerName: dbFilename, + logLevel: LogLevel.ERROR.name, + messages: ['Uncaught Exception in DB worker', errorMessage] + }); + } +} + +// Report unhandled exceptions to all loggers +addEventListener('unhandledrejection', (event) => { + logUnhandledException(event.reason); +}); + +addEventListener('error', (event) => { + logUnhandledException(event.error); +}); + +const baseLogger = createBaseLogger(); + +const logBroadcaster = new LogHandler(); + +const defaultHandler = baseLogger.createDefaultHandler(); +const logHandler: ILogHandler = (messages, context) => { + logBroadcaster.pushLog({ + loggerName: context.name ?? 'unknown', + logLevel: context.level.name, + messages: messages.map((m) => String(m)) + }); + defaultHandler(messages, context); +}; + +baseLogger.useDefaults({ + formatter: logHandler +}); + +const workerLogger = createLogger('db-worker'); + const openWorkerConnection = async (options: ResolvedWASQLiteOpenFactoryOptions): Promise => { const connection = new WASqliteConnection(options); return { init: Comlink.proxy(() => connection.init()), - getConfig: Comlink.proxy(() => connection.getConfig()), + getConfig: Comlink.proxy(async () => { + // Can't send the logger over + const { logger, ...rest } = await connection.getConfig(); + return rest; + }), close: Comlink.proxy(() => connection.close()), execute: Comlink.proxy(async (sql: string, params?: any[]) => connection.execute(sql, params)), executeRaw: Comlink.proxy(async (sql: string, params?: any[]) => connection.executeRaw(sql, params)), @@ -47,17 +93,38 @@ const openWorkerConnection = async (options: ResolvedWASQLiteOpenFactoryOptions) }; }; -const openDBShared = async (options: WorkerDBOpenerOptions): Promise => { +const openDBShared = async ( + options: WorkerDBOpenerOptions, + logHandler?: WorkerLogHandler +): Promise => { // Prevent multiple simultaneous opens from causing race conditions return getNavigatorLocks().request(OPEN_DB_LOCK, async () => { const clientId = nextClientId++; const { dbFilename, logLevel } = options; - logger.setLevel(logLevel); + // This updates the log level for the worker-level logger + // The DB connection logger will automatically track the main context logger + // since it passes logs to it. + workerLogger.setLevel(logLevel); + + let disposeLogListener = logHandler + ? logBroadcaster.registerListener({ + onLog: (event) => { + if (event.loggerName !== dbFilename) { + return; + } + logHandler(event); + } + }) + : null; if (!DBMap.has(dbFilename)) { const clientIds = new Set(); - const connection = await openWorkerConnection(options); + const logger = createLogger(dbFilename); + const connection = await openWorkerConnection({ + ...options, + logger + }); await connection.init(); DBMap.set(dbFilename, { clientIds, @@ -76,14 +143,15 @@ const openDBShared = async (options: WorkerDBOpenerOptions): Promise { const { clientIds } = dbEntry; - logger.debug(`Close requested from client ${clientId} of ${[...clientIds]}`); + disposeLogListener?.(); + workerLogger.debug(`Close requested from client ${clientId} of ${[...clientIds]}`); clientIds.delete(clientId); if (clientIds.size == 0) { - logger.debug(`Closing connection to ${dbFilename}.`); + workerLogger.debug(`Closing connection to ${dbFilename}.`); DBMap.delete(dbFilename); return db.close?.(); } - logger.debug(`Connection to ${dbFilename} not closed yet due to active clients.`); + workerLogger.debug(`Connection to ${dbFilename} not closed yet due to active clients.`); return; }) }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 899edee2f..806d5d66f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -271,21 +271,33 @@ importers: '@capacitor/core': specifier: latest version: 7.4.2 + '@capacitor/filesystem': + specifier: ^6.0.0 + version: 6.0.3(@capacitor/core@7.4.2) '@capacitor/ios': specifier: ^6.0.0 version: 6.2.1(@capacitor/core@7.4.2) + '@capacitor/share': + specifier: ^6.0.0 + version: 6.0.3(@capacitor/core@7.4.2) '@capacitor/splash-screen': specifier: latest - version: 7.0.1(@capacitor/core@7.4.2) + version: 7.0.2(@capacitor/core@7.4.2) '@journeyapps/wa-sqlite': specifier: ^1.2.0 version: 1.2.6 + '@mui/icons-material': + specifier: ^7.3.1 + version: 7.3.1(@mui/material@5.17.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.23)(react@18.3.1) '@powersync/react': specifier: workspace:* version: link:../../packages/react '@powersync/web': specifier: workspace:* version: link:../../packages/web + '@types/react-router-dom': + specifier: ^5.3.3 + version: 5.3.3 react: specifier: ^18.2.0 version: 18.3.1 @@ -293,8 +305,11 @@ importers: specifier: ^18.2.0 version: 18.3.1(react@18.3.1) react-router-dom: - specifier: ^6.23.0 + specifier: ^6.30.1 version: 6.30.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-window: + specifier: ^1.8.11 + version: 1.8.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1) devDependencies: '@capacitor/cli': specifier: ^6.0.0 @@ -311,6 +326,9 @@ importers: '@types/react-dom': specifier: ^18.3.0 version: 18.3.6(@types/react@18.3.23) + '@types/react-window': + specifier: ^1.8.8 + version: 1.8.8 vite: specifier: ^5.2.11 version: 5.4.19(@types/node@20.17.57)(less@4.2.2)(lightningcss@1.30.1)(sass@1.89.1)(terser@5.40.0) @@ -847,7 +865,7 @@ importers: version: 1.23.1(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1)) '@shopify/flash-list': specifier: 1.7.3 - version: 1.7.3(@babel/runtime@7.27.6)(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) + version: 1.7.3(@babel/runtime@7.28.2)(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) '@supabase/supabase-js': specifier: 2.39.0 version: 2.39.0 @@ -3486,6 +3504,10 @@ packages: resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.28.2': + resolution: {integrity: sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==} + engines: {node: '>=6.9.0'} + '@babel/template@7.27.2': resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} @@ -3522,13 +3544,23 @@ packages: '@capacitor/core@7.4.2': resolution: {integrity: sha512-akCf9A1FUR8AWTtmgGjHEq6LmGsjA2U7igaJ9PxiCBfyxKqlDbuGHrlNdpvHEjV5tUPH3KYtkze6gtFcNKPU9A==} + '@capacitor/filesystem@6.0.3': + resolution: {integrity: sha512-PdIP/yOGAbG1lq1wbFbSPhXQ9/5lpTpeiok2NneawJOk6UXvy9W7QZXRo7wXAP7J6FdzU7bKfOORRXpOJpgXyw==} + peerDependencies: + '@capacitor/core': ^6.0.0 + '@capacitor/ios@6.2.1': resolution: {integrity: sha512-tbMlQdQjxe1wyaBvYVU1yTojKJjgluZQsJkALuJxv/6F8QTw5b6vd7X785O/O7cMpIAZfUWo/vtAHzFkRV+kXw==} peerDependencies: '@capacitor/core': ^6.2.0 - '@capacitor/splash-screen@7.0.1': - resolution: {integrity: sha512-Nbqw9bEIe7uHj/HOT81mf4jT6uK1YykozpQw/uIKQDueMg6RJYaJK2/TMajIOohLk8fJF4TYIc1i9nGjNLnfGg==} + '@capacitor/share@6.0.3': + resolution: {integrity: sha512-BkNM73Ix+yxQ7fkni8CrrGcp1kSl7u+YNoPLwWKQ1MuQ5Uav0d+CT8M67ie+3dc4jASmegnzlC6tkTmFcPTLeA==} + peerDependencies: + '@capacitor/core': ^6.0.0 + + '@capacitor/splash-screen@7.0.2': + resolution: {integrity: sha512-bchh4F73CnVONm6XFEgXKEhbSEDQh2CQ0rNSoasIeJ5pf9JqHkkPS3t0Fnm33qHkLVFcaPoKPW69Y9zMpT5Vxg==} peerDependencies: '@capacitor/core': '>=7.0.0' @@ -5935,6 +5967,17 @@ packages: '@types/react': optional: true + '@mui/icons-material@7.3.1': + resolution: {integrity: sha512-upzCtG6awpL6noEZlJ5Z01khZ9VnLNLaj7tb6iPbN6G97eYfUTs8e9OyPKy3rEms3VQWmVBfri7jzeaRxdFIzA==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@mui/material': ^7.3.1 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@mui/material@5.17.1': resolution: {integrity: sha512-2B33kQf+GmPnrvXXweWAx+crbiUEsxCdCN979QDYnlH9ox4pd+0/IBriWLV+l6ORoBF60w39cWjFnJYGFdzXcw==} engines: {node: '>=12.0.0'} @@ -9013,6 +9056,9 @@ packages: peerDependencies: '@types/react': '*' + '@types/react-window@1.8.8': + resolution: {integrity: sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==} + '@types/react@18.3.1': resolution: {integrity: sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==} @@ -17800,6 +17846,13 @@ packages: react: '>=16.6.0' react-dom: '>=16.6.0' + react-window@1.8.11: + resolution: {integrity: sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ==} + engines: {node: '>8.0.0'} + peerDependencies: + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -22492,6 +22545,8 @@ snapshots: '@babel/runtime@7.27.6': {} + '@babel/runtime@7.28.2': {} + '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 @@ -22555,11 +22610,19 @@ snapshots: dependencies: tslib: 2.8.1 + '@capacitor/filesystem@6.0.3(@capacitor/core@7.4.2)': + dependencies: + '@capacitor/core': 7.4.2 + '@capacitor/ios@6.2.1(@capacitor/core@7.4.2)': dependencies: '@capacitor/core': 7.4.2 - '@capacitor/splash-screen@7.0.1(@capacitor/core@7.4.2)': + '@capacitor/share@6.0.3(@capacitor/core@7.4.2)': + dependencies: + '@capacitor/core': 7.4.2 + + '@capacitor/splash-screen@7.0.2(@capacitor/core@7.4.2)': dependencies: '@capacitor/core': 7.4.2 @@ -26489,6 +26552,14 @@ snapshots: optionalDependencies: '@types/react': 18.3.23 + '@mui/icons-material@7.3.1(@mui/material@5.17.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.23)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.2 + '@mui/material': 5.17.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + '@mui/material@5.17.1(@emotion/react@11.11.4(@types/react@18.3.23)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.27.6 @@ -29273,9 +29344,9 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} - '@shopify/flash-list@1.7.3(@babel/runtime@7.27.6)(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)': + '@shopify/flash-list@1.7.3(@babel/runtime@7.28.2)(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)': dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.2 react: 18.3.1 react-native: 0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1) recyclerlistview: 4.2.1(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) @@ -31070,6 +31141,10 @@ snapshots: dependencies: '@types/react': 18.3.23 + '@types/react-window@1.8.8': + dependencies: + '@types/react': 18.3.23 + '@types/react@18.3.1': dependencies: '@types/prop-types': 15.7.14 @@ -43461,6 +43536,13 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + react-window@1.8.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.27.6 + memoize-one: 5.2.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react@18.3.1: dependencies: loose-envify: 1.4.0