mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-11-11 15:08:26 +01:00
Glyph + spoken label replaces every app-written string the guard found: empty states, tile meta, sheet dates/fields, date presets, template/section headings, status footer, snackbars, GitHub and cloud-image results, upload/download badges, Material tooltips. Guard tests (text_free_*_test) walk the painted tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
114 lines
3.5 KiB
Dart
114 lines
3.5 KiB
Dart
/// One attached image as it appears in a note: the picture, or a placeholder
|
|
/// saying why it is not here yet. Tapping opens it full screen.
|
|
library;
|
|
|
|
import 'package:flutter/material.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/app_route.dart';
|
|
import 'package:todo/ui/images/image_viewer_screen.dart';
|
|
import 'package:todo/ui/images/local_image.dart';
|
|
|
|
/// Placeholder text for an image this device does not have yet.
|
|
const kImageNotDownloaded = 'Not downloaded yet';
|
|
|
|
/// Tooltip on the badge of an image dufs has not accepted yet.
|
|
const kImageWaitingToUpload = 'Waiting to upload';
|
|
|
|
/// Renders [ref] from the nearest [ImageStoreScope].
|
|
class ImageEmbed extends StatelessWidget {
|
|
/// Creates an embed; [fit] is `cover` for thumbnails, `contain` inline.
|
|
const ImageEmbed({required this.ref, this.fit = BoxFit.cover, super.key});
|
|
|
|
/// The image to show.
|
|
final AttachmentRef ref;
|
|
|
|
/// How the picture fills the embed's box.
|
|
final BoxFit fit;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final store = ImageStoreScope.of(context);
|
|
final source = store.sourceFor(ref);
|
|
final picture = imageFor(source, fit: fit);
|
|
// Spoken only: nothing paints this string.
|
|
const semanticLabel = 'Attached image';
|
|
return Semantics(
|
|
label: semanticLabel,
|
|
button: source is! MissingImageSource,
|
|
child: GestureDetector(
|
|
onTap: source is MissingImageSource
|
|
? null
|
|
: () => Navigator.of(context).push<void>(
|
|
appRoute(context, (_) => ImageViewerScreen(source: source)),
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: Stack(
|
|
fit: StackFit.passthrough,
|
|
children: [
|
|
picture,
|
|
if (store.isPending(ref))
|
|
const Positioned(
|
|
right: 4,
|
|
top: 4,
|
|
child: Icon(
|
|
Icons.cloud_upload_outlined,
|
|
size: 18,
|
|
semanticLabel: kImageWaitingToUpload,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// The picture for [source], falling back to the not-downloaded placeholder
|
|
/// (also when a network load fails: the wrapper 404s until it has the file).
|
|
Widget imageFor(ImageSource source, {required BoxFit fit}) {
|
|
Widget placeholder(BuildContext context, Object error, StackTrace? stack) =>
|
|
const ImagePlaceholder();
|
|
return switch (source) {
|
|
LocalImageSource(:final path) => localImage(
|
|
path,
|
|
fit: fit,
|
|
errorBuilder: placeholder,
|
|
),
|
|
NetworkImageSource(:final url) => Image.network(
|
|
url,
|
|
fit: fit,
|
|
errorBuilder: placeholder,
|
|
),
|
|
MissingImageSource() => const ImagePlaceholder(),
|
|
};
|
|
}
|
|
|
|
/// Grey box with a download glyph, spoken as [kImageNotDownloaded].
|
|
class ImagePlaceholder extends StatelessWidget {
|
|
/// Creates the placeholder.
|
|
const ImagePlaceholder({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
return ColoredBox(
|
|
color: theme.colorScheme.surfaceContainerHighest,
|
|
child: Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(8),
|
|
child: Icon(
|
|
Icons.cloud_download_outlined,
|
|
color: theme.colorScheme.outline,
|
|
semanticLabel: kImageNotDownloaded,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|