Skip to content

Commit

Permalink
feat(ui_storage): add TaskProgressIndicator (#10859)
Browse files Browse the repository at this point in the history
* feat(ui_storage): TaskProgressIndicator added

* update readme; fix onUploadStarted type

* bind StreamBuilder inside a widget, not a helper function

* add license headaer
  • Loading branch information
lesnitsky authored May 9, 2023
1 parent ecf0924 commit 6ae5773
Show file tree
Hide file tree
Showing 7 changed files with 311 additions and 8 deletions.
29 changes: 26 additions & 3 deletions packages/firebase_ui_storage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Firebase UI Storage is a set of Flutter widgets and utilities designed to help y

## Installation

Intall dependencies
Install dependencies

```sh
flutter pub add firebase_core firebase_storage firebase_ui_storage
Expand Down Expand Up @@ -98,6 +98,8 @@ class MyWidget extends StatelessWidget {

```dart
class MyUploadPage extends StatelessWidget {
const MyUploadPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
Expand All @@ -108,11 +110,32 @@ class MyUploadPage extends StatelessWidget {
print(err.toString());
},
onUploadComplete: (ref) {
print('File uploaded to ${ref.path}');
}
print('File uploaded to ${ref.fullPath}');
},
),
),
);
}
}
```

### TaskProgressIndicator

```dart
class MyUploadProgress extends StatelessWidget {
final UploadTask task;
const MyUploadProgress({super.key, required this.task});
@override
Widget build(BuildContext context) {
return Card(
child: Column(children: [
Text('Uploading ${task.snapshot.ref.name}...'),
TaskProgressIndicator(task: task),
]),
);
}
}
```
1 change: 1 addition & 0 deletions packages/firebase_ui_storage/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ class FirebaseUIStorageGallery extends StatelessWidget {
theme: ThemeData(
primarySwatch: Colors.blue,
brightness: brightness,
useMaterial3: true,
),
home: const Gallery(),
);
Expand Down
5 changes: 4 additions & 1 deletion packages/firebase_ui_storage/example/lib/src/apps.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@
// BSD-style license that can be found in the LICENSE file.

import 'package:firebase_ui_storage_example/main.dart';
import 'package:firebase_ui_storage_example/src/upload_button_app.dart';
import 'package:flutter/material.dart';

import 'progress_bar_app.dart';
import 'upload_button_app.dart';

abstract class App implements Widget {
String get name;
}

const apps = <App>[
UploadButtonApp(),
ProgressBarApp(),
];

class AppList extends StatelessWidget {
Expand Down
77 changes: 77 additions & 0 deletions packages/firebase_ui_storage/example/lib/src/progress_bar_app.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright 2023, the Chromium project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';

import 'package:firebase_storage/firebase_storage.dart';
import 'package:firebase_ui_storage/firebase_ui_storage.dart';
import 'package:flutter/material.dart';

import 'apps.dart';

class ProgressBarApp extends StatefulWidget implements App {
const ProgressBarApp({super.key});

@override
String get name => 'PrgressBar';

@override
State<StatefulWidget> createState() {
return _ProgressBarAppState();
}
}

class MockSnapshot implements TaskSnapshot {
@override
final int bytesTransferred;
@override
final int totalBytes;

MockSnapshot({
required this.bytesTransferred,
required this.totalBytes,
});

@override
noSuchMethod(Invocation invocation) {
return super.noSuchMethod(invocation);
}
}

class MockTask implements Task {
final ctrl = StreamController<TaskSnapshot>();

@override
noSuchMethod(Invocation invocation) {
return super.noSuchMethod(invocation);
}

@override
Stream<TaskSnapshot> get snapshotEvents => ctrl.stream;
}

class _ProgressBarAppState extends State<ProgressBarApp> {
final task = MockTask();

@override
void initState() {
super.initState();
emitProgress();
}

Future<void> emitProgress() async {
for (var i = 0; i <= 10; i++) {
await Future.delayed(const Duration(milliseconds: 300));
task.ctrl.add(MockSnapshot(
bytesTransferred: i * 10,
totalBytes: 100,
));
}
}

@override
Widget build(BuildContext context) {
return TaskProgressIndicator(task: task);
}
}
2 changes: 2 additions & 0 deletions packages/firebase_ui_storage/lib/firebase_ui_storage.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@ export 'src/config.dart'
export 'src/lib.dart' show FirebaseUIStorage;

export 'src/widgets/upload_button.dart' show UploadButton;
export 'src/widgets/progress_indicator.dart'
show TaskProgressIndicator, TaskProgressWidget, ErrorBuilder;
190 changes: 190 additions & 0 deletions packages/firebase_ui_storage/lib/src/widgets/progress_indicator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
// Copyright 2023, the Chromium project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:firebase_storage/firebase_storage.dart';
import 'package:firebase_ui_shared/firebase_ui_shared.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

/// A builder that is invoked for each progress event.
typedef TaskProgressBuilder = Widget Function(
BuildContext context,
double progress,
);

/// A builder that is invoked when an error occurs.
typedef ErrorBuilder = Widget Function(
BuildContext context,
Object? error,
);

class _BindTaskWidget extends StatelessWidget {
final Task task;
final TaskProgressBuilder builder;
final ErrorBuilder? errorBuilder;

const _BindTaskWidget({
required this.task,
required this.builder,
this.errorBuilder,
});

@override
Widget build(BuildContext context) {
return StreamBuilder<TaskSnapshot>(
stream: task.snapshotEvents,
builder: (context, snapshot) {
if (snapshot.hasError) {
return errorBuilder?.call(context, snapshot.error) ??
const SizedBox();
}

double progress;

if (!snapshot.hasData) {
progress = 0;
} else {
final taskSnapshot = snapshot.requireData;
progress = taskSnapshot.bytesTransferred / taskSnapshot.totalBytes;
}

return builder(context, progress);
},
);
}
}

/// An abstract widget that simplifies building custom progress indicators for
/// upload and download tasks.
///
/// Example implementation:
///
/// ```dart
/// class MyProgressIndicator extends TaskProgressWidget {
/// final Task task;
///
/// const MyProgressIndicator({super.key, required this.task});
///
/// @override
/// Widget buildProgressIndicator(BuildContext context, double progress) {
/// return Text('Progress: ${progress.toStringAsFixed(2)}');
/// }
/// }
/// ```
abstract class TaskProgressWidget extends StatelessWidget {
const TaskProgressWidget({super.key});

/// The task to track.
Task get task;

/// A builder that is called when an error occurs.
ErrorBuilder? get errorBuilder;

/// A builder that is called for each progress event.
Widget buildProgressIndicator(BuildContext context, double progress);

@override
Widget build(BuildContext context) {
return _BindTaskWidget(
task: task,
errorBuilder: errorBuilder,
builder: (context, progress) {
return CircularProgressIndicator(value: progress);
},
);
}
}

/// Material/Cupertino app aware task progress indicator widget.
///
/// Uses [LinearProgressIndicator] under MaterialApp and a custom
/// iOS-style progress bar under [CupertinoApp].
class TaskProgressIndicator extends PlatformWidget {
/// The task to track.
final Task task;

/// A builder that is called when an error occurs.
final Widget Function(BuildContext context, Object? error)? errorBuilder;

const TaskProgressIndicator({
super.key,
required this.task,
this.errorBuilder,
});

@override
Widget buildCupertino(BuildContext context) {
return _BindTaskWidget(
task: task,
errorBuilder: errorBuilder,
builder: (context, progress) {
return _CupertinoProgressBar(progress: progress);
},
);
}

@override
Widget buildMaterial(BuildContext context) {
return _BindTaskWidget(
task: task,
errorBuilder: errorBuilder,
builder: (context, progress) {
return LinearProgressIndicator(value: progress);
},
);
}
}

class _CupertinoProgressBar extends StatelessWidget {
final double progress;

const _CupertinoProgressBar({required this.progress});

@override
Widget build(BuildContext context) {
final cupertinoTheme = CupertinoTheme.of(context);

return _CupertinoProgressBarBackground(
child: LayoutBuilder(
builder: (context, constraints) {
return Stack(
children: [
AnimatedPositioned(
duration: const Duration(milliseconds: 100),
left: 0,
top: 0,
bottom: 0,
width: constraints.maxWidth * progress,
child: Container(
decoration: BoxDecoration(
color: cupertinoTheme.primaryColor,
borderRadius: BorderRadius.circular(4),
),
),
),
],
);
},
),
);
}
}

class _CupertinoProgressBarBackground extends StatelessWidget {
final Widget child;

const _CupertinoProgressBarBackground({required this.child});

@override
Widget build(BuildContext context) {
return Container(
constraints: const BoxConstraints(minHeight: 8, maxHeight: 8),
decoration: BoxDecoration(
color: CupertinoColors.secondarySystemFill,
borderRadius: BorderRadius.circular(4),
),
child: child,
);
}
}
Loading

0 comments on commit 6ae5773

Please sign in to comment.