|
1 | 1 | import AdmZip from 'adm-zip'; |
2 | 2 | import { XMLBuilder } from 'fast-xml-parser'; |
3 | 3 | import { AACTree, AACPage, AACButton } from '../../core/treeStructure'; |
| 4 | +import * as fs from 'fs'; |
| 5 | +import * as path from 'path'; |
| 6 | +import { execSync } from 'child_process'; |
4 | 7 |
|
5 | 8 | function normalizeZipPath(p: string): string { |
6 | 9 | const unified = p.replace(/\\/g, '/'); |
@@ -125,3 +128,189 @@ export function createFileMapXml( |
125 | 128 |
|
126 | 129 | return builder.build(fileMapData); |
127 | 130 | } |
| 131 | + |
| 132 | +/** |
| 133 | + * Grid3 user data path information |
| 134 | + */ |
| 135 | +export interface Grid3UserPath { |
| 136 | + userName: string; |
| 137 | + langCode: string; |
| 138 | + basePath: string; |
| 139 | + historyDbPath: string; |
| 140 | +} |
| 141 | + |
| 142 | +export interface Grid3VocabularyPath { |
| 143 | + userName: string; |
| 144 | + gridsetPath: string; |
| 145 | +} |
| 146 | + |
| 147 | +/** |
| 148 | + * Get the Windows Common Documents folder path from registry |
| 149 | + * Falls back to default path if registry access fails |
| 150 | + * @returns Path to Common Documents folder |
| 151 | + */ |
| 152 | +export function getCommonDocumentsPath(): string { |
| 153 | + // Only works on Windows |
| 154 | + if (process.platform !== 'win32') { |
| 155 | + return ''; |
| 156 | + } |
| 157 | + |
| 158 | + try { |
| 159 | + // Query registry for Common Documents path |
| 160 | + const command = |
| 161 | + 'REG.EXE QUERY "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders" /V "Common Documents"'; |
| 162 | + const output = execSync(command, { encoding: 'utf-8', windowsHide: true }); |
| 163 | + |
| 164 | + // Parse the output to extract the path |
| 165 | + const match = output.match(/Common Documents\s+REG_SZ\s+(.+)/); |
| 166 | + if (match && match[1]) { |
| 167 | + return match[1].trim(); |
| 168 | + } |
| 169 | + } catch (error) { |
| 170 | + // Registry access failed, fall back to default |
| 171 | + } |
| 172 | + |
| 173 | + // Default fallback path |
| 174 | + return 'C:\\Users\\Public\\Documents'; |
| 175 | +} |
| 176 | + |
| 177 | +/** |
| 178 | + * Find all Grid3 user data paths |
| 179 | + * Searches for users and language codes in the Grid3 directory structure |
| 180 | + * C:\Users\Public\Documents\Smartbox\Grid 3\Users\{UserName}\{langCode}\Phrases\history.sqlite |
| 181 | + * @returns Array of Grid3 user path information |
| 182 | + */ |
| 183 | +export function findGrid3UserPaths(): Grid3UserPath[] { |
| 184 | + const results: Grid3UserPath[] = []; |
| 185 | + |
| 186 | + // Only works on Windows |
| 187 | + if (process.platform !== 'win32') { |
| 188 | + return results; |
| 189 | + } |
| 190 | + |
| 191 | + try { |
| 192 | + const commonDocs = getCommonDocumentsPath(); |
| 193 | + const grid3BasePath = path.join(commonDocs, 'Smartbox', 'Grid 3', 'Users'); |
| 194 | + |
| 195 | + // Check if Grid3 Users directory exists |
| 196 | + if (!fs.existsSync(grid3BasePath)) { |
| 197 | + return results; |
| 198 | + } |
| 199 | + |
| 200 | + // Enumerate users |
| 201 | + const users = fs.readdirSync(grid3BasePath, { withFileTypes: true }); |
| 202 | + |
| 203 | + for (const userDir of users) { |
| 204 | + if (!userDir.isDirectory()) continue; |
| 205 | + |
| 206 | + const userName = userDir.name; |
| 207 | + const userPath = path.join(grid3BasePath, userName); |
| 208 | + |
| 209 | + // Enumerate language codes |
| 210 | + const langDirs = fs.readdirSync(userPath, { withFileTypes: true }); |
| 211 | + |
| 212 | + for (const langDir of langDirs) { |
| 213 | + if (!langDir.isDirectory()) continue; |
| 214 | + |
| 215 | + const langCode = langDir.name; |
| 216 | + const basePath = path.join(userPath, langCode); |
| 217 | + const historyDbPath = path.join(basePath, 'Phrases', 'history.sqlite'); |
| 218 | + |
| 219 | + // Only include if history database exists |
| 220 | + if (fs.existsSync(historyDbPath)) { |
| 221 | + results.push({ |
| 222 | + userName, |
| 223 | + langCode, |
| 224 | + basePath, |
| 225 | + historyDbPath, |
| 226 | + }); |
| 227 | + } |
| 228 | + } |
| 229 | + } |
| 230 | + } catch (error) { |
| 231 | + // Silently fail if directory access fails |
| 232 | + } |
| 233 | + |
| 234 | + return results; |
| 235 | +} |
| 236 | + |
| 237 | +/** |
| 238 | + * Find all Grid3 history database paths |
| 239 | + * Convenience method that returns just the database file paths |
| 240 | + * @returns Array of paths to history.sqlite files |
| 241 | + */ |
| 242 | +export function findGrid3HistoryDatabases(): string[] { |
| 243 | + return findGrid3UserPaths().map((userPath) => userPath.historyDbPath); |
| 244 | +} |
| 245 | + |
| 246 | +/** |
| 247 | + * Get Grid 3 users (alias of findGrid3UserPaths for clarity) |
| 248 | + */ |
| 249 | +export function findGrid3Users(): Grid3UserPath[] { |
| 250 | + return findGrid3UserPaths(); |
| 251 | +} |
| 252 | + |
| 253 | +/** |
| 254 | + * Find Grid 3 gridset/vocabulary files for each user |
| 255 | + * @param userName Optional user filter; matches case-insensitively |
| 256 | + * @returns Array of user/gridset path pairs |
| 257 | + */ |
| 258 | +export function findGrid3Vocabularies(userName?: string): Grid3VocabularyPath[] { |
| 259 | + const results: Grid3VocabularyPath[] = []; |
| 260 | + |
| 261 | + if (process.platform !== 'win32') { |
| 262 | + return results; |
| 263 | + } |
| 264 | + |
| 265 | + const commonDocs = getCommonDocumentsPath(); |
| 266 | + const grid3BasePath = path.join(commonDocs, 'Smartbox', 'Grid 3', 'Users'); |
| 267 | + |
| 268 | + if (!fs.existsSync(grid3BasePath)) { |
| 269 | + return results; |
| 270 | + } |
| 271 | + |
| 272 | + const normalizedUser = userName?.toLowerCase(); |
| 273 | + const users = fs.readdirSync(grid3BasePath, { withFileTypes: true }); |
| 274 | + |
| 275 | + for (const userDir of users) { |
| 276 | + if (!userDir.isDirectory()) continue; |
| 277 | + if (normalizedUser && userDir.name.toLowerCase() !== normalizedUser) continue; |
| 278 | + |
| 279 | + const userRoot = path.join(grid3BasePath, userDir.name); |
| 280 | + const gridSetsDir = path.join(userRoot, 'Grid Sets'); |
| 281 | + if (!fs.existsSync(gridSetsDir)) continue; |
| 282 | + |
| 283 | + const entries = fs.readdirSync(gridSetsDir, { withFileTypes: true }); |
| 284 | + for (const entry of entries) { |
| 285 | + if (!entry.isFile()) continue; |
| 286 | + const ext = path.extname(entry.name).toLowerCase(); |
| 287 | + if (ext === '.gridset' || ext === '.grd' || ext === '.grdl') { |
| 288 | + results.push({ |
| 289 | + userName: userDir.name, |
| 290 | + gridsetPath: path.join(gridSetsDir, entry.name), |
| 291 | + }); |
| 292 | + } |
| 293 | + } |
| 294 | + } |
| 295 | + |
| 296 | + return results; |
| 297 | +} |
| 298 | + |
| 299 | +/** |
| 300 | + * Find a specific user's Grid 3 history database |
| 301 | + * @param userName User name to search for (case-insensitive) |
| 302 | + * @param langCode Optional language code filter (case-insensitive) |
| 303 | + * @returns Path to history.sqlite or null if not found |
| 304 | + */ |
| 305 | +export function findGrid3UserHistory(userName: string, langCode?: string): string | null { |
| 306 | + const normalizedUser = userName.toLowerCase(); |
| 307 | + const normalizedLang = langCode?.toLowerCase(); |
| 308 | + |
| 309 | + const match = findGrid3UserPaths().find( |
| 310 | + (u) => |
| 311 | + u.userName.toLowerCase() === normalizedUser && |
| 312 | + (!normalizedLang || u.langCode.toLowerCase() === normalizedLang) |
| 313 | + ); |
| 314 | + |
| 315 | + return match?.historyDbPath ?? null; |
| 316 | +} |
0 commit comments