Skip to content

Commit c882e3f

Browse files
committed
Fix encrypted note imports and gesture scaling, closes #1242
1 parent 8712a49 commit c882e3f

9 files changed

Lines changed: 210 additions & 70 deletions

File tree

app/lib/api/connection_encryption.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ NoteFile addConnectionPasswordToNoteFile(
9191
NoteFile file, {
9292
String? password,
9393
}) {
94+
if (file.isEncrypted()) return file;
9495
if (storage is! RemoteStorage || !storage.isConnectionEncryptionEnabled) {
9596
return file;
9697
}

app/lib/api/open.dart

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import 'dart:async';
2-
import 'dart:typed_data';
2+
import 'dart:io';
33

44
import 'package:butterfly/main.dart';
55
import 'package:butterfly/services/logger.dart';
66
import 'package:butterfly_api/butterfly_api.dart';
77
import 'package:file_picker/file_picker.dart';
8+
import 'package:flutter/foundation.dart';
89
import 'package:flutter/material.dart';
910
import 'package:go_router/go_router.dart';
1011
import 'package:lw_file_system/lw_file_system.dart';
@@ -73,15 +74,27 @@ Future<(Uint8List, String, String)> _readPlatformFile(PlatformFile file) async {
7374
return (data, fileName.split('.').lastOrNull ?? '', nameWithoutExtension);
7475
}
7576

77+
(FileType, List<String>?) _getImportPickerOptions(Iterable<String> extensions) {
78+
// Android's system picker filters by MIME type, not extension. The picker
79+
// plugin drops unknown extensions such as bfly and tbfly while converting
80+
// them to MIME types, making Butterfly files unselectable when known file
81+
// types are requested alongside them.
82+
if (!kIsWeb && Platform.isAndroid) {
83+
return (FileType.any, null);
84+
}
85+
return (FileType.custom, extensions.toList());
86+
}
87+
7688
Future<(Uint8List?, String?, String?)> importFile(
7789
BuildContext context, [
7890
List<AssetFileType>? types,
7991
]) async {
92+
final (fileType, allowedExtensions) = _getImportPickerOptions(
93+
(types ?? AssetFileType.values).expand((e) => e.getFileExtensions()),
94+
);
8095
final file = await FilePicker.pickFile(
81-
allowedExtensions: (types ?? AssetFileType.values)
82-
.expand((e) => e.getFileExtensions())
83-
.toList(),
84-
type: FileType.custom,
96+
allowedExtensions: allowedExtensions,
97+
type: fileType,
8598
);
8699
if (file == null) {
87100
return (null, null, null);
@@ -93,11 +106,12 @@ Future<List<(Uint8List, String, String)>> importFiles(
93106
BuildContext context, [
94107
List<AssetFileType>? types,
95108
]) async {
109+
final (fileType, allowedExtensions) = _getImportPickerOptions(
110+
(types ?? AssetFileType.values).expand((e) => e.getFileExtensions()),
111+
);
96112
final result = await FilePicker.pickFiles(
97-
allowedExtensions: (types ?? AssetFileType.values)
98-
.expand((e) => e.getFileExtensions())
99-
.toList(),
100-
type: FileType.custom,
113+
allowedExtensions: allowedExtensions,
114+
type: fileType,
101115
);
102116
final files = <(Uint8List, String, String)>[];
103117
for (final file in result) {
@@ -109,9 +123,10 @@ Future<List<(Uint8List, String, String)>> importFiles(
109123
Future<List<(Uint8List, String, String)>> importFilesWithExtensions(
110124
List<String> extensions,
111125
) async {
126+
final (fileType, allowedExtensions) = _getImportPickerOptions(extensions);
112127
final result = await FilePicker.pickFiles(
113-
allowedExtensions: extensions,
114-
type: FileType.custom,
128+
allowedExtensions: allowedExtensions,
129+
type: fileType,
115130
);
116131
final files = <(Uint8List, String, String)>[];
117132
for (final file in result) {

app/lib/services/import.dart

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ enum _OneNoteManualXpsAction { selectPdf, exportAgain, skipFile, skipAll }
4545
class ImportResult {
4646
final ImportService service;
4747
final NoteData? document;
48+
final NoteFile? file;
4849
final List<PadElement> elements;
4950
final Map<String, Uint8List> assets;
5051
final List<(String?, DocumentPage)> pages;
@@ -57,6 +58,7 @@ class ImportResult {
5758
ImportResult({
5859
required this.service,
5960
required this.document,
61+
this.file,
6062
this.elements = const [],
6163
this.assets = const {},
6264
this.pages = const [],
@@ -66,7 +68,8 @@ class ImportResult {
6668
this.choosePosition = false,
6769
}) : documentReady = false;
6870
ImportResult.ready({required this.service, required NoteData this.document})
69-
: elements = const [],
71+
: file = null,
72+
elements = const [],
7073
assets = document.getAllAssets(),
7174
pages = document
7275
.getPages(true)
@@ -86,6 +89,8 @@ class ImportResult {
8689
exportPresets = const [],
8790
choosePosition = false,
8891
documentReady = true;
92+
ImportResult.file({required ImportService service, required NoteFile file})
93+
: this(service: service, document: null, file: file);
8994

9095
bool _isArchiveAssetPath(String path) =>
9196
validAssetPaths.any((e) => path.startsWith('$e/'));
@@ -152,6 +157,8 @@ class ImportResult {
152157
return document;
153158
}
154159

160+
Future<NoteFile> exportFile() async => file ?? (await export()).toFile();
161+
155162
void submit({bool? choosePosition}) {
156163
choosePosition ??= this.choosePosition;
157164
final state = service._getState();
@@ -240,6 +247,7 @@ class ImportService {
240247
String type = '',
241248
Object? data,
242249
NoteData? document,
250+
bool preserveEncrypted = false,
243251
}) async {
244252
final location = bloc?.editorController.saveCubit.state.location;
245253
Uint8List? bytes;
@@ -279,7 +287,13 @@ class ImportService {
279287
return null;
280288
}
281289
if (bytes == null) return null;
282-
return import(fileType, bytes, document: document, advanced: false);
290+
return import(
291+
fileType,
292+
bytes,
293+
document: document,
294+
advanced: false,
295+
preserveEncrypted: preserveEncrypted,
296+
);
283297
}
284298

285299
@useResult
@@ -293,6 +307,7 @@ class ImportService {
293307
TemplateFileSystem? templateSystem,
294308
PackFileSystem? packSystem,
295309
String? name,
310+
bool preserveEncrypted = false,
296311
}) async {
297312
final realDocument =
298313
document ?? bloc?.state.data ?? DocumentDefaults.createDocument();
@@ -305,6 +320,7 @@ class ImportService {
305320
templateSystem: templateSystem,
306321
packSystem: packSystem,
307322
name: name,
323+
preserveEncrypted: preserveEncrypted,
308324
),
309325
AssetFileType.image => importImage(
310326
bytes,
@@ -397,10 +413,14 @@ class ImportService {
397413
TemplateFileSystem? templateSystem,
398414
PackFileSystem? packSystem,
399415
String? name,
416+
bool preserveEncrypted = false,
400417
}) async {
401418
try {
402419
final file = NoteFile(bytes);
403420
final encrypted = file.isEncrypted();
421+
if (encrypted && preserveEncrypted) {
422+
return ImportResult.file(service: this, file: file);
423+
}
404424
var password = readConnectionEncryptionPassword(effectiveStorage);
405425
NoteData? data;
406426
if (encrypted) {
@@ -1272,7 +1292,7 @@ class ImportService {
12721292
fileSystem.storage,
12731293
document,
12741294
);
1275-
fileSystem.createFile(
1295+
await fileSystem.createFile(
12761296
p.join(path ?? '', document.name ?? ''),
12771297
document.toFile(),
12781298
);
@@ -1290,22 +1310,16 @@ class ImportService {
12901310
fileSystem ??= getDocumentSystem();
12911311
for (final file in archive) {
12921312
const fileExtension = '.bfly';
1293-
if (!file.name.endsWith(fileExtension)) continue;
1313+
if (!file.name.toLowerCase().endsWith(fileExtension)) continue;
12941314
final bytes = file.readBytes();
12951315
if (bytes == null) continue;
1296-
var document = await (await importBfly(
1316+
final noteFile = await (await importBfly(
12971317
bytes,
12981318
advanced: false,
1299-
))?.export();
1300-
if (document != null) {
1301-
document = addConnectionPasswordToNoteData(
1302-
fileSystem.storage,
1303-
document,
1304-
);
1305-
fileSystem.createFile(
1306-
p.join(path ?? '', file.name),
1307-
document.toFile(),
1308-
);
1319+
preserveEncrypted: true,
1320+
))?.exportFile();
1321+
if (noteFile != null) {
1322+
await fileSystem.createFile(p.join(path ?? '', file.name), noteFile);
13091323
}
13101324
}
13111325
return true;

app/lib/views/files/view.dart

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -700,18 +700,15 @@ class FilesViewState extends State<FilesView> {
700700
DocumentDefaults.createDocument(
701701
name: nameWithoutExtension,
702702
),
703+
preserveEncrypted: true,
703704
);
704705
if (importResult == null) {
705706
continue;
706707
}
707-
var document = await importResult.export();
708-
document = addConnectionPasswordToNoteData(
709-
_remote,
710-
document,
711-
);
708+
final noteFile = await importResult.exportFile();
712709
setNativeData(result, fileExtension);
713710

714-
var docName = document.getMetadata()?.name;
711+
var docName = noteFile.display()?.name;
715712
if (docName == null || docName.trim().isEmpty) {
716713
docName = nameWithoutExtension;
717714
}
@@ -726,7 +723,7 @@ class FilesViewState extends State<FilesView> {
726723
directory: _locationController.text,
727724
name: docName,
728725
suffix: '.bfly',
729-
document.toFile(),
726+
noteFile,
730727
);
731728

732729
if (files.length == 1 && context.mounted) {

app/lib/views/view/input.dart

Lines changed: 13 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ class _ViewportInputCoordinator {
77
final Map<int, PointerDeviceKind> _pointerKinds = {};
88
final PointerShortcutManager _shortcutManager = PointerShortcutManager();
99

10-
Offset _gestureFocalPoint = Offset.zero;
1110
double _gestureScale = 1;
1211
double _gestureRotation = 0;
1312
int _gesturePointerCount = 0;
@@ -121,22 +120,7 @@ class _ViewportInputCoordinator {
121120
ruler.transformWithPointerMove(input.getEventContext(), event);
122121
return;
123122
}
124-
final inputState = cubit.inputCubit.state;
125123
if (_isTouchMoveGesture(cubit)) {
126-
if (inputState.pointers.isEmpty) return;
127-
if (event.pointer == inputState.pointers.first) {
128-
final transform = cubit.transformCubit.state;
129-
cubit.transformCubit.moveConstrained(
130-
transform.localToGlobalDelta(-event.delta),
131-
runtime: cubit,
132-
bloc: input.bloc,
133-
currentArea: input.state.currentArea,
134-
);
135-
_delayBakeUnlessSmooth(
136-
input.context.read<SettingsCubit>().state,
137-
input.delayBake,
138-
);
139-
}
140124
return;
141125
}
142126
if (_isHandlerGesture) {
@@ -223,7 +207,6 @@ class _ViewportInputCoordinator {
223207

224208
void _rebaseScaleGesture(ScaleUpdateDetails details) {
225209
_gesturePointerCount = details.pointerCount;
226-
_gestureFocalPoint = details.localFocalPoint;
227210
_gestureScale = details.scale;
228211
_gestureRotation = details.rotation;
229212
}
@@ -259,7 +242,6 @@ class _ViewportInputCoordinator {
259242
handler.onScaleStartAbort(details, eventContext);
260243
}
261244

262-
_gestureFocalPoint = details.localFocalPoint;
263245
_gestureScale = 1;
264246
_gestureRotation = 0;
265247
_gesturePointerCount = details.pointerCount;
@@ -299,6 +281,13 @@ class _ViewportInputCoordinator {
299281
final sensitivity = settings.gestureSensitivity;
300282
final rotationDelta = details.rotation - _gestureRotation;
301283
_gestureRotation = details.rotation;
284+
cubit.transformCubit.moveConstrained(
285+
cubit.transformCubit.state.localToGlobalDelta(-details.focalPointDelta) /
286+
sensitivity,
287+
runtime: cubit,
288+
bloc: input.bloc,
289+
currentArea: input.state.currentArea,
290+
);
302291
if (settings.rotateOnGesture) {
303292
cubit.transformCubit.rotateConstrained(
304293
rotationDelta / sensitivity,
@@ -307,24 +296,12 @@ class _ViewportInputCoordinator {
307296
);
308297
}
309298

310-
if (details.scale == 1) {
311-
cubit.transformCubit.moveConstrained(
312-
cubit.transformCubit.state.localToGlobalDelta(
313-
-details.focalPointDelta,
314-
) /
315-
sensitivity,
316-
runtime: cubit,
317-
bloc: input.bloc,
318-
currentArea: input.state.currentArea,
319-
);
320-
} else {
321-
final scaleDelta = details.scale - _gestureScale;
322-
cubit.transformCubit.zoomConstrained(
323-
scaleDelta / sensitivity + 1,
324-
cursor: _gestureFocalPoint,
325-
runtime: cubit,
326-
);
327-
}
299+
final scaleDelta = details.scale / _gestureScale;
300+
cubit.transformCubit.zoomConstrained(
301+
(scaleDelta - 1) / sensitivity + 1,
302+
cursor: details.localFocalPoint,
303+
runtime: cubit,
304+
);
328305
_gestureScale = details.scale;
329306
_delayBakeUnlessSmooth(settings, input.delayBake);
330307
}

app/test/api/connection_encryption_test.dart

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ void main() {
4848
});
4949

5050
test(
51-
'connection password is added on create but not reapplied on update',
51+
'connection password is added only to plaintext files on create',
5252
() async {
5353
final fileSystem = MockTypedDirectoryFileSystem<NoteFile>(
5454
onCreate: (file) =>
@@ -76,6 +76,13 @@ void main() {
7676
isFalse,
7777
);
7878
expect(updated.data?.load()?.isValid, isTrue);
79+
80+
final imported = document.changePassword('document password').toFile();
81+
final preserved = await fileSystem.createFile('imported.bfly', imported);
82+
expect(
83+
preserved.data?.load(password: 'document password')?.isValid,
84+
isTrue,
85+
);
7986
},
8087
);
8188
}

0 commit comments

Comments
 (0)