fix(app): Reorder among older items, export and text import that hold up

- Reorder: rows from before an order existed all share one position, and
  one moved among them landed at their end; the run it goes into now
  gets positions of its own (placeAmong), for notes, files, paths,
  steps and sub-studysets. Rows whose deletion can still be undone count
  for nothing, as the list leaves them out, and a dropped row takes its
  place at once instead of snapping back until the order is read again.
- Export: two studysets inside each other (moved so on two devices
  offline) no longer make it run forever; links to files whose names
  hold #, % or brackets work; PDFs, images and sounds go into the zip
  without being compressed again; any error says the export failed.
- Text import: a real CSV reader (quoted fields over several lines, a
  back over several quoted fields, a stray quote spoiling only its own
  line), the separator guessed from the text (semicolons from German
  spreadsheets), and files in UTF-16 or Windows-1252 read as meant.
- A tab whose notes, paths or files couldn't be read says so rather than
  "No … yet"; an Anki import from the Library that fails leaves no empty
  studyset behind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-09 13:02:32 +02:00
commit 084af278b5
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 727 additions and 138 deletions

View file

@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/src/feature/card/model/card_text_import.dart';
@ -61,4 +63,59 @@ void main() {
expect(CardSeparator.forFile('words.tsv'), CardSeparator.tab);
expect(CardSeparator.forFile('words.txt'), CardSeparator.tab);
});
test('a quoted field may run over several lines, and a back over '
'several fields keeps none of their quotes', () {
expect(
parseCardLines(
'"word","line one\nline two"\n"a","b","c"\nnext,one',
CardSeparator.comma,
),
[
(front: 'word', back: 'line one\nline two'),
(front: 'a', back: 'b,c'),
(front: 'next', back: 'one'),
],
);
});
test('a quote that never closes spoils only its own line', () {
expect(
parseCardLines('"hello,hola\nsí,yes', CardSeparator.comma),
[(front: 'sí', back: 'yes')],
);
});
test('the separator is guessed from what the lines hold: semicolons as '
'German spreadsheets write them, whatever the name', () {
expect(
guessSeparator(
'Wort;Bedeutung, mit Komma\nHaus;house',
fileName: 'w.csv',
),
CardSeparator.semicolon,
);
expect(
guessSeparator('hola\thello, hi\nsí\tyes', fileName: 'w.csv'),
CardSeparator.tab,
);
expect(
guessSeparator('nothing here', fileName: 'w.csv'),
CardSeparator.comma,
);
});
test('a file is read in whichever encoding it was saved: UTF-8, UTF-16 '
'with its mark, or Windows-1252', () {
expect(decodeCardText(utf8.encode('Straße;street')), 'Straße;street');
expect(
decodeCardText([0xFF, 0xFE, 0x68, 0, 0x3B, 0, 0x42, 0x30]),
'h;あ',
);
// "Café – €" as Windows-1252.
expect(
decodeCardText([0x43, 0x61, 0x66, 0xE9, 0x20, 0x96, 0x20, 0x80]),
'Café – €',
);
});
}

View file

@ -128,14 +128,46 @@ void main() {
expect(await stored(), [('hello, friend', 'hola amigo')]);
});
testWidgets("a file that isn't UTF-8 text says so", (tester) async {
testWidgets('a file that holds no text says so', (tester) async {
await showApp(tester);
await openImport(tester);
files.next = (name: 'words.csv', bytes: Uint8List.fromList([0xff, 0xfe]));
files.next = (
name: 'words.csv',
bytes: Uint8List.fromList([0x89, 0x50, 0x4E, 0x47, 0x00, 0x00, 0x1A]),
);
await tester.tap(find.text('Load a CSV or TSV file'));
await tester.pumpAndSettle();
expect(find.text("This file isn't text in UTF-8."), findsOneWidget);
expect(find.text("This file doesn't hold text."), findsOneWidget);
});
testWidgets('a file an older spreadsheet saved (Windows-1252, semicolons) '
'is read as it is meant', (tester) async {
await showApp(tester);
await openImport(tester);
// "Café;coffee" as Windows-1252.
files.next = (
name: 'words.csv',
bytes: Uint8List.fromList([
0x43,
0x61,
0x66,
0xE9,
0x3B,
0x63,
0x6F,
0x66,
0x66,
0x65,
0x65,
]),
);
await tester.tap(find.text('Load a CSV or TSV file'));
await tester.pumpAndSettle();
expect(find.widgetWithText(TextField, 'Café;coffee'), findsOneWidget);
expect(find.text('1 card ready to import.'), findsOneWidget);
});
}