aves_mio0.31/lib/widgets/collection/grid/tile.dart
2026-07-18 13:39:22 +02:00

272 lines
8.4 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:aves/app_mode.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/selection.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/services/intent_service.dart';
import 'package:aves/widgets/collection/grid/list_details.dart';
import 'package:aves/widgets/collection/grid/list_details_theme.dart';
import 'package:aves/widgets/common/grid/scaling.dart';
import 'package:aves/widgets/common/providers/viewer_entry_provider.dart';
import 'package:aves/widgets/common/thumbnail/decorated.dart';
import 'package:aves/widgets/common/thumbnail/notifications.dart';
import 'package:aves/widgets/viewer/hero.dart';
import 'package:aves_model/aves_model.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// ✅ REMOTE
import 'package:aves/remote/remote_origin.dart';
import 'package:aves/remote/remote_image_tile.dart';
class InteractiveTile extends StatelessWidget {
final CollectionLens collection;
final AvesEntry entry;
final double thumbnailExtent;
final TileLayout tileLayout;
final ValueNotifier<bool>? isScrollingNotifier;
const InteractiveTile({
super.key,
required this.collection,
required this.entry,
required this.thumbnailExtent,
required this.tileLayout,
this.isScrollingNotifier,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () {
final appMode = context.read<ValueNotifier<AppMode>>().value;
switch (appMode) {
case AppMode.main:
final selection = context.read<Selection<AvesEntry>>();
if (selection.isSelecting) {
selection.toggleSelection(entry);
} else {
OpenViewerNotification(entry).dispatch(context);
}
case AppMode.pickSingleMediaExternal:
IntentService.submitPickedItems([entry.uri]);
case AppMode.pickMultipleMediaExternal:
final selection = context.read<Selection<AvesEntry>>();
selection.toggleSelection(entry);
case AppMode.pickFilteredMediaInternal:
case AppMode.pickUnfilteredMediaInternal:
Navigator.maybeOf(context)?.pop(entry);
default:
break;
}
},
child: MetaData(
metaData: ScalerMetadata(entry),
child: Tile(
entry: entry,
thumbnailExtent: thumbnailExtent,
tileLayout: tileLayout,
selectable: true,
highlightable: true,
isScrollingNotifier: isScrollingNotifier,
heroTagger: () => EntryHeroInfo(collection, entry).tag,
),
),
);
}
}
class Tile extends StatelessWidget {
final AvesEntry entry;
final double thumbnailExtent;
final TileLayout tileLayout;
final bool selectable, highlightable;
final ValueNotifier<bool>? isScrollingNotifier;
final Object? Function()? heroTagger;
const Tile({
super.key,
required this.entry,
required this.thumbnailExtent,
required this.tileLayout,
this.selectable = false,
this.highlightable = false,
this.isScrollingNotifier,
this.heroTagger,
});
@override
Widget build(BuildContext context) {
switch (tileLayout) {
case TileLayout.mosaic:
case TileLayout.grid:
return _buildThumbnail(context);
case TileLayout.list:
return Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox.square(
dimension: context.select<EntryListDetailsThemeData, double>((v) => v.extent),
child: _buildThumbnail(context),
),
Expanded(
child: EntryListDetails(
entry: entry,
),
),
],
);
}
}
Widget _buildThumbnail(BuildContext context) {
// ✅ REMOTE: usa RemoteImageTile + overlay selezione stile Aves
if (entry.origin == RemoteOrigin.value) {
return _RemoteDecoratedThumbnail(
entry: entry,
tileExtent: thumbnailExtent,
selectable: selectable,
highlightable: highlightable,
);
}
// ✅ LOCALE: comportamento originale (DecoratedThumbnail)
return DecoratedThumbnail(
entry: entry,
tileExtent: thumbnailExtent,
isMosaic: tileLayout == TileLayout.mosaic,
// when the user is scrolling faster than we can retrieve the thumbnails,
// the retrieval task queue can pile up for thumbnails that got disposed
// in this case we pause the image retrieval task to get it out of the queue
cancellableNotifier: isScrollingNotifier,
selectable: selectable,
highlightable: highlightable,
heroTagger: heroTagger,
// do not use a hero placeholder but hide the thumbnail matching the viewer entry,
// so that it can hero out on an entry and come back with a hero to a different entry
heroPlaceholderBuilder: (context, heroSize, child) => child,
imageDecorator: (context, child) {
return Selector<ViewerEntryNotifier, bool>(
selector: (context, v) => v.value == entry,
builder: (context, isViewerEntry, child) {
return Visibility.maintain(
visible: !isViewerEntry,
child: child!,
);
},
child: child,
);
},
);
}
}
/// ✅ Thumbnail per entry remote che replica:
/// - overlay viola + bordo SOLO in selection mode
/// - badge/check in alto a destra in selection mode
/// - nasconde la thumb quando è lentry aperta nel viewer (come DecoratedThumbnail)
class _RemoteDecoratedThumbnail extends StatelessWidget {
final AvesEntry entry;
final double tileExtent;
final bool selectable;
final bool highlightable;
const _RemoteDecoratedThumbnail({
required this.entry,
required this.tileExtent,
required this.selectable,
required this.highlightable,
});
@override
Widget build(BuildContext context) {
// Selezione
final selection = context.watch<Selection<AvesEntry>>();
final isSelecting = selectable && selection.isSelecting;
final isSelected = selectable && selection.selectedItems.contains(entry);
// Colori (violetto simile ad Aves)
final cs = Theme.of(context).colorScheme;
final highlight = cs.secondary.withOpacity(.28);
final border = cs.secondary.withOpacity(.95);
Widget content = RemoteImageTile(
entry: entry,
borderRadius: 0,
fit: BoxFit.cover,
);
// come DecoratedThumbnail: non mostrare la thumb se è lentry nel viewer
content = Selector<ViewerEntryNotifier, bool>(
selector: (context, v) => v.value == entry,
builder: (context, isViewerEntry, child) {
return Visibility.maintain(
visible: !isViewerEntry,
child: child!,
);
},
child: content,
);
return SizedBox(
width: tileExtent,
height: tileExtent,
child: Stack(
fit: StackFit.expand,
children: [
content,
// ✅ FIX: overlay viola + bordo SOLO durante selection mode
if (isSelecting && isSelected) ...[
Positioned.fill(child: ColoredBox(color: highlight)),
Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
border: Border.all(color: border, width: 2),
),
),
),
],
// badge selezione in alto a destra durante selection mode
if (isSelecting)
Positioned(
top: 6,
right: 6,
child: _SelectionBadge(
selected: isSelected,
color: cs.secondary,
),
),
],
),
);
}
}
class _SelectionBadge extends StatelessWidget {
final bool selected;
final Color color;
const _SelectionBadge({
required this.selected,
required this.color,
});
@override
Widget build(BuildContext context) {
final borderColor = Colors.white.withOpacity(.95);
return Container(
width: 20,
height: 20,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: selected ? color : Colors.black.withOpacity(.25),
border: Border.all(
color: selected ? Colors.transparent : borderColor,
width: 1.5,
),
),
child: selected ? const Icon(Icons.check, size: 14, color: Colors.white) : null,
);
}
}