Fix bugs across app + server, optimize UI/UX, add Gitea CI
Some checks failed
CI / Test (Server, optional) (push) Failing after 2m10s
Windows Build / Build Windows (x64) (push) Failing after 29s
CI / Test (Flutter, Linux) (push) Has been cancelled
CI / Analyze (Flutter) (push) Has been cancelled

Bug fixes (Flutter):
- Wrap multi-statement DB writes (insert/update/delete note, deleteDocument,
  deletePageData, OCR FTS merge, migrations) in transactions to prevent data
  loss on interruption and a read-modify-write FTS race.
- Fix PdfDocument leaks on exception (try/finally dispose) and preserve image
  aspect ratio when stamping images onto PDF pages.
- Guard file-picker against empty selection (was .single -> crash).
- Fix eraser ConcurrentModificationError and unmodifiable-list crash on PDF
  pages; capture page synchronously on save to stop wrong-page data loss.
- Fix Riverpod DB-not-ready races, broken pull-to-refresh, settings load race,
  and search N+1; transform stored annotations on PDF page rotation.
- Normalize pen pressure for devices without a pressure range.
- PPT: single source of truth for slide strokes so ink displays and exports.

UI/UX:
- Material 3 typography, theme-aware colors (dark-mode fixes), hover cursors
  and right-click/visible actions on desktop, keyboard shortcuts (undo/redo/
  save/find), toolbar overflow handling, friendlier empty states, semantic OCR
  status badges, relative timestamps, 1-based page indicators, large-deck PPT
  navigation, and a scratchpad-scope label in split view.

Server (optional backend):
- Persist JWT secret (was per-process random), block path traversal in storage,
  fix CORS '*'+credentials, add OCR job ownership checks, last-writer-wins sync
  guard, constant-time login, and split out heavy OCR deps so the API/tests run
  without them.

CI: Gitea workflows for format+analyze+test (Linux, system sqlite) and a
Windows release build; pristine `flutter analyze`, all Flutter and server tests
green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-21 03:18:00 +08:00
commit 72428dc075
210 changed files with 18171 additions and 0 deletions

View File

@@ -0,0 +1,335 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'document.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
Document _$DocumentFromJson(Map<String, dynamic> json) {
return _Document.fromJson(json);
}
/// @nodoc
mixin _$Document {
String get id => throw _privateConstructorUsedError;
String get filename => throw _privateConstructorUsedError;
String get docType => throw _privateConstructorUsedError;
String get filePath => throw _privateConstructorUsedError;
int get pageCount => throw _privateConstructorUsedError;
int get rotation => throw _privateConstructorUsedError;
DateTime get createdAt => throw _privateConstructorUsedError;
DateTime get updatedAt => throw _privateConstructorUsedError;
/// Serializes this Document to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of Document
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$DocumentCopyWith<Document> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $DocumentCopyWith<$Res> {
factory $DocumentCopyWith(Document value, $Res Function(Document) then) =
_$DocumentCopyWithImpl<$Res, Document>;
@useResult
$Res call({
String id,
String filename,
String docType,
String filePath,
int pageCount,
int rotation,
DateTime createdAt,
DateTime updatedAt,
});
}
/// @nodoc
class _$DocumentCopyWithImpl<$Res, $Val extends Document>
implements $DocumentCopyWith<$Res> {
_$DocumentCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of Document
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? filename = null,
Object? docType = null,
Object? filePath = null,
Object? pageCount = null,
Object? rotation = null,
Object? createdAt = null,
Object? updatedAt = null,
}) {
return _then(
_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
filename: null == filename
? _value.filename
: filename // ignore: cast_nullable_to_non_nullable
as String,
docType: null == docType
? _value.docType
: docType // ignore: cast_nullable_to_non_nullable
as String,
filePath: null == filePath
? _value.filePath
: filePath // ignore: cast_nullable_to_non_nullable
as String,
pageCount: null == pageCount
? _value.pageCount
: pageCount // ignore: cast_nullable_to_non_nullable
as int,
rotation: null == rotation
? _value.rotation
: rotation // ignore: cast_nullable_to_non_nullable
as int,
createdAt: null == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,
updatedAt: null == updatedAt
? _value.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$DocumentImplCopyWith<$Res>
implements $DocumentCopyWith<$Res> {
factory _$$DocumentImplCopyWith(
_$DocumentImpl value,
$Res Function(_$DocumentImpl) then,
) = __$$DocumentImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
String id,
String filename,
String docType,
String filePath,
int pageCount,
int rotation,
DateTime createdAt,
DateTime updatedAt,
});
}
/// @nodoc
class __$$DocumentImplCopyWithImpl<$Res>
extends _$DocumentCopyWithImpl<$Res, _$DocumentImpl>
implements _$$DocumentImplCopyWith<$Res> {
__$$DocumentImplCopyWithImpl(
_$DocumentImpl _value,
$Res Function(_$DocumentImpl) _then,
) : super(_value, _then);
/// Create a copy of Document
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? filename = null,
Object? docType = null,
Object? filePath = null,
Object? pageCount = null,
Object? rotation = null,
Object? createdAt = null,
Object? updatedAt = null,
}) {
return _then(
_$DocumentImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
filename: null == filename
? _value.filename
: filename // ignore: cast_nullable_to_non_nullable
as String,
docType: null == docType
? _value.docType
: docType // ignore: cast_nullable_to_non_nullable
as String,
filePath: null == filePath
? _value.filePath
: filePath // ignore: cast_nullable_to_non_nullable
as String,
pageCount: null == pageCount
? _value.pageCount
: pageCount // ignore: cast_nullable_to_non_nullable
as int,
rotation: null == rotation
? _value.rotation
: rotation // ignore: cast_nullable_to_non_nullable
as int,
createdAt: null == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,
updatedAt: null == updatedAt
? _value.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,
),
);
}
}
/// @nodoc
@JsonSerializable()
class _$DocumentImpl implements _Document {
const _$DocumentImpl({
required this.id,
required this.filename,
required this.docType,
required this.filePath,
this.pageCount = 0,
this.rotation = 0,
required this.createdAt,
required this.updatedAt,
});
factory _$DocumentImpl.fromJson(Map<String, dynamic> json) =>
_$$DocumentImplFromJson(json);
@override
final String id;
@override
final String filename;
@override
final String docType;
@override
final String filePath;
@override
@JsonKey()
final int pageCount;
@override
@JsonKey()
final int rotation;
@override
final DateTime createdAt;
@override
final DateTime updatedAt;
@override
String toString() {
return 'Document(id: $id, filename: $filename, docType: $docType, filePath: $filePath, pageCount: $pageCount, rotation: $rotation, createdAt: $createdAt, updatedAt: $updatedAt)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$DocumentImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.filename, filename) ||
other.filename == filename) &&
(identical(other.docType, docType) || other.docType == docType) &&
(identical(other.filePath, filePath) ||
other.filePath == filePath) &&
(identical(other.pageCount, pageCount) ||
other.pageCount == pageCount) &&
(identical(other.rotation, rotation) ||
other.rotation == rotation) &&
(identical(other.createdAt, createdAt) ||
other.createdAt == createdAt) &&
(identical(other.updatedAt, updatedAt) ||
other.updatedAt == updatedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
id,
filename,
docType,
filePath,
pageCount,
rotation,
createdAt,
updatedAt,
);
/// Create a copy of Document
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$DocumentImplCopyWith<_$DocumentImpl> get copyWith =>
__$$DocumentImplCopyWithImpl<_$DocumentImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$DocumentImplToJson(this);
}
}
abstract class _Document implements Document {
const factory _Document({
required final String id,
required final String filename,
required final String docType,
required final String filePath,
final int pageCount,
final int rotation,
required final DateTime createdAt,
required final DateTime updatedAt,
}) = _$DocumentImpl;
factory _Document.fromJson(Map<String, dynamic> json) =
_$DocumentImpl.fromJson;
@override
String get id;
@override
String get filename;
@override
String get docType;
@override
String get filePath;
@override
int get pageCount;
@override
int get rotation;
@override
DateTime get createdAt;
@override
DateTime get updatedAt;
/// Create a copy of Document
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$DocumentImplCopyWith<_$DocumentImpl> get copyWith =>
throw _privateConstructorUsedError;
}