Compare commits
3 Commits
22f27641ca
...
1d70c029b3
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d70c029b3 | |||
| f48e43f13d | |||
| 5db364d1fc |
7
l10n.yaml
Normal file
7
l10n.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
# Flutter gen-l10n config. Generates AppLocalizations from the ARB files in
|
||||
# lib/l10n. `flutter pub get` / build runs the generator (pubspec `generate: true`).
|
||||
arb-dir: lib/l10n
|
||||
template-arb-file: app_en.arb
|
||||
output-localization-file: app_localizations.dart
|
||||
output-class: AppLocalizations
|
||||
nullable-getter: false
|
||||
@@ -8,6 +8,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pdfrx/pdfrx.dart';
|
||||
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '../../services/database_service.dart';
|
||||
import '../engine/stroke_geometry.dart' show kDefaultPenThinning;
|
||||
import '../engine/stroke_model.dart';
|
||||
@@ -398,6 +399,7 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l = AppLocalizations.of(context);
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
@@ -429,7 +431,7 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: _RoundIconButton(
|
||||
icon: Icons.arrow_back,
|
||||
tooltip: 'Back',
|
||||
tooltip: l.back,
|
||||
onPressed: () => Navigator.of(context).maybePop(),
|
||||
),
|
||||
),
|
||||
@@ -510,15 +512,16 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
final l = AppLocalizations.of(context);
|
||||
if (_openError != null) {
|
||||
return Center(child: Text('Failed to open PDF:\n$_openError'));
|
||||
return Center(child: Text(l.failedToOpenPdf('$_openError')));
|
||||
}
|
||||
final doc = _document;
|
||||
if (doc == null) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (doc.pages.isEmpty) {
|
||||
return const Center(child: Text('PDF has no pages.'));
|
||||
return Center(child: Text(l.pdfNoPages));
|
||||
}
|
||||
|
||||
final page = doc.pages[_pageIndex];
|
||||
@@ -584,6 +587,7 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
/// tools, color dots, and finger-drawing toggle.
|
||||
Widget _buildToolPalette() {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final l = AppLocalizations.of(context);
|
||||
return Material(
|
||||
color: cs.surfaceContainerHigh,
|
||||
elevation: 3,
|
||||
@@ -596,19 +600,19 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
_ToolButton(
|
||||
icon: Icons.edit_outlined,
|
||||
selected: _tool == CanvasTool.pen,
|
||||
tooltip: 'Pen',
|
||||
tooltip: l.toolPen,
|
||||
onPressed: () => setState(() => _tool = CanvasTool.pen),
|
||||
),
|
||||
_ToolButton(
|
||||
icon: Icons.brush_outlined,
|
||||
selected: _tool == CanvasTool.highlighter,
|
||||
tooltip: 'Highlighter',
|
||||
tooltip: l.toolHighlighter,
|
||||
onPressed: () => setState(() => _tool = CanvasTool.highlighter),
|
||||
),
|
||||
_ToolButton(
|
||||
icon: Icons.cleaning_services_outlined,
|
||||
selected: _tool == CanvasTool.eraser,
|
||||
tooltip: 'Eraser',
|
||||
tooltip: l.toolEraser,
|
||||
onPressed: () => setState(() => _tool = CanvasTool.eraser),
|
||||
),
|
||||
_Divider(cs: cs),
|
||||
@@ -616,13 +620,13 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
_ToolButton(
|
||||
icon: Icons.undo,
|
||||
selected: false,
|
||||
tooltip: 'Undo',
|
||||
tooltip: l.actionUndo,
|
||||
onPressed: _undoFor(_pageIndex).canUndo ? _performUndo : null,
|
||||
),
|
||||
_ToolButton(
|
||||
icon: Icons.redo,
|
||||
selected: false,
|
||||
tooltip: 'Redo',
|
||||
tooltip: l.actionRedo,
|
||||
onPressed: _undoFor(_pageIndex).canRedo ? _performRedo : null,
|
||||
),
|
||||
_Divider(cs: cs),
|
||||
@@ -632,28 +636,28 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
icon: _allowFingerDrawing ? Icons.touch_app : Icons.do_not_touch,
|
||||
selected: _allowFingerDrawing,
|
||||
tooltip: _allowFingerDrawing
|
||||
? 'Finger drawing ON'
|
||||
: 'Finger drawing OFF (pen only)',
|
||||
? l.fingerDrawingOn
|
||||
: l.fingerDrawingOff,
|
||||
onPressed: _toggleFingerDrawing,
|
||||
),
|
||||
// Page thumbnail grid.
|
||||
_ToolButton(
|
||||
icon: Icons.grid_view,
|
||||
selected: false,
|
||||
tooltip: 'Pages',
|
||||
tooltip: l.pages,
|
||||
onPressed: _document != null ? _openThumbnails : null,
|
||||
),
|
||||
// Pen settings.
|
||||
_ToolButton(
|
||||
icon: Icons.settings_outlined,
|
||||
selected: false,
|
||||
tooltip: 'Pen settings',
|
||||
tooltip: l.penSettings,
|
||||
onPressed: _penConfig != null ? _openPenSettings : null,
|
||||
),
|
||||
_ToolButton(
|
||||
icon: Icons.bug_report_outlined,
|
||||
selected: _showPenDebug,
|
||||
tooltip: 'Input diagnostic (writes a log file)',
|
||||
tooltip: l.inputDiagnostic,
|
||||
onPressed: () {
|
||||
final on = !_showPenDebug;
|
||||
setState(() => _showPenDebug = on);
|
||||
@@ -699,6 +703,7 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
Widget _buildPagePill() {
|
||||
final doc = _document!;
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final l = AppLocalizations.of(context);
|
||||
final total = doc.pages.length;
|
||||
final shown = (_scrub ?? (_pageIndex + 1).toDouble()).round();
|
||||
return Column(
|
||||
@@ -742,7 +747,7 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
tooltip: 'Previous page',
|
||||
tooltip: l.previousPage,
|
||||
icon: const Icon(Icons.chevron_left),
|
||||
onPressed:
|
||||
_pageIndex > 0 ? () => _goToPage(_pageIndex - 1) : null,
|
||||
@@ -752,7 +757,7 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
? () => setState(() => _showSlider = !_showSlider)
|
||||
: null,
|
||||
child: Text(
|
||||
'$shown / $total',
|
||||
l.pageOfPages(shown, total),
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -760,7 +765,7 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Next page',
|
||||
tooltip: l.nextPage,
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
onPressed: _pageIndex < total - 1
|
||||
? () => _goToPage(_pageIndex + 1)
|
||||
|
||||
@@ -32,6 +32,7 @@ import 'package:flutter/physics.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'input_diagnostics.dart';
|
||||
import 'pinch_scale_solver.dart';
|
||||
|
||||
/// Devices allowed to pan/zoom. Stylus + invertedStylus are excluded so the pen
|
||||
/// is owned exclusively by the drawing `Listener`.
|
||||
@@ -116,6 +117,18 @@ class _PenInteractiveViewerState extends State<PenInteractiveViewer>
|
||||
double _lastRawScale = 1.0;
|
||||
double _lastAppliedScale = 1.0;
|
||||
|
||||
/// The recognizer's cumulative `details.scale` AT THE CURRENT BASELINE (the
|
||||
/// gesture start, or the last pointer-count re-baseline). The absolute target
|
||||
/// is `_scaleStart * (details.scale / _rawScaleAtBaseline)`: dividing by this
|
||||
/// re-normalizes the cumulative scale so it reads 1.0 at the baseline moment.
|
||||
///
|
||||
/// Without this, a mid-gesture re-baseline (a finger blips 2→1→2 — routine on
|
||||
/// Windows touch) captured a fresh `_scaleStart` but left `details.scale` at
|
||||
/// its un-normalized cumulative value, so the next frame computed
|
||||
/// `_scaleStart * 0.40` and the zoom popped to a wrong scale then snapped back
|
||||
/// (the reported flicker). Normalizing kills that pop at the source.
|
||||
double _rawScaleAtBaseline = 1.0;
|
||||
|
||||
// --- Matrix helpers (infinite boundary → no clamping to bounds) -----------
|
||||
|
||||
Matrix4 _matrixTranslate(Matrix4 matrix, Offset translation) {
|
||||
@@ -169,6 +182,7 @@ class _PenInteractiveViewerState extends State<PenInteractiveViewer>
|
||||
_referenceFocalPoint = _transformer.toScene(details.localFocalPoint);
|
||||
_lastRawScale = 1.0;
|
||||
_lastAppliedScale = _scaleStart!;
|
||||
_rawScaleAtBaseline = 1.0;
|
||||
}
|
||||
|
||||
void _onScaleUpdate(ScaleUpdateDetails details) {
|
||||
@@ -184,6 +198,10 @@ class _PenInteractiveViewerState extends State<PenInteractiveViewer>
|
||||
_referenceFocalPoint = _transformer.toScene(details.localFocalPoint);
|
||||
_lastRawScale = details.scale;
|
||||
_lastAppliedScale = _scaleStart!;
|
||||
// Re-anchor the absolute mapping: from here, cumulative scale is measured
|
||||
// relative to THIS frame's details.scale (so the next good frame starts
|
||||
// from _scaleStart, not _scaleStart * a stale cumulative value).
|
||||
_rawScaleAtBaseline = details.scale;
|
||||
InputDiagnostics.instance.recordRebaseline();
|
||||
return;
|
||||
}
|
||||
@@ -242,10 +260,15 @@ class _PenInteractiveViewerState extends State<PenInteractiveViewer>
|
||||
// for a pure scale+translate matrix — no inversion, no live read-back —
|
||||
// so an interleaved/transient matrix write can't survive into the next
|
||||
// frame: every frame is fully re-derived from clean inputs.
|
||||
final double targetScale = clampDouble(
|
||||
_scaleStart! * details.scale,
|
||||
widget.minScale,
|
||||
widget.maxScale,
|
||||
// Absolute target scale, normalized against the baseline so a
|
||||
// mid-gesture re-baseline (finger blip) can't pop the zoom. See
|
||||
// pinch_scale_solver.dart for the full rationale.
|
||||
final double targetScale = absolutePinchScale(
|
||||
scaleStart: _scaleStart!,
|
||||
rawScaleAtBaseline: _rawScaleAtBaseline,
|
||||
rawScale: details.scale,
|
||||
minScale: widget.minScale,
|
||||
maxScale: widget.maxScale,
|
||||
);
|
||||
final Offset focal = details.localFocalPoint;
|
||||
final double tx = focal.dx - targetScale * _referenceFocalPoint!.dx;
|
||||
|
||||
41
lib/editor/canvas/pinch_scale_solver.dart
Normal file
41
lib/editor/canvas/pinch_scale_solver.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
// lib/editor/canvas/pinch_scale_solver.dart
|
||||
//
|
||||
// Pure math for the pen canvas's absolute pinch-zoom. Extracted so the
|
||||
// re-baseline behavior (the subtle part) can be unit-tested without simulating
|
||||
// a flaky multi-pointer gesture.
|
||||
//
|
||||
// The pinch is driven ABSOLUTELY: the scale shown is always
|
||||
// scaleStart * (rawScale / rawScaleAtBaseline)
|
||||
// where `scaleStart` is the matrix scale captured at the current baseline and
|
||||
// `rawScaleAtBaseline` is the recognizer's cumulative `details.scale` at that
|
||||
// same baseline. Dividing by `rawScaleAtBaseline` re-normalizes the cumulative
|
||||
// scale so it reads 1.0 at the baseline instant.
|
||||
//
|
||||
// Why this matters: a baseline is captured at gesture start AND on every
|
||||
// pointer-count change (a finger blips 2→1→2, routine on Windows touch). At
|
||||
// gesture start `details.scale` is 1.0, so a naive `scaleStart * rawScale` is
|
||||
// correct. But at a MID-GESTURE re-baseline `details.scale` is whatever the
|
||||
// pinch has accumulated (e.g. 0.40) — multiplying the fresh `scaleStart` by
|
||||
// that stale 0.40 popped the zoom to a wrong scale and snapped back (the
|
||||
// reported flicker). Normalizing against `rawScaleAtBaseline` removes the pop.
|
||||
|
||||
import 'package:flutter/foundation.dart' show clampDouble;
|
||||
|
||||
/// Returns the absolute target scale for a pinch frame.
|
||||
///
|
||||
/// [scaleStart] — matrix scale captured at the current baseline.
|
||||
/// [rawScaleAtBaseline] — recognizer cumulative `details.scale` at that
|
||||
/// baseline (1.0 at gesture start; the live value at a re-baseline).
|
||||
/// [rawScale] — the recognizer's current cumulative `details.scale`.
|
||||
/// Result is clamped to [minScale, maxScale].
|
||||
double absolutePinchScale({
|
||||
required double scaleStart,
|
||||
required double rawScaleAtBaseline,
|
||||
required double rawScale,
|
||||
required double minScale,
|
||||
required double maxScale,
|
||||
}) {
|
||||
final double cumulative =
|
||||
rawScaleAtBaseline > 0 ? rawScale / rawScaleAtBaseline : 1.0;
|
||||
return clampDouble(scaleStart * cumulative, minScale, maxScale);
|
||||
}
|
||||
46
lib/l10n/app_en.arb
Normal file
46
lib/l10n/app_en.arb
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"@@locale": "en",
|
||||
"appTitle": "BadNote",
|
||||
"settings": "Settings",
|
||||
"search": "Search",
|
||||
"importPdf": "Import PDF",
|
||||
"importPpt": "Import PPT",
|
||||
"penCanvasBeta": "Pen Canvas (beta)",
|
||||
"newNote": "New Note",
|
||||
"open": "Open",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete",
|
||||
"deleteNoteTitle": "Delete note?",
|
||||
"deleteNote": "Delete note",
|
||||
"openInSplitView": "Open in Split View",
|
||||
"splitViewSubtitle": "PDF reference + scratchpad",
|
||||
"removeDocument": "Remove document",
|
||||
"processingPptx": "Processing PPTX...",
|
||||
"processingPresentation": "Processing presentation...",
|
||||
"couldNotOpenPresentation": "Could not open presentation.",
|
||||
"toolPen": "Pen",
|
||||
"toolHighlighter": "Highlighter",
|
||||
"toolEraser": "Eraser",
|
||||
"actionUndo": "Undo",
|
||||
"actionRedo": "Redo",
|
||||
"fingerDrawingOn": "Finger drawing ON",
|
||||
"fingerDrawingOff": "Finger drawing OFF (pen only)",
|
||||
"pages": "Pages",
|
||||
"penSettings": "Pen settings",
|
||||
"inputDiagnostic": "Input diagnostic (writes a log file)",
|
||||
"back": "Back",
|
||||
"previousPage": "Previous page",
|
||||
"nextPage": "Next page",
|
||||
"failedToOpenPdf": "Failed to open PDF:\n{error}",
|
||||
"@failedToOpenPdf": {
|
||||
"placeholders": { "error": { "type": "String" } }
|
||||
},
|
||||
"pdfNoPages": "PDF has no pages.",
|
||||
"pageOfPages": "{current} / {total}",
|
||||
"@pageOfPages": {
|
||||
"placeholders": {
|
||||
"current": { "type": "int" },
|
||||
"total": { "type": "int" }
|
||||
}
|
||||
}
|
||||
}
|
||||
338
lib/l10n/app_localizations.dart
Normal file
338
lib/l10n/app_localizations.dart
Normal file
@@ -0,0 +1,338 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
|
||||
import 'app_localizations_en.dart';
|
||||
import 'app_localizations_zh.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// Callers can lookup localized strings with an instance of AppLocalizations
|
||||
/// returned by `AppLocalizations.of(context)`.
|
||||
///
|
||||
/// Applications need to include `AppLocalizations.delegate()` in their app's
|
||||
/// `localizationDelegates` list, and the locales they support in the app's
|
||||
/// `supportedLocales` list. For example:
|
||||
///
|
||||
/// ```dart
|
||||
/// import 'l10n/app_localizations.dart';
|
||||
///
|
||||
/// return MaterialApp(
|
||||
/// localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
/// supportedLocales: AppLocalizations.supportedLocales,
|
||||
/// home: MyApplicationHome(),
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// ## Update pubspec.yaml
|
||||
///
|
||||
/// Please make sure to update your pubspec.yaml to include the following
|
||||
/// packages:
|
||||
///
|
||||
/// ```yaml
|
||||
/// dependencies:
|
||||
/// # Internationalization support.
|
||||
/// flutter_localizations:
|
||||
/// sdk: flutter
|
||||
/// intl: any # Use the pinned version from flutter_localizations
|
||||
///
|
||||
/// # Rest of dependencies
|
||||
/// ```
|
||||
///
|
||||
/// ## iOS Applications
|
||||
///
|
||||
/// iOS applications define key application metadata, including supported
|
||||
/// locales, in an Info.plist file that is built into the application bundle.
|
||||
/// To configure the locales supported by your app, you’ll need to edit this
|
||||
/// file.
|
||||
///
|
||||
/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file.
|
||||
/// Then, in the Project Navigator, open the Info.plist file under the Runner
|
||||
/// project’s Runner folder.
|
||||
///
|
||||
/// Next, select the Information Property List item, select Add Item from the
|
||||
/// Editor menu, then select Localizations from the pop-up menu.
|
||||
///
|
||||
/// Select and expand the newly-created Localizations item then, for each
|
||||
/// locale your application supports, add a new item and select the locale
|
||||
/// you wish to add from the pop-up menu in the Value field. This list should
|
||||
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
|
||||
/// property.
|
||||
abstract class AppLocalizations {
|
||||
AppLocalizations(String locale)
|
||||
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
|
||||
|
||||
final String localeName;
|
||||
|
||||
static AppLocalizations of(BuildContext context) {
|
||||
return Localizations.of<AppLocalizations>(context, AppLocalizations)!;
|
||||
}
|
||||
|
||||
static const LocalizationsDelegate<AppLocalizations> delegate =
|
||||
_AppLocalizationsDelegate();
|
||||
|
||||
/// A list of this localizations delegate along with the default localizations
|
||||
/// delegates.
|
||||
///
|
||||
/// Returns a list of localizations delegates containing this delegate along with
|
||||
/// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
|
||||
/// and GlobalWidgetsLocalizations.delegate.
|
||||
///
|
||||
/// Additional delegates can be added by appending to this list in
|
||||
/// MaterialApp. This list does not have to be used at all if a custom list
|
||||
/// of delegates is preferred or required.
|
||||
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
|
||||
<LocalizationsDelegate<dynamic>>[
|
||||
delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
];
|
||||
|
||||
/// A list of this localizations delegate's supported locales.
|
||||
static const List<Locale> supportedLocales = <Locale>[
|
||||
Locale('en'),
|
||||
Locale('zh'),
|
||||
];
|
||||
|
||||
/// No description provided for @appTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'BadNote'**
|
||||
String get appTitle;
|
||||
|
||||
/// No description provided for @settings.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Settings'**
|
||||
String get settings;
|
||||
|
||||
/// No description provided for @search.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Search'**
|
||||
String get search;
|
||||
|
||||
/// No description provided for @importPdf.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Import PDF'**
|
||||
String get importPdf;
|
||||
|
||||
/// No description provided for @importPpt.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Import PPT'**
|
||||
String get importPpt;
|
||||
|
||||
/// No description provided for @penCanvasBeta.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Pen Canvas (beta)'**
|
||||
String get penCanvasBeta;
|
||||
|
||||
/// No description provided for @newNote.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'New Note'**
|
||||
String get newNote;
|
||||
|
||||
/// No description provided for @open.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Open'**
|
||||
String get open;
|
||||
|
||||
/// No description provided for @cancel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Cancel'**
|
||||
String get cancel;
|
||||
|
||||
/// No description provided for @delete.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete'**
|
||||
String get delete;
|
||||
|
||||
/// No description provided for @deleteNoteTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete note?'**
|
||||
String get deleteNoteTitle;
|
||||
|
||||
/// No description provided for @deleteNote.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete note'**
|
||||
String get deleteNote;
|
||||
|
||||
/// No description provided for @openInSplitView.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Open in Split View'**
|
||||
String get openInSplitView;
|
||||
|
||||
/// No description provided for @splitViewSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'PDF reference + scratchpad'**
|
||||
String get splitViewSubtitle;
|
||||
|
||||
/// No description provided for @removeDocument.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Remove document'**
|
||||
String get removeDocument;
|
||||
|
||||
/// No description provided for @processingPptx.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Processing PPTX...'**
|
||||
String get processingPptx;
|
||||
|
||||
/// No description provided for @processingPresentation.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Processing presentation...'**
|
||||
String get processingPresentation;
|
||||
|
||||
/// No description provided for @couldNotOpenPresentation.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Could not open presentation.'**
|
||||
String get couldNotOpenPresentation;
|
||||
|
||||
/// No description provided for @toolPen.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Pen'**
|
||||
String get toolPen;
|
||||
|
||||
/// No description provided for @toolHighlighter.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Highlighter'**
|
||||
String get toolHighlighter;
|
||||
|
||||
/// No description provided for @toolEraser.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Eraser'**
|
||||
String get toolEraser;
|
||||
|
||||
/// No description provided for @actionUndo.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Undo'**
|
||||
String get actionUndo;
|
||||
|
||||
/// No description provided for @actionRedo.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Redo'**
|
||||
String get actionRedo;
|
||||
|
||||
/// No description provided for @fingerDrawingOn.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Finger drawing ON'**
|
||||
String get fingerDrawingOn;
|
||||
|
||||
/// No description provided for @fingerDrawingOff.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Finger drawing OFF (pen only)'**
|
||||
String get fingerDrawingOff;
|
||||
|
||||
/// No description provided for @pages.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Pages'**
|
||||
String get pages;
|
||||
|
||||
/// No description provided for @penSettings.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Pen settings'**
|
||||
String get penSettings;
|
||||
|
||||
/// No description provided for @inputDiagnostic.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Input diagnostic (writes a log file)'**
|
||||
String get inputDiagnostic;
|
||||
|
||||
/// No description provided for @back.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Back'**
|
||||
String get back;
|
||||
|
||||
/// No description provided for @previousPage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Previous page'**
|
||||
String get previousPage;
|
||||
|
||||
/// No description provided for @nextPage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Next page'**
|
||||
String get nextPage;
|
||||
|
||||
/// No description provided for @failedToOpenPdf.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Failed to open PDF:\n{error}'**
|
||||
String failedToOpenPdf(String error);
|
||||
|
||||
/// No description provided for @pdfNoPages.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'PDF has no pages.'**
|
||||
String get pdfNoPages;
|
||||
|
||||
/// No description provided for @pageOfPages.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{current} / {total}'**
|
||||
String pageOfPages(int current, int total);
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
extends LocalizationsDelegate<AppLocalizations> {
|
||||
const _AppLocalizationsDelegate();
|
||||
|
||||
@override
|
||||
Future<AppLocalizations> load(Locale locale) {
|
||||
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
|
||||
}
|
||||
|
||||
@override
|
||||
bool isSupported(Locale locale) =>
|
||||
<String>['en', 'zh'].contains(locale.languageCode);
|
||||
|
||||
@override
|
||||
bool shouldReload(_AppLocalizationsDelegate old) => false;
|
||||
}
|
||||
|
||||
AppLocalizations lookupAppLocalizations(Locale locale) {
|
||||
// Lookup logic when only language code is specified.
|
||||
switch (locale.languageCode) {
|
||||
case 'en':
|
||||
return AppLocalizationsEn();
|
||||
case 'zh':
|
||||
return AppLocalizationsZh();
|
||||
}
|
||||
|
||||
throw FlutterError(
|
||||
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
|
||||
'an issue with the localizations generation tool. Please file an issue '
|
||||
'on GitHub with a reproducible sample app and the gen-l10n configuration '
|
||||
'that was used.',
|
||||
);
|
||||
}
|
||||
116
lib/l10n/app_localizations_en.dart
Normal file
116
lib/l10n/app_localizations_en.dart
Normal file
@@ -0,0 +1,116 @@
|
||||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// The translations for English (`en`).
|
||||
class AppLocalizationsEn extends AppLocalizations {
|
||||
AppLocalizationsEn([String locale = 'en']) : super(locale);
|
||||
|
||||
@override
|
||||
String get appTitle => 'BadNote';
|
||||
|
||||
@override
|
||||
String get settings => 'Settings';
|
||||
|
||||
@override
|
||||
String get search => 'Search';
|
||||
|
||||
@override
|
||||
String get importPdf => 'Import PDF';
|
||||
|
||||
@override
|
||||
String get importPpt => 'Import PPT';
|
||||
|
||||
@override
|
||||
String get penCanvasBeta => 'Pen Canvas (beta)';
|
||||
|
||||
@override
|
||||
String get newNote => 'New Note';
|
||||
|
||||
@override
|
||||
String get open => 'Open';
|
||||
|
||||
@override
|
||||
String get cancel => 'Cancel';
|
||||
|
||||
@override
|
||||
String get delete => 'Delete';
|
||||
|
||||
@override
|
||||
String get deleteNoteTitle => 'Delete note?';
|
||||
|
||||
@override
|
||||
String get deleteNote => 'Delete note';
|
||||
|
||||
@override
|
||||
String get openInSplitView => 'Open in Split View';
|
||||
|
||||
@override
|
||||
String get splitViewSubtitle => 'PDF reference + scratchpad';
|
||||
|
||||
@override
|
||||
String get removeDocument => 'Remove document';
|
||||
|
||||
@override
|
||||
String get processingPptx => 'Processing PPTX...';
|
||||
|
||||
@override
|
||||
String get processingPresentation => 'Processing presentation...';
|
||||
|
||||
@override
|
||||
String get couldNotOpenPresentation => 'Could not open presentation.';
|
||||
|
||||
@override
|
||||
String get toolPen => 'Pen';
|
||||
|
||||
@override
|
||||
String get toolHighlighter => 'Highlighter';
|
||||
|
||||
@override
|
||||
String get toolEraser => 'Eraser';
|
||||
|
||||
@override
|
||||
String get actionUndo => 'Undo';
|
||||
|
||||
@override
|
||||
String get actionRedo => 'Redo';
|
||||
|
||||
@override
|
||||
String get fingerDrawingOn => 'Finger drawing ON';
|
||||
|
||||
@override
|
||||
String get fingerDrawingOff => 'Finger drawing OFF (pen only)';
|
||||
|
||||
@override
|
||||
String get pages => 'Pages';
|
||||
|
||||
@override
|
||||
String get penSettings => 'Pen settings';
|
||||
|
||||
@override
|
||||
String get inputDiagnostic => 'Input diagnostic (writes a log file)';
|
||||
|
||||
@override
|
||||
String get back => 'Back';
|
||||
|
||||
@override
|
||||
String get previousPage => 'Previous page';
|
||||
|
||||
@override
|
||||
String get nextPage => 'Next page';
|
||||
|
||||
@override
|
||||
String failedToOpenPdf(String error) {
|
||||
return 'Failed to open PDF:\n$error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pdfNoPages => 'PDF has no pages.';
|
||||
|
||||
@override
|
||||
String pageOfPages(int current, int total) {
|
||||
return '$current / $total';
|
||||
}
|
||||
}
|
||||
116
lib/l10n/app_localizations_zh.dart
Normal file
116
lib/l10n/app_localizations_zh.dart
Normal file
@@ -0,0 +1,116 @@
|
||||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// The translations for Chinese (`zh`).
|
||||
class AppLocalizationsZh extends AppLocalizations {
|
||||
AppLocalizationsZh([String locale = 'zh']) : super(locale);
|
||||
|
||||
@override
|
||||
String get appTitle => 'BadNote';
|
||||
|
||||
@override
|
||||
String get settings => '设置';
|
||||
|
||||
@override
|
||||
String get search => '搜索';
|
||||
|
||||
@override
|
||||
String get importPdf => '导入 PDF';
|
||||
|
||||
@override
|
||||
String get importPpt => '导入 PPT';
|
||||
|
||||
@override
|
||||
String get penCanvasBeta => '手写画布(测试版)';
|
||||
|
||||
@override
|
||||
String get newNote => '新建笔记';
|
||||
|
||||
@override
|
||||
String get open => '打开';
|
||||
|
||||
@override
|
||||
String get cancel => '取消';
|
||||
|
||||
@override
|
||||
String get delete => '删除';
|
||||
|
||||
@override
|
||||
String get deleteNoteTitle => '删除笔记?';
|
||||
|
||||
@override
|
||||
String get deleteNote => '删除笔记';
|
||||
|
||||
@override
|
||||
String get openInSplitView => '分屏打开';
|
||||
|
||||
@override
|
||||
String get splitViewSubtitle => 'PDF 参考 + 草稿纸';
|
||||
|
||||
@override
|
||||
String get removeDocument => '移除文档';
|
||||
|
||||
@override
|
||||
String get processingPptx => '正在处理 PPTX…';
|
||||
|
||||
@override
|
||||
String get processingPresentation => '正在处理演示文稿…';
|
||||
|
||||
@override
|
||||
String get couldNotOpenPresentation => '无法打开演示文稿。';
|
||||
|
||||
@override
|
||||
String get toolPen => '钢笔';
|
||||
|
||||
@override
|
||||
String get toolHighlighter => '荧光笔';
|
||||
|
||||
@override
|
||||
String get toolEraser => '橡皮擦';
|
||||
|
||||
@override
|
||||
String get actionUndo => '撤销';
|
||||
|
||||
@override
|
||||
String get actionRedo => '重做';
|
||||
|
||||
@override
|
||||
String get fingerDrawingOn => '手指书写:开';
|
||||
|
||||
@override
|
||||
String get fingerDrawingOff => '手指书写:关(仅手写笔)';
|
||||
|
||||
@override
|
||||
String get pages => '页面';
|
||||
|
||||
@override
|
||||
String get penSettings => '手写笔设置';
|
||||
|
||||
@override
|
||||
String get inputDiagnostic => '输入诊断(写入日志文件)';
|
||||
|
||||
@override
|
||||
String get back => '返回';
|
||||
|
||||
@override
|
||||
String get previousPage => '上一页';
|
||||
|
||||
@override
|
||||
String get nextPage => '下一页';
|
||||
|
||||
@override
|
||||
String failedToOpenPdf(String error) {
|
||||
return '打开 PDF 失败:\n$error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pdfNoPages => 'PDF 没有任何页面。';
|
||||
|
||||
@override
|
||||
String pageOfPages(int current, int total) {
|
||||
return '$current / $total';
|
||||
}
|
||||
}
|
||||
37
lib/l10n/app_zh.arb
Normal file
37
lib/l10n/app_zh.arb
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"@@locale": "zh",
|
||||
"appTitle": "BadNote",
|
||||
"settings": "设置",
|
||||
"search": "搜索",
|
||||
"importPdf": "导入 PDF",
|
||||
"importPpt": "导入 PPT",
|
||||
"penCanvasBeta": "手写画布(测试版)",
|
||||
"newNote": "新建笔记",
|
||||
"open": "打开",
|
||||
"cancel": "取消",
|
||||
"delete": "删除",
|
||||
"deleteNoteTitle": "删除笔记?",
|
||||
"deleteNote": "删除笔记",
|
||||
"openInSplitView": "分屏打开",
|
||||
"splitViewSubtitle": "PDF 参考 + 草稿纸",
|
||||
"removeDocument": "移除文档",
|
||||
"processingPptx": "正在处理 PPTX…",
|
||||
"processingPresentation": "正在处理演示文稿…",
|
||||
"couldNotOpenPresentation": "无法打开演示文稿。",
|
||||
"toolPen": "钢笔",
|
||||
"toolHighlighter": "荧光笔",
|
||||
"toolEraser": "橡皮擦",
|
||||
"actionUndo": "撤销",
|
||||
"actionRedo": "重做",
|
||||
"fingerDrawingOn": "手指书写:开",
|
||||
"fingerDrawingOff": "手指书写:关(仅手写笔)",
|
||||
"pages": "页面",
|
||||
"penSettings": "手写笔设置",
|
||||
"inputDiagnostic": "输入诊断(写入日志文件)",
|
||||
"back": "返回",
|
||||
"previousPage": "上一页",
|
||||
"nextPage": "下一页",
|
||||
"failedToOpenPdf": "打开 PDF 失败:\n{error}",
|
||||
"pdfNoPages": "PDF 没有任何页面。",
|
||||
"pageOfPages": "{current} / {total}"
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:pdfrx/pdfrx.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'editor/pdf/pen_capture_region.dart';
|
||||
import 'l10n/app_localizations.dart';
|
||||
import 'providers/settings_provider.dart';
|
||||
import 'screens/home_screen.dart';
|
||||
import 'services/database_service.dart';
|
||||
@@ -54,6 +56,14 @@ class BadNoteApp extends ConsumerWidget {
|
||||
themeMode: settings.themeMode,
|
||||
theme: _theme(lightScheme),
|
||||
darkTheme: _theme(darkScheme),
|
||||
// i18n: follows the OS language (en / zh) via the system locale.
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: const HomeScreen(),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../models/document.dart';
|
||||
import '../models/note.dart';
|
||||
import '../providers/document_provider.dart';
|
||||
import '../providers/note_provider.dart';
|
||||
import '../providers/ocr_provider.dart';
|
||||
import '../editor/canvas/pen_editor_screen.dart';
|
||||
import '../editor/pdf/spike_launcher.dart';
|
||||
import '../services/pdf_service.dart';
|
||||
import '../services/pptx_service.dart';
|
||||
import 'note_editor_screen.dart';
|
||||
import 'pdf_annotator_screen.dart';
|
||||
import 'ppt_annotator_screen.dart';
|
||||
import 'search_screen.dart';
|
||||
import 'settings_screen.dart';
|
||||
@@ -34,15 +35,16 @@ class HomeScreen extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final notesAsync = ref.watch(noteListProvider);
|
||||
final l = AppLocalizations.of(context);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('BadNote'),
|
||||
title: Text(l.appTitle),
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings),
|
||||
tooltip: 'Settings',
|
||||
tooltip: l.settings,
|
||||
onPressed: () {
|
||||
Navigator.of(
|
||||
context,
|
||||
@@ -51,17 +53,17 @@ class HomeScreen extends ConsumerWidget {
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.picture_as_pdf),
|
||||
tooltip: 'Import PDF',
|
||||
tooltip: l.importPdf,
|
||||
onPressed: () => _importPdf(context),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.slideshow),
|
||||
tooltip: 'Import PPT',
|
||||
tooltip: l.importPpt,
|
||||
onPressed: () => _importPptx(context),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.search),
|
||||
tooltip: 'Search',
|
||||
tooltip: l.search,
|
||||
onPressed: () {
|
||||
Navigator.of(
|
||||
context,
|
||||
@@ -71,7 +73,7 @@ class HomeScreen extends ConsumerWidget {
|
||||
// New pen-first canvas editor (beta).
|
||||
IconButton(
|
||||
icon: const Icon(Icons.draw_outlined),
|
||||
tooltip: 'Pen Canvas (beta)',
|
||||
tooltip: l.penCanvasBeta,
|
||||
onPressed: () => openM1Spike(context),
|
||||
),
|
||||
],
|
||||
@@ -204,7 +206,7 @@ class HomeScreen extends ConsumerWidget {
|
||||
if (filePath != null && context.mounted) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => PdfAnnotatorScreen(filePath: filePath),
|
||||
builder: (_) => PenEditorScreen(pdfPath: filePath),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -265,19 +267,19 @@ class HomeScreen extends ConsumerWidget {
|
||||
FilledButton.icon(
|
||||
onPressed: () => _createAndOpenNote(context, ref),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('New Note'),
|
||||
label: Text(AppLocalizations.of(context).newNote),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => _importPdf(context),
|
||||
icon: const Icon(Icons.picture_as_pdf),
|
||||
label: const Text('Import PDF'),
|
||||
label: Text(AppLocalizations.of(context).importPdf),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => _importPptx(context),
|
||||
icon: const Icon(Icons.slideshow),
|
||||
label: const Text('Import PPT'),
|
||||
label: Text(AppLocalizations.of(context).importPpt),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -532,7 +534,7 @@ class _DocumentTileState extends ConsumerState<_DocumentTile> {
|
||||
);
|
||||
}
|
||||
|
||||
// [L2] Route by docType: pdf → PdfAnnotatorScreen, ppt/pptx → PptAnnotatorScreen
|
||||
// [L2] Route by docType: pdf → PenEditorScreen (pen-first), ppt/pptx → PptAnnotatorScreen
|
||||
Future<void> _openDocument(BuildContext context) async {
|
||||
final document = widget.document;
|
||||
final isPdf = document.docType == 'pdf';
|
||||
@@ -540,7 +542,7 @@ class _DocumentTileState extends ConsumerState<_DocumentTile> {
|
||||
if (isPdf) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => PdfAnnotatorScreen(filePath: document.filePath),
|
||||
builder: (_) => PenEditorScreen(pdfPath: document.filePath),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -347,6 +347,11 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
flutter_localizations:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_onnxruntime:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -544,7 +549,7 @@ packages:
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
intl:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: intl
|
||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||
|
||||
@@ -13,6 +13,9 @@ environment:
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
intl: any
|
||||
cupertino_icons: ^1.0.8
|
||||
|
||||
# Pen & Ink Rendering
|
||||
@@ -80,6 +83,8 @@ dev_dependencies:
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
# Generate AppLocalizations from lib/l10n/*.arb (see l10n.yaml).
|
||||
generate: true
|
||||
|
||||
assets:
|
||||
- assets/models/ocr/
|
||||
|
||||
55
test/l10n_test.dart
Normal file
55
test/l10n_test.dart
Normal file
@@ -0,0 +1,55 @@
|
||||
// Proves i18n is real, not just wired: the SAME widget resolves English under
|
||||
// Locale('en') and Chinese under Locale('zh'), through the generated
|
||||
// AppLocalizations delegates. Guards against ARB keys drifting out of sync.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:badnote/l10n/app_localizations.dart';
|
||||
|
||||
class _Probe extends StatelessWidget {
|
||||
const _Probe();
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l = AppLocalizations.of(context);
|
||||
return Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Column(children: [
|
||||
Text(l.settings),
|
||||
Text(l.toolEraser),
|
||||
Text(l.importPdf),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pump(WidgetTester tester, Locale locale) {
|
||||
return tester.pumpWidget(MaterialApp(
|
||||
locale: locale,
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: const _Probe(),
|
||||
));
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('English locale resolves English strings', (tester) async {
|
||||
await _pump(tester, const Locale('en'));
|
||||
expect(find.text('Settings'), findsOneWidget);
|
||||
expect(find.text('Eraser'), findsOneWidget);
|
||||
expect(find.text('Import PDF'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Chinese locale resolves Chinese strings', (tester) async {
|
||||
await _pump(tester, const Locale('zh'));
|
||||
expect(find.text('设置'), findsOneWidget);
|
||||
expect(find.text('橡皮擦'), findsOneWidget);
|
||||
expect(find.text('导入 PDF'), findsOneWidget);
|
||||
});
|
||||
|
||||
test('both locales are supported', () {
|
||||
final langs =
|
||||
AppLocalizations.supportedLocales.map((l) => l.languageCode).toSet();
|
||||
expect(langs.containsAll({'en', 'zh'}), isTrue);
|
||||
});
|
||||
}
|
||||
64
test/pinch_scale_solver_test.dart
Normal file
64
test/pinch_scale_solver_test.dart
Normal file
@@ -0,0 +1,64 @@
|
||||
// Proves the absolute pinch-zoom math, focused on the re-baseline case that
|
||||
// produced the on-device flicker: when a finger blips (2→1→2) mid-pinch the
|
||||
// viewer captures a fresh baseline, and the OLD code multiplied that fresh
|
||||
// scaleStart by the recognizer's still-cumulative details.scale — popping the
|
||||
// zoom to a wrong value and snapping back. absolutePinchScale() normalizes
|
||||
// against the baseline so the pop cannot happen.
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:badnote/editor/canvas/pinch_scale_solver.dart';
|
||||
|
||||
void main() {
|
||||
const min = 0.5, max = 8.0;
|
||||
|
||||
double solve(double scaleStart, double baseline, double raw) =>
|
||||
absolutePinchScale(
|
||||
scaleStart: scaleStart,
|
||||
rawScaleAtBaseline: baseline,
|
||||
rawScale: raw,
|
||||
minScale: min,
|
||||
maxScale: max,
|
||||
);
|
||||
|
||||
test('at gesture start (baseline 1.0) target tracks raw directly', () {
|
||||
// scaleStart 1.0, baseline 1.0: pinch out to raw 2.0 → scale 2.0.
|
||||
expect(solve(1.0, 1.0, 2.0), closeTo(2.0, 1e-9));
|
||||
// pinch in to raw 0.5 → scale 0.5.
|
||||
expect(solve(1.0, 1.0, 0.5), closeTo(0.5, 1e-9));
|
||||
});
|
||||
|
||||
test('a re-baseline at the SAME instant does not change the scale', () {
|
||||
// Pinch in: start(2.0,1.0) → raw 0.6 gives scale 1.2 (well inside clamp).
|
||||
final before = solve(2.0, 1.0, 0.6);
|
||||
expect(before, closeTo(1.2, 1e-9));
|
||||
|
||||
// A finger blips: the viewer re-baselines RIGHT HERE — scaleStart becomes
|
||||
// the current scale (1.2) and rawScaleAtBaseline becomes the current raw
|
||||
// (0.6). Re-evaluating the same instant must yield the SAME scale (no pop).
|
||||
final after = solve(1.2, 0.6, 0.6);
|
||||
expect(after, closeTo(before, 1e-9),
|
||||
reason: 're-baseline must be continuous, not a jump');
|
||||
});
|
||||
|
||||
test('after a re-baseline the pinch stays smooth (no pop)', () {
|
||||
// Re-baselined at scale 1.2 / raw 0.6. Continue pinching in: raw 0.54.
|
||||
// Correct: 1.2 * (0.54 / 0.6) = 1.08 — a gentle 10% step, monotonic.
|
||||
final next = solve(1.2, 0.6, 0.54);
|
||||
expect(next, closeTo(1.08, 1e-9));
|
||||
|
||||
// The OLD bug multiplied the fresh scaleStart by the un-normalized raw:
|
||||
// 1.2 * 0.54 = 0.648 — a sudden ~46% drop (the flicker). Guard against it.
|
||||
expect(next, greaterThan(1.0),
|
||||
reason: 'must NOT collapse to scaleStart*raw (the old 0.648 pop)');
|
||||
});
|
||||
|
||||
test('result is clamped to [min, max]', () {
|
||||
expect(solve(4.0, 1.0, 4.0), max); // 16 → 8
|
||||
expect(solve(1.0, 1.0, 0.1), min); // 0.1 → 0.5
|
||||
});
|
||||
|
||||
test('degenerate baseline (0) is treated as no cumulative change', () {
|
||||
expect(solve(2.0, 0.0, 5.0), closeTo(2.0, 1e-9));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user