feat(import): one Import-file entry + vault notebooks
All checks were successful
CI / Windows build (push) Successful in 14m14s
All checks were successful
CI / Windows build (push) Successful in 14m14s
Phase 3. Import becomes a single top-level action beside "Create notebook" and the library is vault-backed. - VaultService.createNotebook copies a picked file into a fresh (de-duplicated) notebook folder under the vault; its sidecar lives beside it, so annotations travel with the file. - Home screen: one "Import file" action with a multi-extension picker (pdf / docx / pptx); routes to the editor by extension. - The document list is now a vault scan (folders with a source file), not the SQLite documents table — no cache, always correct. - PPTX soffice detection fix; DOCX convert-on-import is best-effort and fails gracefully when LibreOffice is unavailable. analyze clean, tests green.
This commit is contained in:
@@ -127,13 +127,15 @@ class PptxService {
|
||||
/// Try converting via LibreOffice headless.
|
||||
Future<List<String>> _convertViaLibreOffice(String pptxPath) async {
|
||||
try {
|
||||
// Check if LibreOffice is available
|
||||
final which = await Process.run('which', ['libreoffice']);
|
||||
if (which.exitCode != 0) return [];
|
||||
// Resolve the LibreOffice binary across platforms. On Windows the binary
|
||||
// is `soffice.exe` (not on PATH for `which`, which is POSIX-only), so we
|
||||
// probe the standard install locations as well — see resolveSoffice().
|
||||
final soffice = await resolveSoffice();
|
||||
if (soffice == null) return [];
|
||||
|
||||
final outDir = await _makeTmpDir('pptx_images');
|
||||
|
||||
final result = await Process.run('libreoffice', [
|
||||
final result = await Process.run(soffice, [
|
||||
'--headless',
|
||||
'--convert-to',
|
||||
'png',
|
||||
@@ -174,6 +176,73 @@ class PptxService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve the LibreOffice CLI binary for the current platform, or null when
|
||||
/// it cannot be found.
|
||||
///
|
||||
/// Order:
|
||||
/// 1. Windows: `soffice.exe` at the standard install paths
|
||||
/// (`C:\Program Files\LibreOffice\program\soffice.exe`, and the 32-bit
|
||||
/// `Program Files (x86)` variant). The POSIX `which` can't find these.
|
||||
/// 2. POSIX: `which libreoffice`, then `which soffice` (macOS/some distros).
|
||||
/// 3. Otherwise null → callers fall back gracefully.
|
||||
static Future<String?> resolveSoffice() async {
|
||||
if (Platform.isWindows) {
|
||||
const candidates = [
|
||||
r'C:\Program Files\LibreOffice\program\soffice.exe',
|
||||
r'C:\Program Files (x86)\LibreOffice\program\soffice.exe',
|
||||
];
|
||||
for (final c in candidates) {
|
||||
if (await File(c).exists()) return c;
|
||||
}
|
||||
// Last resort: maybe soffice is on PATH (e.g. a portable install).
|
||||
if (await _whichOk('soffice')) return 'soffice';
|
||||
return null;
|
||||
}
|
||||
if (await _whichOk('libreoffice')) return 'libreoffice';
|
||||
if (await _whichOk('soffice')) return 'soffice';
|
||||
return null;
|
||||
}
|
||||
|
||||
static Future<bool> _whichOk(String cmd) async {
|
||||
try {
|
||||
final r = await Process.run('which', [cmd]);
|
||||
return r.exitCode == 0;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an arbitrary office document (e.g. DOCX) to PDF via LibreOffice
|
||||
/// headless, writing the PDF NEXT TO [sourcePath] (same folder, same
|
||||
/// basename + `.pdf`). Returns the PDF path on success, or null when
|
||||
/// LibreOffice is unavailable or the conversion fails — callers MUST handle
|
||||
/// null and surface a friendly message rather than crash.
|
||||
Future<String?> convertToPdf(String sourcePath) async {
|
||||
final soffice = await resolveSoffice();
|
||||
if (soffice == null) return null;
|
||||
|
||||
final outDir = p.dirname(sourcePath);
|
||||
try {
|
||||
final result = await Process.run(soffice, [
|
||||
'--headless',
|
||||
'--convert-to',
|
||||
'pdf',
|
||||
'--outdir',
|
||||
outDir,
|
||||
sourcePath,
|
||||
]);
|
||||
if (result.exitCode != 0) return null;
|
||||
final pdfPath = p.join(
|
||||
outDir,
|
||||
'${p.basenameWithoutExtension(sourcePath)}.pdf',
|
||||
);
|
||||
if (await File(pdfPath).exists()) return pdfPath;
|
||||
return null;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate placeholder slide images when LibreOffice is not available.
|
||||
///
|
||||
/// Uses ImageMagick `convert` to create PNG files with slide numbers.
|
||||
|
||||
Reference in New Issue
Block a user