272 lines
8.4 KiB
Dart
272 lines
8.4 KiB
Dart
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 è l’entry 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 è l’entry 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,
|
||
);
|
||
}
|
||
}
|