feat: unified shell, diagnostics pack, native Office, sticky board
All checks were successful
CI / Windows build (push) Successful in 14m22s
All checks were successful
CI / Windows build (push) Successful in 14m22s
Make Surface remote debugging and classroom workflows viable: always-on structured logs with one-click zip export, a single AppShell chrome, OOXML PPTX/DOCX annotation without LibreOffice, and a first-class sticky board. Also drop spike/legacy ink widgets and tighten pen feel (predictor, PenInfoHistory, page-tile layer). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import '../providers/note_provider.dart';
|
||||
import '../providers/ocr_provider.dart';
|
||||
import '../providers/search_provider.dart';
|
||||
import '../editor/canvas/pen_editor_screen.dart';
|
||||
import '../editor/canvas/office_document_screen.dart';
|
||||
import '../services/pptx_service.dart';
|
||||
import '../services/vault_service.dart';
|
||||
import '../editor/canvas/pen_note_screen.dart';
|
||||
@@ -33,7 +34,10 @@ String _formatDate(DateTime d) {
|
||||
}
|
||||
|
||||
class HomeScreen extends ConsumerWidget {
|
||||
const HomeScreen({super.key});
|
||||
const HomeScreen({super.key, this.embeddedInShell = false});
|
||||
|
||||
/// When true, chrome (settings/search) is owned by [AppShell].
|
||||
final bool embeddedInShell;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -42,37 +46,40 @@ class HomeScreen extends ConsumerWidget {
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(l.appTitle),
|
||||
centerTitle: true,
|
||||
title: Text(embeddedInShell ? l.libraryTab : l.appTitle),
|
||||
centerTitle: !embeddedInShell,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings),
|
||||
tooltip: l.settings,
|
||||
onPressed: () {
|
||||
Navigator.of(
|
||||
context,
|
||||
).push(MaterialPageRoute(builder: (_) => const SettingsScreen()));
|
||||
},
|
||||
),
|
||||
if (!embeddedInShell) ...[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings),
|
||||
tooltip: l.settings,
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const SettingsScreen()),
|
||||
);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.search),
|
||||
tooltip: l.search,
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const SearchScreen()),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
IconButton(
|
||||
icon: const Icon(Icons.file_open),
|
||||
tooltip: l.importFile,
|
||||
onPressed: () => _importFile(context, ref),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.search),
|
||||
tooltip: l.search,
|
||||
onPressed: () {
|
||||
Navigator.of(
|
||||
context,
|
||||
).push(MaterialPageRoute(builder: (_) => const SearchScreen()));
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () => _createAndOpenNote(context, ref),
|
||||
child: const Icon(Icons.add),
|
||||
icon: const Icon(Icons.add),
|
||||
label: Text(l.createNotebook),
|
||||
),
|
||||
body: notesAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
@@ -93,15 +100,14 @@ class HomeScreen extends ConsumerWidget {
|
||||
},
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
// Notes section header always shown when documents exist
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||
child: Text(
|
||||
'Notes',
|
||||
l.notesSection,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -133,19 +139,19 @@ class HomeScreen extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
// Documents section header always shown when notes exist
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),
|
||||
child: Text(
|
||||
'Recent Documents',
|
||||
l.documentsSection,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (documents.isNotEmpty)
|
||||
// continue existing document list below — marker for patch
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) =>
|
||||
@@ -154,7 +160,6 @@ class HomeScreen extends ConsumerWidget {
|
||||
),
|
||||
)
|
||||
else
|
||||
// [M2] Per-section empty hint when notes exist but documents don't
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
@@ -284,9 +289,8 @@ class HomeScreen extends ConsumerWidget {
|
||||
}
|
||||
|
||||
/// Route an in-vault [filePath] to the correct editor by extension:
|
||||
/// pdf → [PenEditorScreen]; pptx/ppt → [PenSlideScreen]; docx → convert to
|
||||
/// PDF (best-effort, LibreOffice) then open as PDF. Unsupported / failed
|
||||
/// conversions surface a friendly message instead of crashing.
|
||||
/// pdf → [PenEditorScreen]; pptx/docx → native [OfficeDocumentScreen];
|
||||
/// legacy .ppt may still use image fallback.
|
||||
Future<void> _openVaultFile(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
@@ -303,27 +307,15 @@ class HomeScreen extends ConsumerWidget {
|
||||
),
|
||||
);
|
||||
case 'pptx':
|
||||
case 'ppt':
|
||||
await _openPresentation(context, filePath);
|
||||
case 'docx':
|
||||
final pptxService = PptxService();
|
||||
final pdfPath = await pptxService.convertToPdf(filePath);
|
||||
if (!context.mounted) return;
|
||||
if (pdfPath == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l.convertNeedsLibreOffice)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
// The converted PDF lives next to the docx in the notebook folder, so
|
||||
// it becomes the annotatable artifact; re-scan picks it up.
|
||||
await ref.read(documentListProvider.notifier).loadDocuments();
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => PenEditorScreen(pdfPath: pdfPath),
|
||||
builder: (_) => OfficeDocumentScreen(filePath: filePath),
|
||||
),
|
||||
);
|
||||
case 'ppt':
|
||||
// Legacy binary PPT — try native-ish image path for now.
|
||||
await _openPresentation(context, filePath);
|
||||
default:
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l.unsupportedFileType(ext))),
|
||||
@@ -374,14 +366,14 @@ class HomeScreen extends ConsumerWidget {
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'No notes yet',
|
||||
AppLocalizations.of(context).emptyLibraryTitle,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Create your first note',
|
||||
AppLocalizations.of(context).emptyLibraryBody,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
@@ -639,8 +631,7 @@ class _DocumentTileState extends ConsumerState<_DocumentTile> {
|
||||
);
|
||||
}
|
||||
|
||||
// Route by docType: pdf → PenEditorScreen (pen-first), ppt/pptx → PenSlideScreen,
|
||||
// docx → best-effort convert-to-PDF then open as PDF.
|
||||
// Route by docType: pdf → PenEditorScreen; pptx/docx → native OfficeDocumentScreen.
|
||||
Future<void> _openDocument(BuildContext context) async {
|
||||
final document = widget.document;
|
||||
final l = AppLocalizations.of(context);
|
||||
@@ -654,27 +645,17 @@ class _DocumentTileState extends ConsumerState<_DocumentTile> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (document.docType == 'docx') {
|
||||
final pdfPath = await PptxService().convertToPdf(document.filePath);
|
||||
if (!mounted) return;
|
||||
if (pdfPath == null) {
|
||||
ScaffoldMessenger.of(this.context).showSnackBar(
|
||||
SnackBar(content: Text(l.convertNeedsLibreOffice)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
await ref.read(documentListProvider.notifier).loadDocuments();
|
||||
if (!mounted) return;
|
||||
Navigator.of(this.context).push(
|
||||
if (document.docType == 'docx' || document.docType == 'pptx') {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => PenEditorScreen(pdfPath: pdfPath),
|
||||
builder: (_) => OfficeDocumentScreen(filePath: document.filePath),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
// PPT/PPTX: convert to images then push PenSlideScreen
|
||||
// Legacy .ppt: convert to images then push PenSlideScreen
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(this.context).showSnackBar(
|
||||
SnackBar(content: Text(l.processingPresentation)),
|
||||
|
||||
Reference in New Issue
Block a user