#407 quick copy/move

This commit is contained in:
Thibault Deckers 2022-11-30 12:51:31 +01:00
parent 5a6153b970
commit f57e2306e2
33 changed files with 626 additions and 115 deletions

View file

@ -7,7 +7,7 @@ All notable changes to this project will be documented in this file.
### Added
- Viewer: optionally show rating & tags on overlay
- Viewer: long press on rating quick action for quicker rating
- Viewer: long press on copy/move/rating quick action for quicker action
- Search: missing address, portrait, landscape filters
- Lithuanian translation (thanks Gediminas Murauskas)

View file

@ -29,6 +29,8 @@ class Settings extends ChangeNotifier {
Settings._private();
static const int moveDestinationAlbumMax = 3;
static const Set<String> _internalKeys = {
hasAcceptedTermsKey,
catalogTimeZoneKey,
@ -37,6 +39,7 @@ class Settings extends ChangeNotifier {
platformAccelerometerRotationKey,
platformTransitionAnimationScaleKey,
topEntryIdsKey,
moveDestinationAlbumsKey,
};
static const _widgetKeyPrefix = 'widget_';
@ -51,6 +54,7 @@ class Settings extends ChangeNotifier {
static const tileLayoutPrefixKey = 'tile_layout_';
static const entryRenamingPatternKey = 'entry_renaming_pattern';
static const topEntryIdsKey = 'top_entry_ids';
static const moveDestinationAlbumsKey = 'move_destination_albums';
// display
static const displayRefreshRateModeKey = 'display_refresh_rate_mode';
@ -314,6 +318,10 @@ class Settings extends ChangeNotifier {
set topEntryIds(List<int>? newValue) => setAndNotify(topEntryIdsKey, newValue?.map((id) => id.toString()).whereNotNull().toList());
List<String> get moveDestinationAlbums => getStringList(moveDestinationAlbumsKey) ?? [];
set moveDestinationAlbums(List<String> newValue) => setAndNotify(moveDestinationAlbumsKey, newValue.take(Settings.moveDestinationAlbumMax).toList());
// display
DisplayRefreshRateMode get displayRefreshRateMode => getEnumOrDefault(displayRefreshRateModeKey, SettingsDefaults.displayRefreshRateMode, DisplayRefreshRateMode.values);

View file

@ -317,7 +317,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
Future<void> _move(BuildContext context, {required MoveType moveType}) async {
final entries = _getTargetItems(context);
await move(context, moveType: moveType, entries: entries);
await doMove(context, moveType: moveType, entries: entries);
_leaveSelectionMode(context);
}

View file

@ -34,50 +34,20 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
Future<void> move(
Future<void> doQuickMove(
BuildContext context, {
required MoveType moveType,
required Set<AvesEntry> entries,
required Map<String, Iterable<AvesEntry>> entriesByDestination,
bool hideShowAction = false,
VoidCallback? onSuccess,
}) async {
final entries = entriesByDestination.values.expand((v) => v).toSet();
final todoCount = entries.length;
assert(todoCount > 0);
final toBin = moveType == MoveType.toBin;
final copy = moveType == MoveType.copy;
final l10n = context.l10n;
if (toBin) {
if (!await showConfirmationDialog(
context: context,
type: ConfirmationDialog.moveToBin,
message: l10n.binEntriesConfirmationDialogMessage(todoCount),
confirmationButtonLabel: l10n.deleteButtonLabel,
)) return;
}
final entriesByDestination = <String, Set<AvesEntry>>{};
switch (moveType) {
case MoveType.copy:
case MoveType.move:
case MoveType.export:
final destinationAlbum = await pickAlbum(context: context, moveType: moveType);
if (destinationAlbum == null) return;
entriesByDestination[destinationAlbum] = entries;
break;
case MoveType.toBin:
entriesByDestination[AndroidFileUtils.trashDirPath] = entries;
break;
case MoveType.fromBin:
groupBy<AvesEntry, String?>(entries, (e) => e.directory).forEach((originAlbum, dirEntries) {
if (originAlbum != null) {
entriesByDestination[originAlbum] = dirEntries.toSet();
}
});
break;
}
// permission for modification at destinations
final destinationAlbums = entriesByDestination.keys.toSet();
if (!await checkStoragePermissionForAlbums(context, destinationAlbums)) return;
@ -90,6 +60,7 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
if (!await checkFreeSpaceForMove(context, entries, destinationAlbum, moveType)) return;
});
final l10n = context.l10n;
var nameConflictStrategy = NameConflictStrategy.rename;
if (!toBin && destinationAlbums.length == 1) {
final destinationDirectory = Directory(destinationAlbums.single);
@ -174,7 +145,7 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
// local context may be deactivated when action is triggered after navigation
final context = AvesApp.navigatorKey.currentContext;
if (context != null) {
move(
doMove(
context,
moveType: MoveType.fromBin,
entries: movedEntries,
@ -213,6 +184,55 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
);
}
Future<void> doMove(
BuildContext context, {
required MoveType moveType,
required Set<AvesEntry> entries,
bool hideShowAction = false,
VoidCallback? onSuccess,
}) async {
if (moveType == MoveType.toBin) {
final l10n = context.l10n;
if (!await showConfirmationDialog(
context: context,
type: ConfirmationDialog.moveToBin,
message: l10n.binEntriesConfirmationDialogMessage(entries.length),
confirmationButtonLabel: l10n.deleteButtonLabel,
)) return;
}
final entriesByDestination = <String, Set<AvesEntry>>{};
switch (moveType) {
case MoveType.copy:
case MoveType.move:
case MoveType.export:
final destinationAlbum = await pickAlbum(context: context, moveType: moveType);
if (destinationAlbum == null) return;
settings.moveDestinationAlbums = settings.moveDestinationAlbums
..remove(destinationAlbum)
..insert(0, destinationAlbum);
entriesByDestination[destinationAlbum] = entries;
break;
case MoveType.toBin:
entriesByDestination[AndroidFileUtils.trashDirPath] = entries;
break;
case MoveType.fromBin:
groupBy<AvesEntry, String?>(entries, (e) => e.directory).forEach((originAlbum, dirEntries) {
if (originAlbum != null) {
entriesByDestination[originAlbum] = dirEntries.toSet();
}
});
break;
}
await doQuickMove(
context,
moveType: moveType,
entriesByDestination: entriesByDestination,
);
}
Future<void> rename(
BuildContext context, {
required Map<AvesEntry, String> entriesToNewName,

View file

@ -0,0 +1,67 @@
import 'package:aves/model/actions/entry_actions.dart';
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/widgets/common/app_bar/quick_choosers/album_chooser.dart';
import 'package:aves/widgets/common/app_bar/quick_choosers/chooser_button.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/filter_grids/common/filter_nav_page.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class MoveButton extends ChooserQuickButton<String> {
final bool copy;
const MoveButton({
super.key,
required this.copy,
super.chooserPosition,
super.onChooserValue,
required super.onPressed,
});
@override
State<MoveButton> createState() => _MoveQuickButtonState();
}
class _MoveQuickButtonState extends ChooserQuickButtonState<MoveButton, String> {
EntryAction get action => widget.copy ? EntryAction.copy : EntryAction.move;
@override
Widget get icon => action.getIcon();
@override
String get tooltip => action.getText(context);
@override
String? get defaultValue => null;
@override
Widget buildChooser(Animation<double> animation) {
final options = settings.moveDestinationAlbums;
final takeCount = Settings.moveDestinationAlbumMax - options.length;
if (takeCount > 0) {
final source = context.read<CollectionSource>();
final filters = source.rawAlbums.whereNot(options.contains).map((album) => AlbumFilter(album, null)).toSet();
final allMapEntries = filters.map((filter) => FilterGridItem(filter, source.recentEntry(filter))).toList();
allMapEntries.sort(FilterNavigationPage.compareFiltersByDate);
options.addAll(allMapEntries.take(takeCount).map((v) => v.filter.album));
}
return MediaQueryDataProvider(
child: FadeTransition(
opacity: animation,
child: ScaleTransition(
scale: animation,
child: AlbumQuickChooser(
valueNotifier: chooserValueNotifier,
pointerGlobalPosition: pointerGlobalPosition,
options: widget.chooserPosition == PopupMenuPosition.over ? options.reversed.toList() : options,
),
),
),
);
}
}

View file

@ -0,0 +1,35 @@
import 'dart:async';
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/widgets/common/app_bar/quick_choosers/filter_chooser.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class AlbumQuickChooser extends StatelessWidget {
final ValueNotifier<String?> valueNotifier;
final List<String> options;
final Stream<Offset> pointerGlobalPosition;
const AlbumQuickChooser({
super.key,
required this.valueNotifier,
required this.options,
required this.pointerGlobalPosition,
});
@override
Widget build(BuildContext context) {
final source = context.read<CollectionSource>();
return FilterQuickChooser<String>(
valueNotifier: valueNotifier,
options: options,
pointerGlobalPosition: pointerGlobalPosition,
buildFilterChip: (context, album) => AvesFilterChip(
filter: AlbumFilter(album, source.getAlbumDisplayName(context, album)),
showGenericIcon: false,
),
);
}
}

View file

@ -39,7 +39,7 @@ abstract class ChooserQuickButtonState<T extends ChooserQuickButton<U>, U> exten
ValueNotifier<U?> get chooserValueNotifier => _chooserValueNotifier;
Stream<LongPressMoveUpdateDetails> get moveUpdates => _moveUpdateStreamController.stream;
Stream<Offset> get pointerGlobalPosition => _moveUpdateStreamController.stream.map((event) => event.globalPosition);
@override
void dispose() {

View file

@ -0,0 +1,146 @@
import 'dart:async';
import 'package:aves/widgets/dialogs/aves_dialog.dart';
import 'package:aves_ui/aves_ui.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
class FilterQuickChooser<T> extends StatefulWidget {
final ValueNotifier<T?> valueNotifier;
final List<T> options;
final Stream<Offset> pointerGlobalPosition;
final Widget Function(BuildContext context, T album) buildFilterChip;
const FilterQuickChooser({
super.key,
required this.valueNotifier,
required this.options,
required this.pointerGlobalPosition,
required this.buildFilterChip,
});
@override
State<FilterQuickChooser<T>> createState() => _FilterQuickChooserState<T>();
}
class _FilterQuickChooserState<T> extends State<FilterQuickChooser<T>> {
final List<StreamSubscription> _subscriptions = [];
final ValueNotifier<Rect> _selectedRowRect = ValueNotifier(Rect.zero);
ValueNotifier<T?> get valueNotifier => widget.valueNotifier;
List<T> get options => widget.options;
static const margin = EdgeInsets.all(8);
static const padding = EdgeInsets.all(8);
static const double intraPadding = 8;
@override
void initState() {
super.initState();
_registerWidget(widget);
}
@override
void didUpdateWidget(covariant FilterQuickChooser<T> oldWidget) {
super.didUpdateWidget(oldWidget);
_unregisterWidget(oldWidget);
_registerWidget(widget);
}
@override
void dispose() {
_unregisterWidget(widget);
super.dispose();
}
void _registerWidget(FilterQuickChooser<T> widget) {
_subscriptions.add(widget.pointerGlobalPosition.listen(_onPointerMove));
}
void _unregisterWidget(FilterQuickChooser<T> widget) {
_subscriptions
..forEach((sub) => sub.cancel())
..clear();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: margin,
child: Material(
shape: AvesDialog.shape(context),
child: Padding(
padding: padding,
child: ValueListenableBuilder<T?>(
valueListenable: valueNotifier,
builder: (context, selectedValue, child) {
return Stack(
children: [
ValueListenableBuilder<Rect>(
valueListenable: _selectedRowRect,
builder: (context, selectedRowRect, child) {
Widget child = const Center(child: AvesDot());
child = AnimatedOpacity(
opacity: selectedValue != null ? 1 : 0,
curve: Curves.easeInOutCubic,
duration: const Duration(milliseconds: 200),
child: child,
);
child = AnimatedPositioned(
top: selectedRowRect.top,
height: selectedRowRect.height,
curve: Curves.easeInOutCubic,
duration: const Duration(milliseconds: 200),
child: child,
);
return child;
},
),
Padding(
padding: const EdgeInsetsDirectional.only(start: 24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: options.mapIndexed((index, value) {
return Padding(
padding: index == 0 ? EdgeInsets.zero : const EdgeInsets.only(top: intraPadding),
child: widget.buildFilterChip(context, value),
);
}).toList(),
),
),
],
);
},
),
),
),
);
}
void _onPointerMove(Offset globalPosition) {
final chooserBox = context.findRenderObject() as RenderBox;
final chooserSize = chooserBox.size;
final contentWidth = chooserSize.width;
final contentHeight = chooserSize.height - (margin.vertical + padding.vertical);
final optionCount = options.length;
final itemHeight = (contentHeight - (optionCount - 1) * intraPadding) / optionCount;
final local = chooserBox.globalToLocal(globalPosition);
final dx = local.dx;
final dy = local.dy - (margin.vertical + padding.vertical) / 2;
T? selectedValue;
if (0 < dx && dx < contentWidth && 0 < dy && dy < contentHeight) {
final index = (options.length * dy / contentHeight).floor();
if (0 <= index && index < options.length) {
selectedValue = options[index];
final top = index * (itemHeight + intraPadding);
_selectedRowRect.value = Rect.fromLTWH(0, top, contentWidth, itemHeight);
}
}
valueNotifier.value = selectedValue;
}
}

View file

@ -5,13 +5,13 @@ import 'package:aves/widgets/dialogs/aves_dialog.dart';
import 'package:flutter/material.dart';
class RateQuickChooser extends StatefulWidget {
final ValueNotifier<int?> ratingNotifier;
final Stream<LongPressMoveUpdateDetails> moveUpdates;
final ValueNotifier<int?> valueNotifier;
final Stream<Offset> pointerGlobalPosition;
const RateQuickChooser({
super.key,
required this.ratingNotifier,
required this.moveUpdates,
required this.valueNotifier,
required this.pointerGlobalPosition,
});
@override
@ -21,6 +21,11 @@ class RateQuickChooser extends StatefulWidget {
class _RateQuickChooserState extends State<RateQuickChooser> {
final List<StreamSubscription> _subscriptions = [];
ValueNotifier<int?> get valueNotifier => widget.valueNotifier;
static const margin = EdgeInsets.all(8);
static const padding = EdgeInsets.all(8);
@override
void initState() {
super.initState();
@ -41,7 +46,7 @@ class _RateQuickChooserState extends State<RateQuickChooser> {
}
void _registerWidget(RateQuickChooser widget) {
_subscriptions.add(widget.moveUpdates.map((event) => event.globalPosition).listen(_onPointerMove));
_subscriptions.add(widget.pointerGlobalPosition.listen(_onPointerMove));
}
void _unregisterWidget(RateQuickChooser widget) {
@ -53,19 +58,18 @@ class _RateQuickChooserState extends State<RateQuickChooser> {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8),
padding: margin,
child: Material(
shape: AvesDialog.shape(context),
child: Padding(
padding: const EdgeInsets.all(8),
padding: padding,
child: ValueListenableBuilder<int?>(
valueListenable: widget.ratingNotifier,
builder: (context, rating, child) {
final _rating = rating ?? 0;
valueListenable: valueNotifier,
builder: (context, selectedValue, child) {
final _rating = selectedValue ?? 0;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
...List.generate(5, (i) {
children: List.generate(5, (i) {
final thisRating = i + 1;
return Padding(
padding: const EdgeInsets.all(4),
@ -74,8 +78,7 @@ class _RateQuickChooserState extends State<RateQuickChooser> {
color: _rating < thisRating ? Colors.grey : Colors.amber,
),
);
})
],
}).toList(),
);
},
),
@ -85,9 +88,13 @@ class _RateQuickChooserState extends State<RateQuickChooser> {
}
void _onPointerMove(Offset globalPosition) {
final rowBox = context.findRenderObject() as RenderBox;
final rowSize = rowBox.size;
final local = rowBox.globalToLocal(globalPosition);
widget.ratingNotifier.value = (5 * local.dx / rowSize.width).ceil().clamp(0, 5);
final chooserBox = context.findRenderObject() as RenderBox;
final chooserSize = chooserBox.size;
final contentWidth = chooserSize.width - (margin.horizontal + padding.horizontal);
final local = chooserBox.globalToLocal(globalPosition);
final dx = local.dx - (margin.horizontal + padding.horizontal) / 2;
valueNotifier.value = (5 * dx / contentWidth).ceil().clamp(0, 5);
}
}

View file

@ -34,8 +34,8 @@ class _RateQuickButtonState extends ChooserQuickButtonState<RateButton, int> {
child: ScaleTransition(
scale: animation,
child: RateQuickChooser(
ratingNotifier: chooserValueNotifier,
moveUpdates: moveUpdates,
valueNotifier: chooserValueNotifier,
pointerGlobalPosition: pointerGlobalPosition,
),
),
);

View file

@ -213,8 +213,8 @@ class _GeoMapState extends State<GeoMap> {
MapThemeData.markerImageExtent + MapThemeData.markerOuterBorderWidth * 2 + MapThemeData.markerArrowSize.height,
),
dotMarkerSize: const Size(
DotMarker.diameter + MapThemeData.markerOuterBorderWidth * 2,
DotMarker.diameter + MapThemeData.markerOuterBorderWidth * 2,
MapThemeData.markerDotDiameter + MapThemeData.markerOuterBorderWidth * 2,
MapThemeData.markerDotDiameter + MapThemeData.markerOuterBorderWidth * 2,
),
overlayOpacityNotifier: widget.overlayOpacityNotifier,
overlayEntry: widget.overlayEntry,

View file

@ -53,7 +53,7 @@ class EntryLeafletMap<T> extends StatefulWidget {
});
@override
State<StatefulWidget> createState() => _EntryLeafletMapState<T>();
State<EntryLeafletMap<T>> createState() => _EntryLeafletMapState<T>();
}
class _EntryLeafletMapState<T> extends State<EntryLeafletMap<T>> with TickerProviderStateMixin {

View file

@ -212,7 +212,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with
final emptyAlbums = todoAlbums.whereNot(filledAlbums.contains).toSet();
if (settings.enableBin && filledAlbums.isNotEmpty) {
await move(
await doMove(
context,
moveType: MoveType.toBin,
entries: todoEntries,

View file

@ -280,6 +280,19 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
}
}
void quickMove(BuildContext context, String? album, {required bool copy}) {
final targetEntry = _getTargetEntry(context, EntryAction.editRating);
if (album == null || (!copy && targetEntry.directory == album)) return;
doQuickMove(
context,
moveType: copy ? MoveType.copy : MoveType.move,
entriesByDestination: {
album: {targetEntry}
},
);
}
void quickRate(BuildContext context, int? rating) {
final targetEntry = _getTargetEntry(context, EntryAction.editRating);
_metadataActionDelegate.quickRate(context, targetEntry, rating);
@ -441,7 +454,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
);
}
Future<void> _move(BuildContext context, AvesEntry targetEntry, {required MoveType moveType}) => move(
Future<void> _move(BuildContext context, AvesEntry targetEntry, {required MoveType moveType}) => doMove(
context,
moveType: moveType,
entries: {targetEntry},

View file

@ -148,7 +148,7 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi
}
Future<void> quickRate(BuildContext context, AvesEntry targetEntry, int? rating) async {
if (rating == null) return;
if (rating == null || targetEntry.rating == rating) return;
await edit(context, targetEntry, () => targetEntry.editRating(rating));
}

View file

@ -5,6 +5,7 @@ import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/app_bar/favourite_toggler.dart';
import 'package:aves/widgets/common/app_bar/move_button.dart';
import 'package:aves/widgets/common/app_bar/rate_button.dart';
import 'package:aves/widgets/common/basic/menu.dart';
import 'package:aves/widgets/common/basic/popup_menu_button.dart';
@ -204,6 +205,22 @@ class ViewerButtonRowContent extends StatelessWidget {
}
switch (action) {
case EntryAction.copy:
child = MoveButton(
copy: true,
chooserPosition: PopupMenuPosition.over,
onChooserValue: (album) => _quickMove(context, album, copy: true),
onPressed: onPressed,
);
break;
case EntryAction.move:
child = MoveButton(
copy: false,
chooserPosition: PopupMenuPosition.over,
onChooserValue: (album) => _quickMove(context, album, copy: false),
onPressed: onPressed,
);
break;
case EntryAction.toggleFavourite:
child = FavouriteToggler(
entries: {favouriteTargetEntry},
@ -365,5 +382,7 @@ class ViewerButtonRowContent extends StatelessWidget {
void _onActionSelected(BuildContext context, EntryAction action) => _entryActionDelegate.onActionSelected(context, action);
void _quickMove(BuildContext context, String? album, {required bool copy}) => _entryActionDelegate.quickMove(context, album, copy: copy);
void _quickRate(BuildContext context, int? rating) => _entryActionDelegate.quickRate(context, rating);
}

View file

@ -1,54 +1,18 @@
import 'package:aves_map/src/theme.dart';
import 'package:aves_ui/aves_ui.dart';
import 'package:flutter/material.dart';
class DotMarker extends StatelessWidget {
const DotMarker({super.key});
static const double diameter = 16;
static const double outerBorderRadiusDim = diameter;
static const double outerBorderWidth = MapThemeData.markerOuterBorderWidth;
static const double innerBorderWidth = MapThemeData.markerInnerBorderWidth;
static const outerBorderRadius = BorderRadius.all(Radius.circular(outerBorderRadiusDim));
static const innerRadius = Radius.circular(outerBorderRadiusDim - outerBorderWidth);
static const innerBorderRadius = BorderRadius.all(innerRadius);
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
final outerBorderColor = MapThemeData.markerThemedOuterBorderColor(isDark);
final innerBorderColor = MapThemeData.markerThemedInnerBorderColor(isDark);
final outerDecoration = BoxDecoration(
border: Border.fromBorderSide(BorderSide(
color: outerBorderColor,
width: outerBorderWidth,
)),
borderRadius: outerBorderRadius,
);
final innerDecoration = BoxDecoration(
border: Border.fromBorderSide(BorderSide(
color: innerBorderColor,
width: innerBorderWidth,
)),
borderRadius: innerBorderRadius,
);
return Container(
decoration: outerDecoration,
child: DecoratedBox(
decoration: innerDecoration,
position: DecorationPosition.foreground,
child: ClipRRect(
borderRadius: innerBorderRadius,
child: Container(
width: diameter,
height: diameter,
color: theme.colorScheme.secondary,
),
),
),
return const AvesDot(
diameter: MapThemeData.markerDotDiameter,
outerBorderWidth: MapThemeData.markerOuterBorderWidth,
innerBorderWidth: MapThemeData.markerInnerBorderWidth,
getOuterBorderColor: MapThemeData.markerThemedOuterBorderColor,
getInnerBorderColor: MapThemeData.markerThemedInnerBorderColor,
);
}
}

View file

@ -22,6 +22,7 @@ class MapThemeData {
static const double markerInnerBorderWidth = 2;
static const double markerImageExtent = 48.0;
static const Size markerArrowSize = Size(8, 6);
static const double markerDotDiameter = 16;
static Color markerThemedOuterBorderColor(bool isDark) => isDark ? Colors.white30 : Colors.black26;

View file

@ -8,6 +8,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.10.0"
aves_ui:
dependency: "direct main"
description:
path: "../aves_ui"
relative: true
source: path
version: "0.0.1"
characters:
dependency: transitive
description:

View file

@ -8,6 +8,8 @@ environment:
dependencies:
flutter:
sdk: flutter
aves_ui:
path: ../aves_ui
collection:
# TODO TLAD as of 2022/02/22, null safe version is pre-release
custom_rounded_rectangle_border: '>=0.2.0-nullsafety.0'

View file

@ -15,6 +15,13 @@ packages:
relative: true
source: path
version: "0.0.1"
aves_ui:
dependency: transitive
description:
path: "../aves_ui"
relative: true
source: path
version: "0.0.1"
characters:
dependency: transitive
description:

View file

@ -22,6 +22,13 @@ packages:
relative: true
source: path
version: "0.0.1"
aves_ui:
dependency: transitive
description:
path: "../aves_ui"
relative: true
source: path
version: "0.0.1"
characters:
dependency: transitive
description:

View file

@ -29,6 +29,13 @@ packages:
relative: true
source: path
version: "0.0.1"
aves_ui:
dependency: transitive
description:
path: "../aves_ui"
relative: true
source: path
version: "0.0.1"
characters:
dependency: transitive
description:

View file

@ -22,6 +22,13 @@ packages:
relative: true
source: path
version: "0.0.1"
aves_ui:
dependency: transitive
description:
path: "../aves_ui"
relative: true
source: path
version: "0.0.1"
characters:
dependency: transitive
description:

30
plugins/aves_ui/.gitignore vendored Normal file
View file

@ -0,0 +1,30 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
#/pubspec.lock
**/doc/api/
.dart_tool/
.packages
build/

10
plugins/aves_ui/.metadata Normal file
View file

@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: 5464c5bac742001448fe4fc0597be939379f88ea
channel: stable
project_type: package

View file

@ -0,0 +1 @@
include: ../../analysis_options.yaml

View file

@ -0,0 +1,3 @@
library aves_ui;
export 'src/dot.dart';

View file

@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
class AvesDot extends StatelessWidget {
final double diameter, outerBorderWidth, innerBorderWidth;
final Color Function(bool isDark) getOuterBorderColor, getInnerBorderColor;
const AvesDot({
super.key,
this.diameter = 16,
this.outerBorderWidth = 1.5,
this.innerBorderWidth = 2,
this.getOuterBorderColor = themedOuterBorderColor,
this.getInnerBorderColor = themedInnerBorderColor,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
final outerBorderColor = getOuterBorderColor(isDark);
final innerBorderColor = getInnerBorderColor(isDark);
final outerBorderRadius = BorderRadius.all(Radius.circular(diameter));
final innerRadius = Radius.circular(diameter - outerBorderWidth);
final innerBorderRadius = BorderRadius.all(innerRadius);
final outerDecoration = BoxDecoration(
border: Border.fromBorderSide(BorderSide(
color: outerBorderColor,
width: outerBorderWidth,
)),
borderRadius: outerBorderRadius,
);
final innerDecoration = BoxDecoration(
border: Border.fromBorderSide(BorderSide(
color: innerBorderColor,
width: innerBorderWidth,
)),
borderRadius: innerBorderRadius,
);
return Container(
decoration: outerDecoration,
child: DecoratedBox(
decoration: innerDecoration,
position: DecorationPosition.foreground,
child: ClipRRect(
borderRadius: innerBorderRadius,
child: Container(
width: diameter,
height: diameter,
color: theme.colorScheme.secondary,
),
),
),
);
}
static Color themedOuterBorderColor(bool isDark) => isDark ? Colors.white30 : Colors.black26;
static Color themedInnerBorderColor(bool isDark) => isDark ? const Color(0xFF212121) : Colors.white;
}

View file

@ -0,0 +1,64 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
characters:
dependency: transitive
description:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.1"
collection:
dependency: transitive
description:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.16.0"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
lints:
dependency: transitive
description:
name: lints
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.5"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
vector_math:
dependency: transitive
description:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.2"
sdks:
dart: ">=2.18.0 <3.0.0"

View file

@ -0,0 +1,15 @@
name: aves_ui
version: 0.0.1
publish_to: none
environment:
sdk: ">=2.18.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_lints:
flutter:

View file

@ -85,6 +85,13 @@ packages:
relative: true
source: path
version: "0.0.1"
aves_ui:
dependency: "direct main"
description:
path: "plugins/aves_ui"
relative: true
source: path
version: "0.0.1"
barcode:
dependency: transitive
description:
@ -615,7 +622,7 @@ packages:
name: mime
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
version: "1.0.3"
motion_sensors:
dependency: transitive
description:

View file

@ -37,6 +37,8 @@ dependencies:
path: plugins/aves_services
aves_services_platform:
path: plugins/aves_services_google
aves_ui:
path: plugins/aves_ui
charts_flutter:
collection:
connectivity_plus: