Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'package:papyrus/media/media_cache_service.dart';
import 'package:papyrus/media/media_models.dart';
import 'package:papyrus/media/media_storage_scope.dart';
import 'package:papyrus/media/media_upload_queue.dart';
import 'package:papyrus/platform/book_import_drop_registration.dart';
import 'package:papyrus/platform/hot_restart_cleanup.dart';
import 'package:papyrus/powersync/powersync_service.dart';
import 'package:papyrus/powersync/papyrus_powersync_connector.dart';
Expand All @@ -37,6 +38,7 @@ import 'config/app_router.dart';
Future main() async {
await runPreviousHotRestartCleanup();
WidgetsFlutterBinding.ensureInitialized();
ensureBookImportDropPluginRegistered();
usePathUrlStrategy();

final prefs = await SharedPreferences.getInstance();
Expand Down
1 change: 1 addition & 0 deletions app/lib/platform/book_import_drop_registration.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export 'book_import_drop_registration_stub.dart' if (dart.library.js_interop) 'book_import_drop_registration_web.dart';
1 change: 1 addition & 0 deletions app/lib/platform/book_import_drop_registration_stub.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
void ensureBookImportDropPluginRegistered() {}
11 changes: 11 additions & 0 deletions app/lib/platform/book_import_drop_registration_web.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import 'package:desktop_drop/desktop_drop_web.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart';

bool _isRegistered = false;

void ensureBookImportDropPluginRegistered() {
if (_isRegistered) return;
DesktopDropWeb.registerWith(webPluginRegistrar);
webPluginRegistrar.registerMessageHandler();
_isRegistered = true;
}
28 changes: 18 additions & 10 deletions app/lib/widgets/add_book/add_book_choice_sheet.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:papyrus/themes/design_tokens.dart';
import 'package:papyrus/widgets/add_book/add_physical_book_sheet.dart';
import 'package:papyrus/widgets/add_book/import_book_sheet.dart';
import 'package:papyrus/widgets/add_book/book_import_sheet.dart';
import 'package:papyrus/widgets/shared/bottom_sheet_handle.dart';

/// Choice sheet for selecting digital import, physical entry, or optional online search.
Expand All @@ -15,7 +15,13 @@ class AddBookChoiceSheet extends StatefulWidget {
final VoidCallback? onFindOnline;

/// Show the choice sheet as a modal bottom sheet.
static Future<void> show(BuildContext context, {VoidCallback? onFindOnline}) async {
static Future<void> show(
BuildContext context, {
VoidCallback? onFindOnline,
DigitalBookFilePicker? digitalFilePicker,
BookImportProcessor? bookImportProcessor,
ImportedBookFileDeleter? deleteImportedBookFile,
}) async {
Future<_AddBookChoice?>? sheetCompleted;
final choice = await showModalBottomSheet<_AddBookChoice>(
context: context,
Expand All @@ -40,8 +46,15 @@ class AddBookChoiceSheet extends StatefulWidget {
}

switch (choice) {
case _AddBookChoice.importDigital:
await BookImportSheet.show(
context,
pickFiles: digitalFilePicker,
processor: bookImportProcessor,
deleteBookFile: deleteImportedBookFile,
);
case _AddBookChoice.addPhysical:
AddPhysicalBookSheet.show(context);
await AddPhysicalBookSheet.show(context);
case _AddBookChoice.findOnline:
onFindOnline?.call();
}
Expand All @@ -53,7 +66,6 @@ class AddBookChoiceSheet extends StatefulWidget {

class _AddBookChoiceSheetState extends State<AddBookChoiceSheet> {
bool _isSelecting = false;
bool _showImport = false;

void _select(_AddBookChoice choice) {
if (_isSelecting) {
Expand All @@ -66,10 +78,6 @@ class _AddBookChoiceSheetState extends State<AddBookChoiceSheet> {

@override
Widget build(BuildContext context) {
if (_showImport) {
return const ImportBookSheet();
}

final textTheme = Theme.of(context).textTheme;

return Column(
Expand All @@ -84,7 +92,7 @@ class _AddBookChoiceSheetState extends State<AddBookChoiceSheet> {
icon: Icons.upload_file,
title: 'Import digital books',
subtitle: 'EPUB, PDF, AZW3, MOBI, CBZ/CBR',
onTap: () => setState(() => _showImport = true),
onTap: () => _select(_AddBookChoice.importDigital),
),
const SizedBox(height: Spacing.sm),
_ChoiceOption(
Expand All @@ -107,7 +115,7 @@ class _AddBookChoiceSheetState extends State<AddBookChoiceSheet> {
}
}

enum _AddBookChoice { addPhysical, findOnline }
enum _AddBookChoice { importDigital, addPhysical, findOnline }

class _ChoiceOption extends StatelessWidget {
final IconData icon;
Expand Down
81 changes: 81 additions & 0 deletions app/lib/widgets/add_book/add_book_sheet_scaffold.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import 'package:flutter/material.dart';
import 'package:papyrus/themes/design_tokens.dart';
import 'package:papyrus/widgets/shared/bottom_sheet_handle.dart';

/// Lays out an add-book sheet with fixed header and footer regions.
class AddBookSheetScaffold extends StatelessWidget {
final String title;
final VoidCallback onClose;
final Widget body;
final Widget footer;
final bool canClose;

const AddBookSheetScaffold({
super.key,
required this.title,
required this.onClose,
required this.body,
required this.footer,
this.canClose = true,
});

@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;

return LayoutBuilder(
builder: (context, constraints) {
final isCompactHeight = constraints.maxHeight < 280;
final verticalPadding = isCompactHeight ? 0.0 : Spacing.md;
final handleSpacing = isCompactHeight ? 0.0 : Spacing.lg;

return Column(
children: [
Container(
key: const Key('add-book-sheet-header'),
padding: EdgeInsets.fromLTRB(Spacing.lg, verticalPadding, Spacing.lg, verticalPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const BottomSheetHandle(),
SizedBox(height: handleSpacing),
Row(
children: [
Expanded(
child: Text(
title,
maxLines: isCompactHeight ? 1 : 2,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.headlineSmall,
textScaler: isCompactHeight ? TextScaler.noScaling : null,
),
),
IconButton(
constraints: isCompactHeight ? const BoxConstraints.tightFor(width: 44, height: 44) : null,
icon: const Icon(Icons.close),
padding: isCompactHeight ? EdgeInsets.zero : null,
tooltip: 'Close',
onPressed: canClose ? onClose : null,
),
],
),
],
),
),
const Divider(height: 1),
Expanded(child: body),
Container(
key: const Key('add-book-sheet-footer'),
padding: EdgeInsets.symmetric(horizontal: Spacing.lg, vertical: verticalPadding),
decoration: BoxDecoration(
color: colorScheme.surface,
border: Border(top: BorderSide(color: colorScheme.outlineVariant)),
),
child: SafeArea(top: false, child: footer),
),
],
);
},
);
}
}
98 changes: 41 additions & 57 deletions app/lib/widgets/add_book/add_physical_book_sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@ import 'package:papyrus/models/book.dart';
import 'package:papyrus/services/metadata_service.dart';
import 'package:papyrus/themes/design_tokens.dart';
import 'package:papyrus/utils/image_utils.dart';
import 'package:papyrus/widgets/add_book/add_book_sheet_scaffold.dart';
import 'package:papyrus/widgets/add_book/isbn_scanner_dialog.dart';
import 'package:papyrus/widgets/book_edit/cover_image_picker.dart';
import 'package:papyrus/widgets/book_form/book_date_field.dart';
import 'package:papyrus/widgets/book_form/book_text_field.dart';
import 'package:papyrus/widgets/book_form/co_author_editor.dart';
import 'package:papyrus/widgets/shared/bottom_sheet_handle.dart';
import 'package:papyrus/widgets/shared/bottom_sheet_header.dart';
import 'package:provider/provider.dart';
import 'package:uuid/uuid.dart';

Expand Down Expand Up @@ -269,66 +268,51 @@ class _PhysicalBookContentState extends State<_PhysicalBookContent> {
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: Form(
key: _formKey,
child: Column(
children: [
// Fixed header
Padding(
padding: const EdgeInsets.fromLTRB(Spacing.md, Spacing.md, Spacing.md, 0),
child: Column(
children: [
const BottomSheetHandle(),
const SizedBox(height: Spacing.md),
BottomSheetHeader(
title: 'Add physical book',
onCancel: () => Navigator.pop(context),
onSave: _onSave,
saveLabel: 'Add',
canSave: _canSave,
),
],
),
),
const SizedBox(height: Spacing.md),
const Divider(height: 1),

// Scrollable form content
Expanded(
child: ListView(
controller: widget.scrollController,
padding: const EdgeInsets.symmetric(horizontal: Spacing.lg, vertical: Spacing.md),
child: AddBookSheetScaffold(
title: 'Add physical book',
onClose: () => Navigator.of(context).pop(),
body: ListView(
controller: widget.scrollController,
padding: const EdgeInsets.symmetric(horizontal: Spacing.lg, vertical: Spacing.md),
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Card(
margin: const EdgeInsets.only(bottom: Spacing.xs),
child: Padding(
padding: const EdgeInsets.all(Spacing.md),
child: CoverImagePicker(
initialUrl: _coverUrl,
initialBytes: _coverImageBytes,
onUrlChanged: (url) => setState(() => _coverUrl = url),
onFileChanged: (bytes) => setState(() {
_coverImageBytes = bytes;
if (bytes != null) _coverUrl = null;
}),
coverWidth: 240,
),
),
Card(
margin: const EdgeInsets.only(bottom: Spacing.xs),
child: Padding(
padding: const EdgeInsets.all(Spacing.md),
child: CoverImagePicker(
initialUrl: _coverUrl,
initialBytes: _coverImageBytes,
onUrlChanged: (url) => setState(() => _coverUrl = url),
onFileChanged: (bytes) => setState(() {
_coverImageBytes = bytes;
if (bytes != null) _coverUrl = null;
}),
coverWidth: 240,
),

_buildIsbnSection(),
_buildBasicInfoSection(),
_buildPublicationSection(),
_buildIdentifiersSection(),
_buildSeriesSection(),
_buildPhysicalBookSection(),
],
),
),

_buildIsbnSection(),
_buildBasicInfoSection(),
_buildPublicationSection(),
_buildIdentifiersSection(),
_buildSeriesSection(),
_buildPhysicalBookSection(),
],
),
),
],
],
),
footer: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Cancel')),
const SizedBox(width: Spacing.sm),
FilledButton(onPressed: _canSave ? _onSave : null, child: const Text('Add')),
],
),
),
),
);
Expand Down
Loading
Loading