mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-11-10 19:27:01 +01:00
Some checks failed
📎 now picks any file (Android: Camera / Gallery / File; desktop: an
unfiltered dialog) and the desktop page accepts any dropped file. Photo and
screenshot formats up to 64 MB are still re-encoded and shown inline;
everything else is stored byte for byte on dufs beside the images and linked
as [name](url), shown as a row with its size that opens in the platform's
app (Android) or a new browser tab (desktop). BACKLOG.md rewrites file links
to ~/data/cloud/todo-images/ like image links.
There is no size cap, so no layer holds a file in memory: uploads and
downloads stream through DufsImageClient, writes land in a .tmp renamed when
whole (an interrupted download no longer leaves a truncated file that looks
done), the desktop page POSTs the browser's own Blob with fetch, and Android's
"File" uses file_picker, because file_selector's Android side reads the whole
file into memory on the UI thread. Verified with a 1 GB file end to end on
desktop (peak RSS +36 MB) and on an API 36 emulator.
The wrapper serves attachments from the origin that holds the GitHub token
and the notes, so only script-free types open inline (nosniff), the rest
download; writes carrying another site's Origin are refused (this also
closes cross-site overwrites of the backup files); and attachment routes run
concurrently so a long upload or a playing video cannot stall the page.
Also: cupertino_icons 2.0.0 (unused, but the freshness gate blocks the
pubspec otherwise), open_file 4.0.0 and file_picker 13.1.0 pinned.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
128 lines
4.2 KiB
Dart
128 lines
4.2 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:todo/attachments/image_store_api.dart';
|
|
import 'package:todo/attachments/image_store_scope.dart';
|
|
import 'package:todo/images/attachment_ref.dart';
|
|
import 'package:todo/ui/images/attachment_strip.dart';
|
|
import 'package:todo/ui/images/file_embed.dart';
|
|
import 'package:todo/ui/images/image_embed.dart';
|
|
|
|
import '../fake_image_store.dart';
|
|
|
|
void main() {
|
|
final pdf = AttachmentRef.forFile('n1', 'a.pdf');
|
|
final attachment = Attachment(ref: pdf, name: 'Raport.pdf', isImage: false);
|
|
late FakeImageStore store;
|
|
late List<(ImageSource, String)> opened;
|
|
String? failure;
|
|
|
|
setUp(() {
|
|
store = FakeImageStore();
|
|
opened = [];
|
|
failure = null;
|
|
});
|
|
|
|
Future<void> pump(WidgetTester tester, Widget child) async {
|
|
await tester.pumpWidget(
|
|
ImageStoreScope(
|
|
store: store,
|
|
child: MaterialApp(home: Scaffold(body: child)),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
}
|
|
|
|
FileEmbed embed({bool compact = false}) => FileEmbed(
|
|
attachment: attachment,
|
|
compact: compact,
|
|
opening: (source, name) async {
|
|
opened.add((source, name));
|
|
return failure;
|
|
},
|
|
);
|
|
|
|
testWidgets('a row shows the name and size and opens on tap', (tester) async {
|
|
await pump(tester, embed());
|
|
expect(find.text('Raport.pdf'), findsOneWidget);
|
|
expect(find.text('2.0 KB'), findsOneWidget);
|
|
expect(find.byIcon(Icons.picture_as_pdf_outlined), findsOneWidget);
|
|
await tester.tap(find.text('Raport.pdf'));
|
|
await tester.pump();
|
|
expect(opened.single.$2, 'Raport.pdf');
|
|
expect(opened.single.$1, isA<LocalImageSource>());
|
|
});
|
|
|
|
testWidgets('a failed open is reported', (tester) async {
|
|
failure = 'No app on this phone opens Raport.pdf';
|
|
await pump(tester, embed());
|
|
await tester.tap(find.text('Raport.pdf'));
|
|
await tester.pump();
|
|
expect(find.text(failure!), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('a file not here yet says so and does not open', (tester) async {
|
|
store.missing.add(pdf);
|
|
await pump(tester, embed());
|
|
expect(find.text(kImageNotDownloaded), findsOneWidget);
|
|
await tester.tap(find.text('Raport.pdf'));
|
|
await tester.pump();
|
|
expect(opened, isEmpty);
|
|
});
|
|
|
|
testWidgets('a queued file carries the upload badge, in both shapes', (
|
|
tester,
|
|
) async {
|
|
store.pending.add(pdf);
|
|
await pump(tester, embed());
|
|
expect(find.byTooltip(kImageWaitingToUpload), findsOneWidget);
|
|
await pump(tester, embed(compact: true));
|
|
expect(find.byTooltip(kImageWaitingToUpload), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('the size is asked again when the store changes', (tester) async {
|
|
store.missing.add(pdf);
|
|
await pump(tester, embed());
|
|
expect(find.text(kImageNotDownloaded), findsOneWidget);
|
|
store
|
|
..missing.clear()
|
|
..changed();
|
|
await tester.pump();
|
|
await tester.pump();
|
|
expect(find.text('2.0 KB'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('the strip shows a compact tile for a file', (tester) async {
|
|
final image = AttachmentRef.create('n1');
|
|
await pump(
|
|
tester,
|
|
AttachmentStrip(
|
|
text: '${image.imageMarkdown}\n${pdf.fileMarkdown('Raport.pdf')}',
|
|
),
|
|
);
|
|
expect(find.byType(ImageEmbed), findsOneWidget);
|
|
final tile = tester.widget<FileEmbed>(find.byType(FileEmbed));
|
|
expect(tile.compact, isTrue);
|
|
expect(find.text('2.0 KB'), findsNothing, reason: 'no room for a size');
|
|
});
|
|
|
|
test('formatBytes picks a readable unit', () {
|
|
expect(formatBytes(0), '0 B');
|
|
expect(formatBytes(512), '512 B');
|
|
expect(formatBytes(1536), '1.5 KB');
|
|
expect(formatBytes(10 * 1024), '10 KB');
|
|
expect(formatBytes(34 * 1024 * 1024), '34 MB');
|
|
expect(formatBytes(2254857830), '2.1 GB');
|
|
expect(formatBytes(5000 * 1024 * 1024 * 1024 * 1024), '5000 TB');
|
|
});
|
|
|
|
test('fileIconFor groups types by kind', () {
|
|
expect(fileIconFor('pdf'), Icons.picture_as_pdf_outlined);
|
|
expect(fileIconFor('mkv'), Icons.movie_outlined);
|
|
expect(fileIconFor('flac'), Icons.audiotrack_outlined);
|
|
expect(fileIconFor('md'), Icons.description_outlined);
|
|
expect(fileIconFor('7z'), Icons.folder_zip_outlined);
|
|
expect(fileIconFor('psd'), Icons.image_outlined);
|
|
expect(fileIconFor('bin'), Icons.insert_drive_file_outlined);
|
|
});
|
|
}
|