Skip to content

Commit

Permalink
Bin sheet (#172)
Browse files Browse the repository at this point in the history
* Bin sheet

* Bin content

* Tweak empty state

* Style thumbnails

* Destroy slide out

* Label placeholder

* Restore button

* Read metadata

* Display count and size

* Format size

* All files

* Spec type

* Bump version
  • Loading branch information
ruskakimov authored Jun 6, 2021
1 parent de9246d commit 931c37d
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 104 deletions.
42 changes: 42 additions & 0 deletions lib/common/data/project/project.dart
Original file line number Diff line number Diff line change
Expand Up @@ -296,4 +296,46 @@ class Project extends ChangeNotifier {

notifyListeners();
}

// =========
// Metadata:
// =========

/// Number of files in project folder.
String get fileCountLabel {
final count = _fileCount;
if (count == null) return '-';
if (count == 1) return '1 file';
return '$count files';
}

/// Total size of project folder.
String get projectSizeLabel {
final size = _sizeInBytes;
if (size == null) return '-';

if (size < 1000000) {
final kb = size / 1000;
return '${kb.toStringAsFixed(1)} KB';
}

final mb = size / 1000000;
return '${mb.toStringAsFixed(1)} MB';
}

int? _fileCount;
int? _sizeInBytes;

Future<void> readMetadata() async {
final files = await directory
.list(recursive: true)
.where((entity) => entity is File)
.toList();
_fileCount = files.length;

final fileStats = await Future.wait(files.map((file) => file.stat()));
_sizeInBytes = fileStats.fold<int>(0, (sum, stat) => sum + stat.size);

notifyListeners();
}
}
4 changes: 2 additions & 2 deletions lib/common/ui/show_side_sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import 'package:flutter/material.dart';

showSideSheet({
required BuildContext context,
Widget Function(BuildContext)? builder,
required Widget Function(BuildContext) builder,
bool rightSide = true,
}) {
showGeneralDialog(
Expand All @@ -21,7 +21,7 @@ showSideSheet({
width: sheetWidth,
child: Material(
color: Theme.of(context).colorScheme.surface,
child: builder!(context),
child: builder(context),
),
),
);
Expand Down
6 changes: 3 additions & 3 deletions lib/home/home_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class HomePage extends StatefulWidget {
}

class _HomePageState extends State<HomePage> {
final GalleryModel manager = GalleryModel();
final GalleryModel gallery = GalleryModel();

@override
void initState() {
Expand All @@ -29,14 +29,14 @@ class _HomePageState extends State<HomePage> {
Future<void> _initProjectsManager() async {
if (await Permission.storage.request().isGranted) {
final Directory dir = await getApplicationDocumentsDirectory();
await manager.init(dir);
await gallery.init(dir);
}
}

@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<GalleryModel>.value(
value: manager,
value: gallery,
child: Scaffold(
backgroundColor: Theme.of(context).colorScheme.background,
body: Stack(
Expand Down
43 changes: 17 additions & 26 deletions lib/home/ui/bin_button.dart
Original file line number Diff line number Diff line change
@@ -1,41 +1,32 @@
import 'package:flutter/material.dart';
import 'package:mooltik/home/data/gallery_model.dart';
import 'package:provider/provider.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:mooltik/common/ui/app_icon_button.dart';
import 'package:mooltik/common/ui/popup_with_arrow.dart';
import 'package:mooltik/common/ui/show_side_sheet.dart';

import 'bin_contents.dart';

class BinButton extends StatefulWidget {
class BinButton extends StatelessWidget {
const BinButton({
Key? key,
}) : super(key: key);

@override
_BinButtonState createState() => _BinButtonState();
}

class _BinButtonState extends State<BinButton> {
bool _binOpen = false;

@override
Widget build(BuildContext context) {
return PopupWithArrowEntry(
visible: _binOpen,
arrowSide: ArrowSide.top,
arrowSidePosition: ArrowSidePosition.end,
popupBody: SizedBox(
width: 200,
height: (MediaQuery.of(context).size.height - 70).clamp(0.0, 500.0),
child: BinContents(),
),
child: AppIconButton(
icon: FontAwesomeIcons.trashAlt,
onTap: () {
setState(() => _binOpen = true);
},
),
onTapOutside: () {
setState(() => _binOpen = false);
BinContents();
return AppIconButton(
icon: FontAwesomeIcons.trashAlt,
onTap: () {
final gallery = context.read<GalleryModel>();

showSideSheet(
context: context,
builder: (context) => ChangeNotifierProvider.value(
value: gallery,
child: BinContents(),
),
);
},
);
}
Expand Down
191 changes: 119 additions & 72 deletions lib/home/ui/bin_contents.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:mooltik/common/data/project/project.dart';
import 'package:mooltik/common/ui/labeled_icon_button.dart';
import 'package:provider/provider.dart';
Expand All @@ -15,108 +16,154 @@ class BinContents extends StatelessWidget {
final gallery = context.watch<GalleryModel>();
final binnedProjects = gallery.binnedProjects;

if (binnedProjects.isEmpty) {
return Column(
return ClipRect(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Text(
'Bin',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
),
Expanded(
child: binnedProjects.isEmpty
? _EmptyState()
: _BinItemList(binnedProjects: binnedProjects),
),
],
),
);
}
}

class _EmptyState extends StatelessWidget {
const _EmptyState({
Key? key,
}) : super(key: key);

@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(FontAwesomeIcons.toiletPaperSlash),
Icon(MdiIcons.bee),
SizedBox(height: 12),
Text('Nothing here...'),
SizedBox(height: 52),
],
);
}
),
);
}
}

class _BinItemList extends StatefulWidget {
const _BinItemList({
Key? key,
required this.binnedProjects,
}) : super(key: key);

final List<Project> binnedProjects;

@override
__BinItemListState createState() => __BinItemListState();
}

class __BinItemListState extends State<_BinItemList> {
@override
void initState() {
super.initState();
widget.binnedProjects.forEach((project) => project.readMetadata());
}

@override
Widget build(BuildContext context) {
return ListView(
padding: const EdgeInsets.symmetric(vertical: 6.0),
children: [
for (final project in binnedProjects)
_BinItem(
key: Key('${project.creationEpoch}'),
project: project,
for (final project in widget.binnedProjects)
ChangeNotifierProvider.value(
value: project,
child: _BinItem(
key: Key('${project.creationEpoch}'),
),
)
],
);
}
}

class _BinItem extends StatelessWidget {
const _BinItem({
Key? key,
required this.project,
}) : super(key: key);

final Project project;
const _BinItem({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
final project = context.watch<Project>();

return Slidable(
actionPane: SlidableDrawerActionPane(),
actionExtentRatio: 0.5,
actions: [
_BinSlideAction(
color: Colors.red,
icon: FontAwesomeIcons.fireAlt,
label: 'Destroy',
onTap: () {
context.read<GalleryModel>().deleteProject(project);
},
),
],
closeOnScroll: true,
secondaryActions: [
_BinSlideAction(
icon: FontAwesomeIcons.reply,
label: 'Restore',
onTap: () {
context.read<GalleryModel>().restoreProject(project);
},
SlideAction(
color: Colors.red,
closeOnTap: true,
child: LabeledIconButton(
icon: FontAwesomeIcons.burn,
label: 'Destroy',
color: Colors.white,
onTap: () {
context.read<GalleryModel>().deleteProject(project);
},
),
),
],
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 6.0,
),
child: Container(
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Colors.white, // in case thumbnail is missing background
borderRadius: BorderRadius.circular(8),
),
child: Image.file(project.thumbnail),
child: SizedBox(
height: 80,
child: Row(
children: [
_buildThumbnail(project),
_buildLabel(context, project),
Spacer(),
LabeledIconButton(
icon: FontAwesomeIcons.reply,
label: 'Restore',
color: Theme.of(context).colorScheme.onSurface,
onTap: () {
context.read<GalleryModel>().restoreProject(project);
},
),
],
),
),
);
}
}

class _BinSlideAction extends StatelessWidget {
const _BinSlideAction({
Key? key,
required this.icon,
required this.label,
this.color,
this.onTap,
}) : super(key: key);

final IconData icon;
final String label;
final Color? color;
final VoidCallback? onTap;

@override
Widget build(BuildContext context) {
return SlideAction(
child: Material(
color: color,
Widget _buildThumbnail(Project project) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16),
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
clipBehavior: Clip.antiAlias,
child: LabeledIconButton(
icon: icon,
label: label,
color: Colors.white,
onTap: onTap,
),
),
child: Image.file(project.thumbnail),
);
}

Widget _buildLabel(BuildContext context, Project project) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(project.fileCountLabel),
SizedBox(height: 4),
Text(
project.projectSizeLabel,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
),
),
],
);
}
}
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.7.1
version: 1.8.0

environment:
sdk: '>=2.12.0 <3.0.0'
Expand Down

0 comments on commit 931c37d

Please sign in to comment.