diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff13f81..97602a9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,9 +17,22 @@ jobs: channel: stable - run: flutter pub get + + - uses: actions/upload-artifact@v4 + with: + name: pubspec-lock + path: pubspec.lock + retention-days: 14 + - run: flutter analyze - run: flutter test # Tests compile the host (Linux) native assets only. Compile one shipping # Android ABI as well so Gradle/NDK/native-asset regressions fail before # a release tag is created. - run: flutter build apk --debug --target-platform android-arm64 + + - uses: actions/upload-artifact@v4 + with: + name: app-debug-arm64 + path: build/app/outputs/flutter-apk/app-debug.apk + retention-days: 14 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6fc9d3a..99f93b2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,6 +41,9 @@ add `Co-Authored-By` trailers for AI coding assistants. ## Pull requests +- Base your branch on `dev` and open pull requests against `dev`, not + `main`. `main` only advances when `dev` is deliberately promoted for a + release — PRs opened against it directly won't be merged there. - Keep PRs focused — one logical change per PR is easier to review and keeps `git bisect` useful. - CI (`flutter analyze` + `flutter test`) must pass before merge. diff --git a/README.md b/README.md index 4271ff9..1568fec 100644 --- a/README.md +++ b/README.md @@ -40,10 +40,14 @@ and this page list.* - Re-edit any page after the fact (corners, filter, rotation, brightness, and contrast) without re-scanning - Capture multiple pages in sequence and reorder them with drag-and-drop +- Import existing photos from your gallery instead of (or alongside) + capturing new ones - Combine captured pages into a single PDF - Share the PDF via the OS share sheet (or download it directly on web) - Material 3 UI that follows the system's light/dark theme - No accounts, no cloud storage, no tracking +- An in-app About screen (the ⓘ icon) shows the exact running version and + links straight to this source repo Edge detection, perspective correction, and filters run on real OpenCV (`opencv_dart`) on Android/iOS/desktop. Desktop builds support importing diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 31d0bdf..c70bb41 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -2,7 +2,7 @@ + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index ba3000a..c842436 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -7,7 +7,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - Fosscanner + FOSScanner CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -15,7 +15,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - fosscanner + FOSScanner CFBundlePackageType APPL CFBundleShortVersionString diff --git a/lib/screens/barcode_scan_screen.dart b/lib/screens/barcode_scan_screen.dart new file mode 100644 index 0000000..f608923 --- /dev/null +++ b/lib/screens/barcode_scan_screen.dart @@ -0,0 +1,187 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show Clipboard, ClipboardData; +import 'package:flutter_zxing/flutter_zxing.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../widgets/transient_message.dart'; + +/// A live QR/barcode scanning mode, separate from the document-scan flow: +/// point the camera at a code and get the decoded value with quick actions +/// (copy, open link), rather than treating the code as a document page. +class BarcodeScanScreen extends StatefulWidget { + const BarcodeScanScreen({super.key}); + + @override + State createState() => _BarcodeScanScreenState(); +} + +class _BarcodeScanScreenState extends State { + String? _lastResult; + + Uri? get _resultUri { + final result = _lastResult; + if (result == null) return null; + final uri = Uri.tryParse(result); + if (uri == null || !(uri.scheme == 'http' || uri.scheme == 'https')) { + return null; + } + return uri; + } + + void _handleScan(Code code) { + if (!mounted) return; + final text = code.text; + if (!code.isValid || text == null || text.isEmpty) return; + if (text == _lastResult) return; + setState(() => _lastResult = text); + } + + void _dismissResult() { + setState(() => _lastResult = null); + } + + Future _copyResult() async { + final result = _lastResult; + if (result == null) return; + await Clipboard.setData(ClipboardData(text: result)); + if (!mounted) return; + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('Copied to clipboard'))); + } + + Future _openResult() async { + final uri = _resultUri; + if (uri == null) return; + try { + final launched = await launchUrl( + uri, + mode: LaunchMode.externalApplication, + ); + if (!launched && mounted) { + showTransientMessage(context, 'Could not open the link.'); + } + } catch (_) { + if (mounted) showTransientMessage(context, 'Could not open the link.'); + } + } + + @override + Widget build(BuildContext context) { + final result = _lastResult; + return Scaffold( + appBar: AppBar(title: const Text('Scan QR / Barcode')), + body: Stack( + children: [ + // cropPercent must stay 0 here: ReaderWidget's crop-indicator square + // only lines up with the region it actually decodes when the widget + // is truly full-screen. This screen has an AppBar above it (so the + // preview is letterboxed), which is exactly the case the flutter_zxing + // maintainers flag as producing a crop guide that visually looks + // centered on the code while the real decode window is offset + // elsewhere — the code never gets read even though it's framed + // correctly on screen. See khoren93/flutter_zxing#196. + // + // showScannerOverlay is also off: at cropPercent 0, flutter_zxing's + // built-in overlay switches to a "tap the highlighted code" mode + // instead of a plain guide, which reads as scanning requiring a + // tap when it doesn't — onScan already fires as soon as a frame + // decodes. The plain square below is a purely cosmetic aiming hint + // with no effect on what actually gets decoded (the whole frame + // always does), so it can't drift out of sync the way the built-in + // one did. + ReaderWidget( + onScan: _handleScan, + showGallery: true, + cropPercent: 0, + showScannerOverlay: false, + // 1D formats (EAN/UPC/Code128, common on physical product + // packaging) carry far less redundancy than a QR code and are + // much more sensitive to a slight skew/angle, so they need the + // more exhaustive per-frame decode attempt this enables. + tryHarder: true, + ), + if (result == null) + IgnorePointer( + child: Center( + child: Container( + width: MediaQuery.sizeOf(context).shortestSide * 0.6, + height: MediaQuery.sizeOf(context).shortestSide * 0.6, + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.primary, + width: 3, + ), + borderRadius: BorderRadius.circular(16), + ), + ), + ), + ), + if (result != null) + Positioned( + left: 0, + right: 0, + bottom: 0, + child: SafeArea( + top: false, + child: Card( + margin: const EdgeInsets.all(16), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + result, + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ), + IconButton( + icon: const Icon(Icons.close), + tooltip: 'Dismiss and keep scanning', + onPressed: _dismissResult, + visualDensity: VisualDensity.compact, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + if (_resultUri != null) ...[ + Expanded( + child: FilledButton.icon( + onPressed: _openResult, + icon: const Icon(Icons.open_in_new), + label: const Text('Open'), + ), + ), + const SizedBox(width: 12), + ], + Expanded( + child: OutlinedButton.icon( + onPressed: _copyResult, + icon: const Icon(Icons.copy), + label: const Text('Copy'), + ), + ), + ], + ), + ], + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/corner_adjust_screen.dart b/lib/screens/corner_adjust_screen.dart index 0473cb0..5ad0fff 100644 --- a/lib/screens/corner_adjust_screen.dart +++ b/lib/screens/corner_adjust_screen.dart @@ -10,6 +10,7 @@ import '../models/scanned_page.dart'; import '../services/corner_geometry.dart'; import '../services/document_processor.dart'; import '../widgets/corner_overlay.dart'; +import '../widgets/transient_message.dart'; const _fullPreviewDecodeSize = 2048; const _filterChipDecodeSize = 256; @@ -132,6 +133,8 @@ class _CornerAdjustScreenState extends State { ]; } + void _showError(String message) => showTransientMessage(context, message); + Future _updatePreviews() async { final corners = _corners; if (corners == null) return; @@ -139,7 +142,6 @@ class _CornerAdjustScreenState extends State { _isGeneratingPreviews = true; _filterPreviews = null; _finalPreviewBytes = null; - _error = null; }); // Keep geometry failures separate from decoder/backend failures so the // recovery guidance matches what the user can actually fix. @@ -151,11 +153,10 @@ class _CornerAdjustScreenState extends State { ); } on ArgumentError { if (!mounted) return; - setState(() { - _isGeneratingPreviews = false; - _error = - 'Could not preview this crop. Adjust the corners and try again.'; - }); + setState(() => _isGeneratingPreviews = false); + _showError( + 'Could not preview this crop. Adjust the corners and try again.', + ); return; } @@ -177,10 +178,8 @@ class _CornerAdjustScreenState extends State { _updateFinalPreview(); } catch (_) { if (!mounted) return; - setState(() { - _isGeneratingPreviews = false; - _error = 'Could not process this photo. Try another image.'; - }); + setState(() => _isGeneratingPreviews = false); + _showError('Could not process this photo. Try another image.'); } } @@ -244,10 +243,7 @@ class _CornerAdjustScreenState extends State { final rotationQuarterTurns = _rotationQuarterTurns; final brightness = _brightness; final contrast = _contrast; - setState(() { - _isProcessing = true; - _error = null; - }); + setState(() => _isProcessing = true); try { final processed = await _processForExport( corners, @@ -270,10 +266,8 @@ class _CornerAdjustScreenState extends State { ); } catch (_) { if (!mounted) return; - setState(() { - _isProcessing = false; - _error = 'Could not process this page. Try another image.'; - }); + setState(() => _isProcessing = false); + _showError('Could not process this page. Try another image.'); } } @@ -326,14 +320,6 @@ class _CornerAdjustScreenState extends State { ) { return Column( children: [ - if (_error != null) - Padding( - padding: const EdgeInsets.all(12), - child: Text( - _error!, - style: TextStyle(color: Theme.of(context).colorScheme.error), - ), - ), Expanded( child: Padding( padding: const EdgeInsets.all(16), @@ -400,14 +386,6 @@ class _CornerAdjustScreenState extends State { _finalPreviewBytes ?? _filterPreviews?[_selectedFilter]; return Column( children: [ - if (_error != null) - Padding( - padding: const EdgeInsets.all(12), - child: Text( - _error!, - style: TextStyle(color: Theme.of(context).colorScheme.error), - ), - ), Expanded( child: Padding( padding: const EdgeInsets.all(16), diff --git a/lib/screens/scanner_home_page.dart b/lib/screens/scanner_home_page.dart index a2bac77..dbaebde 100644 --- a/lib/screens/scanner_home_page.dart +++ b/lib/screens/scanner_home_page.dart @@ -6,15 +6,20 @@ import 'package:flutter/foundation.dart' import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:package_info_plus/package_info_plus.dart'; import 'package:pdf/pdf.dart'; import 'package:pdf/widgets.dart' as pw; import 'package:share_plus/share_plus.dart'; +import 'package:url_launcher/url_launcher.dart'; import '../models/scanned_page.dart'; import '../services/image_metadata.dart'; +import '../widgets/transient_message.dart'; +import 'barcode_scan_screen.dart'; import 'corner_adjust_screen.dart'; const _thumbnailCacheWidth = 512; +const _sourceCodeUrl = 'https://github.com/FOSScanner/fosscanner-app'; class _DocumentCapacityException implements Exception { const _DocumentCapacityException(); @@ -99,16 +104,87 @@ class _ScannerHomePageState extends State { return true; } - void _showMessage(String message) { + void _showMessage(String message) => showTransientMessage(context, message); + + Future _openSourceCode() async { + try { + final launched = await launchUrl( + Uri.parse(_sourceCodeUrl), + mode: LaunchMode.externalApplication, + ); + if (!launched) _showMessage('Could not open the source code link.'); + } catch (_) { + _showMessage('Could not open the source code link.'); + } + } + + Future _showAppInfo() async { + final packageInfo = await PackageInfo.fromPlatform(); if (!mounted) return; - // Startup lost-data recovery can fail from initState, before this page's - // Scaffold has registered with the surrounding ScaffoldMessenger. - WidgetsBinding.instance.addPostFrameCallback((_) { - if (!mounted) return; - ScaffoldMessenger.maybeOf( - context, - )?.showSnackBar(SnackBar(content: Text(message))); - }); + final theme = Theme.of(context); + final mutedStyle = theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ); + + await showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: Row( + children: [ + Image.asset('assets/icon/icon.png', width: 40, height: 40), + const SizedBox(width: 12), + const Text('FOSScanner'), + ], + ), + content: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Version ${packageInfo.version} (build ${packageInfo.buildNumber})', + style: mutedStyle, + ), + const SizedBox(height: 16), + const Text( + 'A privacy-first, free and open-source document scanner. ' + 'Scan documents with your camera, auto-crop and dewarp them, ' + 'and export a PDF — all on-device. No accounts, no cloud, ' + 'no tracking.', + ), + const SizedBox(height: 20), + const Divider(height: 1), + const SizedBox(height: 12), + InkWell( + onTap: _openSourceCode, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.code, size: 18, color: theme.colorScheme.primary), + const SizedBox(width: 8), + Text( + 'View source code', + style: TextStyle( + color: theme.colorScheme.primary, + decoration: TextDecoration.underline, + ), + ), + ], + ), + ), + const SizedBox(height: 4), + Text('Licensed under the GNU GPL v3.0', style: mutedStyle), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(), + child: const Text('Close'), + ), + ], + ), + ); } Future _recoverLostImages() async { @@ -464,6 +540,18 @@ class _ScannerHomePageState extends State { appBar: AppBar( title: const Text('FOSScanner'), actions: [ + // flutter_zxing has no web decoding backend (its web implementation + // throws UnimplementedError on every frame) — same platform gap as + // opencv_dart, so this follows the same kIsWeb convention used for + // the detect/adjust flow elsewhere in this screen. + if (!kIsWeb) + IconButton( + icon: const Icon(Icons.qr_code_scanner), + onPressed: () => Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const BarcodeScanScreen()), + ), + tooltip: 'Scan QR/barcode', + ), IconButton( icon: const Icon(Icons.photo_library_outlined), onPressed: _isPickingImages || !_canStartImagePick @@ -625,11 +713,14 @@ class _ScannerHomePageState extends State { child: const Icon(Icons.camera_alt), ) : null, - bottomNavigationBar: _pages.isNotEmpty - ? SafeArea( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: ElevatedButton.icon( + bottomNavigationBar: SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (_pages.isNotEmpty) ...[ + ElevatedButton.icon( key: _shareButtonKey, style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), @@ -649,9 +740,21 @@ class _ScannerHomePageState extends State { style: const TextStyle(fontSize: 16), ), ), + const SizedBox(height: 4), + ], + TextButton( + onPressed: _showAppInfo, + child: Text( + 'About FOSScanner', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), ), - ) - : null, + ], + ), + ), + ), ); } } diff --git a/lib/services/corner_geometry.dart b/lib/services/corner_geometry.dart index 111ccc2..7a085f8 100644 --- a/lib/services/corner_geometry.dart +++ b/lib/services/corner_geometry.dart @@ -101,6 +101,34 @@ void _validateConvexQuad(List corners) { } } +// A convex, consistently-wound quad can still have its 4 points in the +// wrong tl/tr/br/bl positions — e.g. two adjacent handles dragged past +// each other, or all 4 roles shifted by one — which looks like a +// perfectly valid crop shape but warps into a flipped/rotated page. +// orderCorners applies the same geometric role assignment used at +// detection time; if it disagrees with the order given here, the corners +// are mislabeled even though the shape itself is fine. +// +// Deliberately separate from _validateConvexQuad (and only ever called +// once, on the caller's original un-rounded corners): orderCorners' choice +// of starting edge is a tie-break on midpoint y that can flip once corners +// are rounded to integers, even when the un-rounded quad was unambiguous. +// Running this same check again against a rounded copy would then reject +// perfectly valid, correctly-labeled crops whenever that tie happened to +// land on opposite sides of the rounding boundary. +void _validateCornerRoles(List corners) { + final canonical = orderCorners(corners); + for (var i = 0; i < 4; i++) { + if (canonical[i] != corners[i]) { + throw ArgumentError.value( + corners, + 'corners', + 'Corners must be ordered top-left, top-right, bottom-right, bottom-left', + ); + } + } +} + /// Calculates a bounded output size from ordered document [corners], preserving /// the selected crop's aspect ratio when it must be downscaled. (int width, int height) calculateWarpSize( @@ -112,6 +140,7 @@ void _validateConvexQuad(List corners) { throw ArgumentError('Warp limits must allow an image of at least 2×2'); } _validateConvexQuad(corners); + _validateCornerRoles(corners); // OpenCV receives integer source points. Validate and size from those exact // points so a valid-looking fractional quad cannot collapse when converted. diff --git a/lib/widgets/transient_message.dart b/lib/widgets/transient_message.dart new file mode 100644 index 0000000..03fd767 --- /dev/null +++ b/lib/widgets/transient_message.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +/// Shows a short-lived SnackBar, deferred to a post-frame callback so it +/// works even when called from code that runs before this frame's Scaffold +/// has registered with the surrounding ScaffoldMessenger (e.g. startup +/// recovery, or a callback fired mid-build). +void showTransientMessage(BuildContext context, String message) { + if (!context.mounted) return; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!context.mounted) return; + ScaffoldMessenger.maybeOf( + context, + )?.showSnackBar(SnackBar(content: Text(message))); + }); +} diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc index 25c3632..73c2a8c 100644 --- a/linux/runner/my_application.cc +++ b/linux/runner/my_application.cc @@ -45,11 +45,11 @@ static void my_application_activate(GApplication* application) { if (use_header_bar) { GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "fosscanner"); + gtk_header_bar_set_title(header_bar, "FOSScanner"); gtk_header_bar_set_show_close_button(header_bar, TRUE); gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); } else { - gtk_window_set_title(window, "fosscanner"); + gtk_window_set_title(window, "FOSScanner"); } gtk_window_set_default_size(window, 1280, 720); diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 8ad9182..f3e5e80 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,9 +6,13 @@ import FlutterMacOS import Foundation import file_selector_macos +import package_info_plus import share_plus +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index d239140..3d368af 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -65,7 +65,7 @@ 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* fosscanner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "fosscanner.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* FOSScanner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "FOSScanner.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -134,7 +134,7 @@ 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( - 33CC10ED2044A3C60003C045 /* fosscanner.app */, + 33CC10ED2044A3C60003C045 /* FOSScanner.app */, 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, ); name = Products; @@ -224,7 +224,7 @@ 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, ); productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* fosscanner.app */; + productReference = 33CC10ED2044A3C60003C045 /* FOSScanner.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -398,7 +398,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.fosscanner.app.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/fosscanner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/fosscanner"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FOSScanner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/FOSScanner"; }; name = Debug; }; @@ -412,7 +412,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.fosscanner.app.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/fosscanner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/fosscanner"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FOSScanner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/FOSScanner"; }; name = Release; }; @@ -426,7 +426,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.fosscanner.app.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/fosscanner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/fosscanner"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FOSScanner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/FOSScanner"; }; name = Profile; }; diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 779c1f2..95d2bc8 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -15,7 +15,7 @@ @@ -33,7 +33,7 @@ @@ -49,7 +49,7 @@ @@ -84,7 +84,7 @@ @@ -101,7 +101,7 @@ diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig index 716cec4..66ff51c 100644 --- a/macos/Runner/Configs/AppInfo.xcconfig +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -5,7 +5,7 @@ // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = fosscanner +PRODUCT_NAME = FOSScanner // The application's bundle identifier PRODUCT_BUNDLE_IDENTIFIER = com.fosscanner.app diff --git a/pubspec.lock b/pubspec.lock index e973163..bdd378c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -49,6 +49,46 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + camera: + dependency: transitive + description: + name: camera + sha256: "558230d6ce6ccea856b32d390db7e7b557adf4d9320aa614481bd3f2f608953f" + url: "https://pub.dev" + source: hosted + version: "0.12.0+2" + camera_android_camerax: + dependency: transitive + description: + name: camera_android_camerax + sha256: a13f12f9c067679bf11f0f61e3f2079ac5c740388dec2e46d82e9f28ea093b4f + url: "https://pub.dev" + source: hosted + version: "0.7.4+7" + camera_avfoundation: + dependency: transitive + description: + name: camera_avfoundation + sha256: "843247230a583e04742ea1434ce3f91e536760922fab67c72ce46aa34fac3e9d" + url: "https://pub.dev" + source: hosted + version: "0.10.3" + camera_platform_interface: + dependency: transitive + description: + name: camera_platform_interface + sha256: "4524ca6eb4176b066864036ad4fe02c3e4863e63b77eadc21a5bf56824f43498" + url: "https://pub.dev" + source: hosted + version: "2.13.1" + camera_web: + dependency: transitive + description: + name: camera_web + sha256: "081441bd2f92b5841aa70ff4d6eddfe34078d97f9d085cc9e0f9d0102f145925" + url: "https://pub.dev" + source: hosted + version: "0.3.5+5" change_case: dependency: transitive description: @@ -240,6 +280,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_zxing: + dependency: "direct main" + description: + name: flutter_zxing + sha256: "6bf0cb0fde122455e73ae78214457c106fbde3c633af4624ce29567ac590ef21" + url: "https://pub.dev" + source: hosted + version: "2.3.0" glob: dependency: transitive description: @@ -420,10 +468,10 @@ packages: dependency: transitive description: name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd" url: "https://pub.dev" source: hosted - version: "0.12.19" + version: "0.12.20" material_color_utilities: dependency: transitive description: @@ -436,10 +484,10 @@ packages: dependency: transitive description: name: meta - sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" mime: dependency: transitive description: @@ -480,6 +528,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: "127e1751e37ffb2ff4658beeaca77bad0c27bf5f932bd3a501c2296926d4b481" + url: "https://pub.dev" + source: hosted + version: "10.2.1" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: db762cb2f4f25ee60fb6359773861b0f199e00b90d237bd85a76a1e806b46ef4 + url: "https://pub.dev" + source: hosted + version: "4.1.0" path: dependency: transitive description: @@ -653,6 +717,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" string_scanner: dependency: transitive description: @@ -673,10 +745,10 @@ packages: dependency: transitive description: name: test_api - sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11" url: "https://pub.dev" source: hosted - version: "0.7.11" + version: "0.7.12" typed_data: dependency: transitive description: @@ -685,6 +757,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32 + url: "https://pub.dev" + source: hosted + version: "6.3.32" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + url: "https://pub.dev" + source: hosted + version: "6.4.1" url_launcher_linux: dependency: transitive description: @@ -693,6 +789,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" url_launcher_platform_interface: dependency: transitive description: @@ -729,10 +833,10 @@ packages: dependency: transitive description: name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + sha256: f36f9f3be64c6198714492bb455c11056e33e2f85d9a0b676a48301e44fdcf47 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.4.2" vm_service: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 217e118..7e16d95 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,6 +14,9 @@ dependencies: pdf: ^3.10.7 share_plus: ^13.3.0 opencv_dart: ^2.2.1 + package_info_plus: ^10.2.1 + url_launcher: ^6.3.2 + flutter_zxing: ^2.3.0 dev_dependencies: flutter_test: diff --git a/test/services/corner_geometry_test.dart b/test/services/corner_geometry_test.dart index 3d47f4f..5ec3d01 100644 --- a/test/services/corner_geometry_test.dart +++ b/test/services/corner_geometry_test.dart @@ -94,6 +94,27 @@ void main() { ); }); + test( + 'warp dimensions reject a valid quad whose corners are role-shifted', + () { + // Same rectangle as tl,tr,br,bl but rotated one position in the array + // (tr,br,bl,tl). Still convex and consistently wound — the shape + // itself is completely valid — so this only fails if role order is + // checked, not just geometry. Regression test for the corner-adjust + // bug where dragging handles past each other silently flipped output + // instead of being rejected. + expect( + () => calculateWarpSize(const [ + Offset(100, 0), + Offset(100, 100), + Offset(0, 100), + Offset(0, 0), + ]), + throwsArgumentError, + ); + }, + ); + test('warp dimensions reject self-intersecting corners', () { expect( () => calculateWarpSize(const [ diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp index 26dcd67..5d31218 100644 --- a/windows/runner/main.cpp +++ b/windows/runner/main.cpp @@ -27,7 +27,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); - if (!window.Create(L"fosscanner", origin, size)) { + if (!window.Create(L"FOSScanner", origin, size)) { return EXIT_FAILURE; } window.SetQuitOnClose(true);