filter bar: clear filter, app filter color

This commit is contained in:
Thibault Deckers 2020-03-27 13:05:54 +09:00
parent 4c23a0f5ad
commit cb553df009
27 changed files with 257 additions and 141 deletions

View file

@ -5,8 +5,8 @@ import 'package:aves/model/settings.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/utils/viewer_service.dart';
import 'package:aves/widgets/album/collection_page.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/common/providers/media_store_collection_provider.dart';
import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart';
import 'package:aves/widgets/common/data_providers/media_store_collection_provider.dart';
import 'package:aves/widgets/fullscreen/fullscreen_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

View file

@ -1,10 +1,16 @@
import 'package:aves/model/image_entry.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/utils/color_utils.dart';
import 'package:aves/widgets/common/icons.dart';
import 'package:aves/widgets/common/image_providers/app_icon_image_provider.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:outline_material_icons/outline_material_icons.dart';
import 'package:palette_generator/palette_generator.dart';
import 'package:path/path.dart';
abstract class CollectionFilter {
abstract class CollectionFilter implements Comparable<CollectionFilter> {
static const List<String> collectionFilterOrder = [
VideoFilter.type,
GifFilter.type,
@ -20,11 +26,19 @@ abstract class CollectionFilter {
String get label;
Widget iconBuilder(BuildContext context);
Widget iconBuilder(BuildContext context, double size);
Future<Color> color(BuildContext context) => SynchronousFuture(stringToColor(label));
String get typeKey;
int get displayPriority => collectionFilterOrder.indexOf(typeKey);
@override
int compareTo(CollectionFilter other) {
final c = displayPriority.compareTo(other.displayPriority);
return c != 0 ? c : compareAsciiUpperCase(label, other.label);
}
}
class AlbumFilter extends CollectionFilter {
@ -41,7 +55,23 @@ class AlbumFilter extends CollectionFilter {
String get label => album.split(separator).last;
@override
Widget iconBuilder(context) => IconUtils.getAlbumIcon(context, album) ?? Icon(OMIcons.photoAlbum);
Widget iconBuilder(context, size) {
return IconUtils.getAlbumIcon(context: context, album: album, size: size) ?? Icon(OMIcons.photoAlbum, size: size);
}
Future<Color> color(BuildContext context) async {
Color color;
if (androidFileUtils.getAlbumType(album) == AlbumType.App) {
final palette = await PaletteGenerator.fromImageProvider(
AppIconImage(
packageName: androidFileUtils.getAlbumAppPackageName(album),
size: 24,
),
);
color = palette.dominantColor?.color;
}
return color ?? super.color(context);
}
@override
String get typeKey => type;
@ -70,7 +100,7 @@ class TagFilter extends CollectionFilter {
String get label => tag;
@override
Widget iconBuilder(context) => Icon(OMIcons.localOffer);
Widget iconBuilder(context, size) => Icon(OMIcons.localOffer, size: size);
@override
String get typeKey => type;
@ -99,7 +129,7 @@ class CountryFilter extends CollectionFilter {
String get label => country;
@override
Widget iconBuilder(context) => Icon(OMIcons.place);
Widget iconBuilder(context, size) => Icon(OMIcons.place, size: size);
@override
String get typeKey => type;
@ -124,7 +154,7 @@ class VideoFilter extends CollectionFilter {
String get label => 'Video';
@override
Widget iconBuilder(context) => Icon(OMIcons.movie);
Widget iconBuilder(context, size) => Icon(OMIcons.movie, size: size);
@override
String get typeKey => type;
@ -149,7 +179,7 @@ class GifFilter extends CollectionFilter {
String get label => 'GIF';
@override
Widget iconBuilder(context) => Icon(OMIcons.gif);
Widget iconBuilder(context, size) => Icon(OMIcons.gif, size: size);
@override
String get typeKey => type;
@ -178,7 +208,7 @@ class QueryFilter extends CollectionFilter {
String get label => '${query}';
@override
Widget iconBuilder(context) => Icon(OMIcons.formatQuote);
Widget iconBuilder(context, size) => Icon(OMIcons.formatQuote, size: size);
@override
String get typeKey => type;

View file

@ -52,7 +52,7 @@ class CollectionLens with ChangeNotifier {
source: source,
filters: [
...filters,
if (filter != null) filter,
filter,
],
groupFactor: groupFactor,
sortFactor: sortFactor,
@ -71,6 +71,14 @@ class CollectionLens with ChangeNotifier {
Object heroTag(ImageEntry entry) => '$hashCode${entry.uri}';
void removeFilter(CollectionFilter filter) {
if (!filters.contains(filter)) return;
filters.remove(filter);
_applyFilters();
_applySort();
_applyGroup();
}
void sort(SortFactor sortFactor) {
this.sortFactor = sortFactor;
_applySort();

View file

@ -82,7 +82,7 @@ class _CollectionDrawerState extends State<CollectionDrawer> {
);
final buildAlbumEntry = (album) => _FilteredCollectionNavTile(
source: source,
leading: IconUtils.getAlbumIcon(context, album),
leading: IconUtils.getAlbumIcon(context: context, album: album),
title: CollectionSource.getUniqueAlbumName(album, source.sortedAlbums),
dense: true,
filter: AlbumFilter(album),

View file

@ -3,8 +3,8 @@ import 'package:aves/widgets/album/all_collection_app_bar.dart';
import 'package:aves/widgets/album/collection_drawer.dart';
import 'package:aves/widgets/album/filter_bar.dart';
import 'package:aves/widgets/album/thumbnail_collection.dart';
import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart';
import 'package:aves/widgets/common/menu_row.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/stats.dart';
import 'package:flutter/material.dart';
import 'package:outline_material_icons/outline_material_icons.dart';
@ -27,7 +27,7 @@ class CollectionPage extends StatelessWidget {
: SliverAppBar(
title: const Text('Aves'),
actions: _buildActions(),
bottom: FilterBar(collection.filters),
bottom: FilterBar(),
floating: true,
),
),

View file

@ -4,7 +4,7 @@ import 'dart:ui' as ui;
import 'package:aves/model/image_entry.dart';
import 'package:aves/widgets/album/collection_section.dart';
import 'package:aves/widgets/album/thumbnail.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_sticky_header/flutter_sticky_header.dart';

View file

@ -162,7 +162,7 @@ class SectionHeader extends StatelessWidget {
}
Widget _buildAlbumSectionHeader(BuildContext context) {
Widget albumIcon = IconUtils.getAlbumIcon(context, sectionKey as String);
Widget albumIcon = IconUtils.getAlbumIcon(context: context, album: sectionKey as String);
if (albumIcon != null) {
albumIcon = Material(
type: MaterialType.circle,

View file

@ -1,27 +1,20 @@
import 'package:aves/model/collection_filters.dart';
import 'package:aves/model/collection_lens.dart';
import 'package:aves/widgets/common/aves_filter_chip.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class FilterBar extends StatelessWidget implements PreferredSizeWidget {
final List<CollectionFilter> filters;
final ScrollController _scrollController = ScrollController();
static const EdgeInsets padding = EdgeInsets.only(left: 8, right: 8, bottom: 8);
static const EdgeInsets padding = EdgeInsets.symmetric(horizontal: 8);
@override
final Size preferredSize = Size.fromHeight(kMinInteractiveDimension + padding.vertical);
FilterBar(Set<CollectionFilter> filters)
: this.filters = filters.toList()
..sort((a, b) {
final c = a.displayPriority.compareTo(b.displayPriority);
return c != 0 ? c : compareAsciiUpperCase(a.label, b.label);
});
@override
Widget build(BuildContext context) {
debugPrint('$runtimeType build');
final collection = Provider.of<CollectionLens>(context);
final filters = collection.filters.toList()..sort();
return Container(
// specify transparent as a workaround to prevent
// chip border clipping when the floating app bar is fading
@ -34,16 +27,18 @@ class FilterBar extends StatelessWidget implements PreferredSizeWidget {
onNotification: (notification) => true,
child: ListView.separated(
scrollDirection: Axis.horizontal,
controller: _scrollController,
primary: false,
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.all(AvesFilterChip.buttonBorderWidth / 2),
itemBuilder: (context, index) {
if (index >= filters.length) return null;
final filter = filters[index];
return AvesFilterChip(
filter,
onPressed: (filter) {},
return Center(
child: AvesFilterChip(
filter,
clearable: true,
onPressed: collection.removeFilter,
),
);
},
separatorBuilder: (context, index) => const SizedBox(width: 8),

View file

@ -2,7 +2,7 @@ import 'package:aves/model/collection_filters.dart';
import 'package:aves/model/collection_lens.dart';
import 'package:aves/model/image_entry.dart';
import 'package:aves/widgets/album/thumbnail_collection.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart';
import 'package:flutter/material.dart';
import 'package:outline_material_icons/outline_material_icons.dart';
import 'package:provider/provider.dart';

View file

@ -4,7 +4,7 @@ import 'package:aves/model/image_entry.dart';
import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/common/icons.dart';
import 'package:aves/widgets/common/image_preview.dart';
import 'package:aves/widgets/fullscreen/uri_picture_provider.dart';
import 'package:aves/widgets/common/image_providers/uri_picture_provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';

View file

@ -1,49 +0,0 @@
import 'dart:typed_data';
import 'package:aves/utils/android_app_service.dart';
import 'package:flutter/material.dart';
import 'package:transparent_image/transparent_image.dart';
class AppIcon extends StatefulWidget {
final String packageName;
final double size;
final double devicePixelRatio;
const AppIcon({
Key key,
@required this.packageName,
@required this.size,
@required this.devicePixelRatio,
}) : super(key: key);
@override
State<StatefulWidget> createState() => AppIconState();
}
class AppIconState extends State<AppIcon> {
Future<Uint8List> _byteLoader;
@override
void initState() {
super.initState();
final dim = (widget.size * widget.devicePixelRatio).round();
_byteLoader = AndroidAppService.getAppIcon(widget.packageName, dim);
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: _byteLoader,
builder: (futureContext, AsyncSnapshot<Uint8List> snapshot) {
final bytes = (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) ? snapshot.data : kTransparentImage;
return bytes.isNotEmpty
? Image.memory(
bytes,
width: widget.size,
height: widget.size,
)
: const SizedBox.shrink();
},
);
}
}

View file

@ -1,56 +1,101 @@
import 'package:aves/model/collection_filters.dart';
import 'package:aves/utils/color_utils.dart';
import 'package:flutter/material.dart';
import 'package:outline_material_icons/outline_material_icons.dart';
typedef FilterCallback = void Function(CollectionFilter filter);
class AvesFilterChip extends StatelessWidget {
class AvesFilterChip extends StatefulWidget {
final CollectionFilter filter;
final bool clearable;
final FilterCallback onPressed;
String get label => filter.label;
static const double buttonBorderWidth = 2;
static const double maxChipWidth = 160;
static const double iconSize = 20;
static const double padding = 6;
const AvesFilterChip(
this.filter, {
this.clearable = false,
@required this.onPressed,
});
@override
_AvesFilterChipState createState() => _AvesFilterChipState();
}
class _AvesFilterChipState extends State<AvesFilterChip> {
Future<Color> _colorFuture;
CollectionFilter get filter => widget.filter;
String get label => filter.label;
@override
void initState() {
super.initState();
_initColorLoader();
}
@override
void didUpdateWidget(AvesFilterChip oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.filter != filter) {
_initColorLoader();
}
}
void _initColorLoader() => _colorFuture = filter.color(context);
@override
Widget build(BuildContext context) {
final icon = filter.iconBuilder(context);
final leading = filter.iconBuilder(context, AvesFilterChip.iconSize);
final trailing = widget.clearable ? Icon(OMIcons.clear, size: AvesFilterChip.iconSize) : null;
final child = Row(
mainAxisSize: MainAxisSize.min,
children: [
if (leading != null) ...[
leading,
const SizedBox(width: AvesFilterChip.padding * 1.6),
],
Flexible(
child: Text(
label,
softWrap: false,
overflow: TextOverflow.fade,
maxLines: 1,
),
),
if (trailing != null) ...[
const SizedBox(width: AvesFilterChip.padding),
trailing,
],
],
);
final shape = RoundedRectangleBorder(
borderRadius: BorderRadius.circular(42),
);
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: maxChipWidth),
constraints: const BoxConstraints(maxWidth: AvesFilterChip.maxChipWidth),
child: Tooltip(
message: label,
child: OutlineButton(
onPressed: onPressed != null ? () => onPressed(filter) : null,
borderSide: BorderSide(
color: stringToColor(label),
width: buttonBorderWidth,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(42),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
icon,
const SizedBox(width: 8),
],
Flexible(
child: Text(
label,
softWrap: false,
overflow: TextOverflow.fade,
maxLines: 1,
),
child: FutureBuilder(
future: _colorFuture,
builder: (context, AsyncSnapshot<Color> snapshot) {
return OutlineButton(
onPressed: widget.onPressed != null ? () => widget.onPressed(filter) : null,
borderSide: BorderSide(
color: snapshot.hasData ? snapshot.data : Colors.transparent,
width: AvesFilterChip.buttonBorderWidth,
),
],
),
padding: const EdgeInsets.symmetric(horizontal: AvesFilterChip.padding * 2),
shape: shape,
child: child,
);
},
),
),
);

View file

@ -2,10 +2,9 @@ import 'dart:ui';
import 'package:aves/model/image_entry.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/widgets/common/app_icon.dart';
import 'package:aves/widgets/common/image_providers/app_icon_image_provider.dart';
import 'package:flutter/material.dart';
import 'package:outline_material_icons/outline_material_icons.dart';
import 'package:provider/provider.dart';
class VideoIcon extends StatelessWidget {
final ImageEntry entry;
@ -88,23 +87,27 @@ class OverlayIcon extends StatelessWidget {
}
class IconUtils {
static Widget getAlbumIcon(BuildContext context, String albumDirectory) {
switch (androidFileUtils.getAlbumType(albumDirectory)) {
static Widget getAlbumIcon({
@required BuildContext context,
@required String album,
double size = 24,
}) {
switch (androidFileUtils.getAlbumType(album)) {
case AlbumType.Camera:
return Icon(OMIcons.photoCamera);
return Icon(OMIcons.photoCamera, size: size);
case AlbumType.Screenshots:
case AlbumType.ScreenRecordings:
return Icon(OMIcons.smartphone);
return Icon(OMIcons.smartphone, size: size);
case AlbumType.Download:
return Icon(Icons.file_download);
return Icon(Icons.file_download, size: size);
case AlbumType.App:
return Selector<MediaQueryData, double>(
selector: (c, mq) => mq.devicePixelRatio,
builder: (c, devicePixelRatio, child) => AppIcon(
packageName: androidFileUtils.getAlbumAppPackageName(albumDirectory),
size: IconTheme.of(context).size,
devicePixelRatio: devicePixelRatio,
return Image(
image: AppIconImage(
packageName: androidFileUtils.getAlbumAppPackageName(album),
size: size,
),
width: size,
height: size,
);
case AlbumType.Default:
default:

View file

@ -0,0 +1,69 @@
import 'dart:typed_data';
import 'dart:ui' as ui show Codec;
import 'package:aves/utils/android_app_service.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class AppIconImage extends ImageProvider<AppIconImageKey> {
const AppIconImage({
@required this.packageName,
@required this.size,
this.scale = 1.0,
}) : assert(packageName != null),
assert(scale != null);
final String packageName;
final double size;
final double scale;
@override
Future<AppIconImageKey> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<AppIconImageKey>(AppIconImageKey(
packageName: packageName,
sizePixels: (size * configuration.devicePixelRatio).round(),
scale: scale,
));
}
@override
ImageStreamCompleter load(AppIconImageKey key, DecoderCallback decode) {
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode),
scale: key.scale,
informationCollector: () sync* {
yield ErrorDescription('uri=$packageName, size=$size');
},
);
}
Future<ui.Codec> _loadAsync(AppIconImageKey key, DecoderCallback decode) async {
final Uint8List bytes = await AndroidAppService.getAppIcon(key.packageName, key.sizePixels);
if (bytes.lengthInBytes == 0) {
return null;
}
return await decode(bytes);
}
}
class AppIconImageKey {
final String packageName;
final int sizePixels;
final double scale;
const AppIconImageKey({
@required this.packageName,
@required this.sizePixels,
this.scale,
});
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) return false;
return other is AppIconImageKey && other.packageName == packageName && other.sizePixels == sizePixels && other.scale == scale;
}
@override
int get hashCode => hashValues(packageName, sizePixels, scale);
}

View file

@ -47,7 +47,7 @@ class UriImage extends ImageProvider<UriImage> {
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) return false;
return other is UriImage && other.uri == uri && other.scale == scale;
return other is UriImage && other.uri == uri && other.mimeType == mimeType && other.scale == scale;
}
@override

View file

@ -4,9 +4,10 @@ import 'package:aves/model/image_metadata.dart';
import 'package:aves/model/metadata_db.dart';
import 'package:aves/model/settings.dart';
import 'package:aves/utils/file_utils.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_svg/flutter_svg.dart';
class DebugPage extends StatefulWidget {
final CollectionSource source;
@ -86,6 +87,9 @@ class DebugPageState extends State<DebugPage> {
},
),
const Divider(),
Text('Image cache: ${imageCache.currentSize} items, ${formatFilesize(imageCache.currentSizeBytes)}'),
Text('SVG cache: ${PictureProvider.cacheCount} items'),
const Divider(),
const Text('Time dilation'),
Slider(
value: timeDilation,

View file

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:aves/model/collection_lens.dart';
import 'package:aves/model/image_entry.dart';
import 'package:aves/utils/android_app_service.dart';
import 'package:aves/widgets/common/image_providers/uri_image_provider.dart';
import 'package:flushbar/flushbar.dart';
import 'package:flutter/material.dart';
import 'package:pdf/widgets.dart' as pdf;
@ -77,7 +78,7 @@ class FullscreenActionDelegate {
final doc = pdf.Document(title: entry.title);
final image = await pdfImageFromImageProvider(
pdf: doc.document,
image: FileImage(File(entry.path)),
image: UriImage(uri: entry.uri, mimeType: entry.mimeType),
);
doc.addPage(pdf.Page(build: (context) => pdf.Center(child: pdf.Image(image)))); // Page
unawaited(Printing.layoutPdf(

View file

@ -3,13 +3,14 @@ import 'dart:math';
import 'package:aves/model/collection_lens.dart';
import 'package:aves/model/image_entry.dart';
import 'package:aves/widgets/common/image_providers/uri_image_provider.dart';
import 'package:aves/widgets/fullscreen/fullscreen_action_delegate.dart';
import 'package:aves/widgets/fullscreen/image_page.dart';
import 'package:aves/widgets/fullscreen/info/info_page.dart';
import 'package:aves/widgets/fullscreen/overlay/bottom.dart';
import 'package:aves/widgets/fullscreen/overlay/top.dart';
import 'package:aves/widgets/fullscreen/overlay/video.dart';
import 'package:aves/widgets/fullscreen/uri_image_provider.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
@ -126,14 +127,14 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
if (_currentVerticalPage.value == infoPage) {
// back from info to image
_goToVerticalPage(imagePage);
return Future.value(false);
return SynchronousFuture(false);
}
if (!ModalRoute.of(context).canPop) {
// exit app when trying to pop a fullscreen page that is a viewer for a single entry
exit(0);
}
_onLeave();
return Future.value(true);
return SynchronousFuture(true);
},
child: Stack(
children: [

View file

@ -1,6 +1,6 @@
import 'package:aves/model/collection_lens.dart';
import 'package:aves/model/image_entry.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart';
import 'package:aves/widgets/fullscreen/fullscreen_body.dart';
import 'package:flutter/material.dart';

View file

@ -1,7 +1,7 @@
import 'package:aves/model/image_entry.dart';
import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/fullscreen/uri_image_provider.dart';
import 'package:aves/widgets/fullscreen/uri_picture_provider.dart';
import 'package:aves/widgets/common/image_providers/uri_image_provider.dart';
import 'package:aves/widgets/common/image_providers/uri_picture_provider.dart';
import 'package:aves/widgets/fullscreen/video.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';

View file

@ -3,7 +3,7 @@ import 'package:aves/model/collection_lens.dart';
import 'package:aves/model/image_entry.dart';
import 'package:aves/widgets/album/collection_page.dart';
import 'package:aves/widgets/common/aves_filter_chip.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart';
import 'package:aves/widgets/fullscreen/info/basic_section.dart';
import 'package:aves/widgets/fullscreen/info/location_section.dart';
import 'package:aves/widgets/fullscreen/info/metadata_section.dart';
@ -155,11 +155,12 @@ class InfoPageState extends State<InfoPage> {
void _goToFilteredCollection(CollectionFilter filter) {
if (collection == null) return;
Navigator.push(
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
builder: (context) => CollectionPage(collection.derive(filter)),
),
(route) => false,
);
}
}

View file

@ -1,7 +1,7 @@
import 'package:aves/model/collection_lens.dart';
import 'package:aves/model/image_entry.dart';
import 'package:aves/utils/color_utils.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart';
import 'package:charts_flutter/flutter.dart' as charts;
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';

View file

@ -232,6 +232,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.1"
palette_generator:
dependency: "direct main"
description:
name: palette_generator
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.2"
path:
dependency: "direct main"
description:

View file

@ -40,6 +40,7 @@ dependencies:
outline_material_icons:
path:
pdf:
palette_generator:
pedantic:
percent_indicator:
permission_handler: