Skip to content

Commit

Permalink
feat: Allow adding custom media sources to example (#1637)
Browse files Browse the repository at this point in the history
# Description

Update the example to allow playing custom media sources.
Closes #1622

(cherry picked from commit 1eabe61)
  • Loading branch information
Gustl22 committed Sep 26, 2023
1 parent daf6668 commit d374e4d
Show file tree
Hide file tree
Showing 3 changed files with 220 additions and 38 deletions.
13 changes: 2 additions & 11 deletions packages/audioplayers/example/lib/components/dlg.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,10 @@ class Dlg extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
elevation: 0,
backgroundColor: Colors.white,
child: Container(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: contentBox(context),
child: child,
),
);
}

Widget contentBox(BuildContext context) {
return child;
}
}
13 changes: 6 additions & 7 deletions packages/audioplayers/example/lib/tabs/controls.dart
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ class _ControlsTabState extends State<ControlsTab>
txt: 'Custom',
onPressed: () async {
dialog(
SeekDialog(
_SeekDialog(
value: modalInputSeek,
setValue: (it) => setState(() => modalInputSeek = it),
seekDuration: () => _seekDuration(
Expand All @@ -184,18 +184,17 @@ class _ControlsTabState extends State<ControlsTab>
bool get wantKeepAlive => true;
}

class SeekDialog extends StatelessWidget {
class _SeekDialog extends StatelessWidget {
final VoidCallback seekDuration;
final VoidCallback seekPercent;
final void Function(String val) setValue;
final String value;

const SeekDialog({
const _SeekDialog({
required this.seekDuration,
required this.seekPercent,
required this.value,
required this.setValue,
super.key,
});

@override
Expand All @@ -209,7 +208,7 @@ class SeekDialog extends StatelessWidget {
onChange: setValue,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Btn(
txt: 'millis',
Expand All @@ -232,9 +231,9 @@ class SeekDialog extends StatelessWidget {
seekPercent();
},
),
Btn(
txt: 'Cancel',
TextButton(
onPressed: Navigator.of(context).pop,
child: const Text('Cancel'),
),
],
),
Expand Down
232 changes: 212 additions & 20 deletions packages/audioplayers/example/lib/tabs/sources.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import 'dart:io';

import 'package:audioplayers/audioplayers.dart';
import 'package:audioplayers_example/components/btn.dart';
import 'package:audioplayers_example/components/drop_down.dart';
import 'package:audioplayers_example/components/tab_content.dart';
import 'package:audioplayers_example/utils.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart' as http;

const useLocalServer = bool.fromEnvironment('USE_LOCAL_SERVER');
Expand Down Expand Up @@ -44,6 +47,8 @@ class _SourcesTabState extends State<SourcesTab>
with AutomaticKeepAliveClientMixin<SourcesTab> {
AudioPlayer get player => widget.player;

final List<Widget> sourceWidgets = [];

Future<void> _setSource(Source source) async {
await player.setSource(source);
toast(
Expand All @@ -61,6 +66,13 @@ class _SourcesTabState extends State<SourcesTab>
);
}

Future<void> _removeSourceWidget(Widget sourceWidget) async {
setState(() {
sourceWidgets.remove(sourceWidget);
});
toast('Source removed.');
}

Widget _createSourceTile({
required String title,
required String subtitle,
Expand All @@ -72,6 +84,7 @@ class _SourcesTabState extends State<SourcesTab>
_SourceTile(
setSource: () => _setSource(source),
play: () => _play(source),
removeSource: _removeSourceWidget,
title: title,
subtitle: subtitle,
setSourceKey: setSourceKey,
Expand All @@ -95,19 +108,11 @@ class _SourcesTabState extends State<SourcesTab>
await fun(BytesSource(bytes));
}

Future<void> _setSourceFilePicker(Future<void> Function(Source) fun) async {
final result = await FilePicker.platform.pickFiles();
final path = result?.files.single.path;
if (path != null) {
_setSource(DeviceFileSource(path));
}
}

@override
Widget build(BuildContext context) {
super.build(context);
return TabContent(
children: [
void initState() {
super.initState();
sourceWidgets.addAll(
[
_createSourceTile(
setSourceKey: const Key('setSource-url-remote-wav-1'),
title: 'Remote URL WAV 1',
Expand Down Expand Up @@ -160,24 +165,18 @@ class _SourcesTabState extends State<SourcesTab>
setSource: () => _setSourceBytesAsset(_setSource, asset: wavAsset),
setSourceKey: const Key('setSource-bytes-local'),
play: () => _setSourceBytesAsset(_play, asset: wavAsset),
removeSource: _removeSourceWidget,
title: 'Bytes - Local',
subtitle: 'laser.wav',
),
_SourceTile(
setSource: () => _setSourceBytesRemote(_setSource, url: mp3Url1),
setSourceKey: const Key('setSource-bytes-remote'),
play: () => _setSourceBytesRemote(_play, url: mp3Url1),
removeSource: _removeSourceWidget,
title: 'Bytes - Remote',
subtitle: 'ambient.mp3',
),
_SourceTile(
setSource: () => _setSourceFilePicker(_setSource),
setSourceKey: const Key('setSource-url-local'),
play: () => _setSourceFilePicker(_play),
title: 'Device File',
subtitle: 'Pick local file from device',
buttonColor: Colors.green,
),
_createSourceTile(
setSourceKey: const Key('setSource-asset-invalid'),
title: 'Invalid Asset',
Expand All @@ -189,13 +188,52 @@ class _SourcesTabState extends State<SourcesTab>
);
}

@override
Widget build(BuildContext context) {
super.build(context);
return Stack(
alignment: Alignment.bottomCenter,
children: [
TabContent(
children: sourceWidgets
.expand((element) => [element, const Divider()])
.toList(),
),
Padding(
padding: const EdgeInsets.all(16),
child: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () {
dialog(
_SourceDialog(
onAdd: (Source source, String path) {
setState(() {
sourceWidgets.add(
_createSourceTile(
title: source.runtimeType.toString(),
subtitle: path,
source: source,
),
);
});
},
),
);
},
),
),
],
);
}

@override
bool get wantKeepAlive => true;
}

class _SourceTile extends StatelessWidget {
final void Function() setSource;
final void Function() play;
final void Function(Widget sourceWidget) removeSource;
final String title;
final String? subtitle;
final Key? setSourceKey;
Expand All @@ -205,6 +243,7 @@ class _SourceTile extends StatelessWidget {
const _SourceTile({
required this.setSource,
required this.play,
required this.removeSource,
required this.title,
this.subtitle,
this.setSourceKey,
Expand Down Expand Up @@ -234,8 +273,161 @@ class _SourceTile extends StatelessWidget {
icon: const Icon(Icons.play_arrow),
color: buttonColor ?? Theme.of(context).primaryColor,
),
IconButton(
tooltip: 'Remove',
onPressed: () => removeSource(this),
icon: const Icon(Icons.delete),
color: buttonColor ?? Theme.of(context).primaryColor,
),
],
),
);
}
}

class _SourceDialog extends StatefulWidget {
final void Function(Source source, String path) onAdd;

const _SourceDialog({required this.onAdd});

@override
State<_SourceDialog> createState() => _SourceDialogState();
}

class _SourceDialogState extends State<_SourceDialog> {
Type sourceType = UrlSource;
String path = '';

final Map<String, String> assetsList = {'': 'Nothing selected'};

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

AssetManifest.loadFromAssetBundle(rootBundle).then((assetManifest) {
setState(() {
assetsList.addAll(
assetManifest
.listAssets()
.map((e) => e.replaceFirst('assets/', ''))
.toList()
.asMap()
.map((key, value) => MapEntry(value, value)),
);
});
});
}

Widget _buildSourceValue() {
switch (sourceType) {
case AssetSource:
return Row(
children: [
const Text('Asset path'),
const SizedBox(width: 16),
Expanded(
child: CustomDropDown<String>(
options: assetsList,
selected: path,
onChange: (value) => setState(() {
path = value ?? '';
}),
),
),
],
);
case BytesSource:
case DeviceFileSource:
return Row(
children: [
const Text('Device File path'),
const SizedBox(width: 16),
Expanded(child: Text(path)),
TextButton.icon(
onPressed: () async {
final result = await FilePicker.platform.pickFiles();
final path = result?.files.single.path;
if (path != null) {
setState(() {
this.path = path;
});
}
},
icon: const Icon(Icons.file_open),
label: const Text('Browse'),
),
],
);
default:
return Row(
children: [
const Text('URL'),
const SizedBox(width: 16),
Expanded(
child: TextField(
decoration: const InputDecoration(
hintText: 'https://example.com/myFile.wav',
),
onChanged: (String? url) => path = url ?? '',
),
),
],
);
}
}

@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
LabeledDropDown<Type>(
label: 'Source type',
options: const {
AssetSource: 'Asset',
DeviceFileSource: 'Device File',
UrlSource: 'Url',
BytesSource: 'Byte array',
},
selected: sourceType,
onChange: (Type? value) {
setState(() {
if (value != null) {
sourceType = value;
}
});
},
),
ListTile(title: _buildSourceValue()),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Btn(
onPressed: () async {
switch (sourceType) {
case BytesSource:
widget.onAdd(
BytesSource(await File(path).readAsBytes()),
path,
);
case AssetSource:
widget.onAdd(AssetSource(path), path);
case DeviceFileSource:
widget.onAdd(DeviceFileSource(path), path);
default:
widget.onAdd(UrlSource(path), path);
}
Navigator.of(context).pop();
},
txt: 'Add',
),
TextButton(
onPressed: Navigator.of(context).pop,
child: const Text('Cancel'),
),
],
),
],
);
}
}

0 comments on commit d374e4d

Please sign in to comment.