mirror of
https://github.com/johrpan/musicus_mobile.git
synced 2025-10-26 18:57:25 +01:00
Move reusable code from mobile to common
This will be useful for a future desktop application.
This commit is contained in:
parent
6e1255f26e
commit
711b19c998
40 changed files with 813 additions and 581 deletions
|
|
@ -1,367 +0,0 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:musicus_database/musicus_database.dart';
|
||||
|
||||
import '../backend.dart';
|
||||
import '../widgets/texts.dart';
|
||||
|
||||
/// A list view supporting pagination and searching.
|
||||
///
|
||||
/// The [fetch] function will be called, when the user has scrolled to the end
|
||||
/// of the list. If you recreate this widget with a new [search] parameter, it
|
||||
/// will update, but it will NOT correctly react to a changed [fetch] method.
|
||||
/// You can call update() on the corresponding state object to manually refresh
|
||||
/// the contents.
|
||||
class PagedListView<T> extends StatefulWidget {
|
||||
/// A search string.
|
||||
///
|
||||
/// This will be provided when calling [fetch].
|
||||
final String search;
|
||||
|
||||
/// Callback for fetching a page of entities.
|
||||
///
|
||||
/// This has to tolerate abitrary high page numbers.
|
||||
final Future<List<T>> Function(int page, String search) fetch;
|
||||
|
||||
/// Build function to be called for each entity.
|
||||
final Widget Function(BuildContext context, T entity) builder;
|
||||
|
||||
PagedListView({
|
||||
Key key,
|
||||
this.search,
|
||||
@required this.fetch,
|
||||
@required this.builder,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
PagedListViewState<T> createState() => PagedListViewState<T>();
|
||||
}
|
||||
|
||||
class PagedListViewState<T> extends State<PagedListView<T>> {
|
||||
final _scrollController = ScrollController();
|
||||
final _entities = <T>[];
|
||||
|
||||
bool loading = true;
|
||||
|
||||
/// The last parameters of _fetch().
|
||||
int _page;
|
||||
String _search;
|
||||
|
||||
/// Whether the last fetch() call returned no results.
|
||||
bool _end = false;
|
||||
|
||||
/// Fetch new entities.
|
||||
///
|
||||
/// If the function was called again with other parameters, while it was
|
||||
/// running, it will discard the result.
|
||||
Future<void> _fetch(int page, String search) async {
|
||||
if (page != _page || search != _search) {
|
||||
_page = page;
|
||||
_search = search;
|
||||
|
||||
setState(() {
|
||||
loading = true;
|
||||
});
|
||||
|
||||
final newEntities = await widget.fetch(page, search);
|
||||
|
||||
if (mounted && search == _search) {
|
||||
setState(() {
|
||||
if (newEntities.isNotEmpty) {
|
||||
_entities.addAll(newEntities);
|
||||
} else {
|
||||
_end = true;
|
||||
}
|
||||
|
||||
loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_scrollController.addListener(() {
|
||||
if (_scrollController.position.pixels >
|
||||
_scrollController.position.maxScrollExtent - 64.0 &&
|
||||
!loading &&
|
||||
!_end) {
|
||||
_fetch(_page + 1, widget.search);
|
||||
}
|
||||
});
|
||||
|
||||
_fetch(0, widget.search);
|
||||
}
|
||||
|
||||
/// Update the content manually.
|
||||
///
|
||||
/// This will reset the current page to zero and call the provided fetch()
|
||||
/// method.
|
||||
void update() {
|
||||
setState(() {
|
||||
_entities.clear();
|
||||
});
|
||||
|
||||
_page = null;
|
||||
_fetch(0, widget.search);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(PagedListView<T> oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
if (oldWidget.search != widget.search) {
|
||||
// We don't nedd to call setState() because the framework will always call
|
||||
// build() after this.
|
||||
_entities.clear();
|
||||
_page = null;
|
||||
_fetch(0, widget.search);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView.builder(
|
||||
controller: _scrollController,
|
||||
itemCount: _entities.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index < _entities.length) {
|
||||
return widget.builder(context, _entities[index]);
|
||||
} else {
|
||||
return SizedBox(
|
||||
height: 64.0,
|
||||
child: Center(
|
||||
child: loading ? CircularProgressIndicator() : Container(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A list of persons.
|
||||
class PersonsList extends StatefulWidget {
|
||||
/// Called, when the user has selected a person.
|
||||
final void Function(Person person) onSelected;
|
||||
|
||||
PersonsList({
|
||||
@required this.onSelected,
|
||||
});
|
||||
|
||||
@override
|
||||
_PersonsListState createState() => _PersonsListState();
|
||||
}
|
||||
|
||||
class _PersonsListState extends State<PersonsList> {
|
||||
String _search;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final backend = Backend.of(context);
|
||||
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Material(
|
||||
elevation: 2.0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 4.0,
|
||||
),
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
onChanged: (text) {
|
||||
setState(() {
|
||||
_search = text;
|
||||
});
|
||||
},
|
||||
decoration: InputDecoration.collapsed(
|
||||
hintText: 'Search by last name...',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: PagedListView<Person>(
|
||||
search: _search,
|
||||
fetch: (page, search) async {
|
||||
return await backend.client.getPersons(page, search);
|
||||
},
|
||||
builder: (context, person) => ListTile(
|
||||
title: Text('${person.lastName}, ${person.firstName}'),
|
||||
onTap: () {
|
||||
widget.onSelected(person);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A list of ensembles.
|
||||
class EnsemblesList extends StatefulWidget {
|
||||
/// Called, when the user has selected an ensemble.
|
||||
final void Function(Ensemble ensemble) onSelected;
|
||||
|
||||
EnsemblesList({
|
||||
@required this.onSelected,
|
||||
});
|
||||
|
||||
@override
|
||||
_EnsemblesListState createState() => _EnsemblesListState();
|
||||
}
|
||||
|
||||
class _EnsemblesListState extends State<EnsemblesList> {
|
||||
String _search;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final backend = Backend.of(context);
|
||||
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Material(
|
||||
elevation: 2.0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 4.0,
|
||||
),
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
onChanged: (text) {
|
||||
setState(() {
|
||||
_search = text;
|
||||
});
|
||||
},
|
||||
decoration: InputDecoration.collapsed(
|
||||
hintText: 'Search by name...',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: PagedListView<Ensemble>(
|
||||
search: _search,
|
||||
fetch: (page, search) async {
|
||||
return await backend.client.getEnsembles(page, search);
|
||||
},
|
||||
builder: (context, ensemble) => ListTile(
|
||||
title: Text(ensemble.name),
|
||||
onTap: () {
|
||||
widget.onSelected(ensemble);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A list of works by one composer.
|
||||
class WorksList extends StatefulWidget {
|
||||
/// The ID of the composer.
|
||||
final int personId;
|
||||
|
||||
/// Called, when the user has selected a work.
|
||||
final void Function(WorkInfo workInfo) onSelected;
|
||||
|
||||
WorksList({
|
||||
this.personId,
|
||||
this.onSelected,
|
||||
});
|
||||
|
||||
@override
|
||||
_WorksListState createState() => _WorksListState();
|
||||
}
|
||||
|
||||
class _WorksListState extends State<WorksList> {
|
||||
String _search;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final backend = Backend.of(context);
|
||||
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Material(
|
||||
elevation: 2.0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 4.0,
|
||||
),
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
onChanged: (text) {
|
||||
setState(() {
|
||||
_search = text;
|
||||
});
|
||||
},
|
||||
decoration: InputDecoration.collapsed(
|
||||
hintText: 'Search by title...',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: PagedListView<WorkInfo>(
|
||||
search: _search,
|
||||
fetch: (page, search) async {
|
||||
return await backend.client
|
||||
.getWorks(widget.personId, page, search);
|
||||
},
|
||||
builder: (context, workInfo) => ListTile(
|
||||
title: Text(workInfo.work.title),
|
||||
onTap: () {
|
||||
widget.onSelected(workInfo);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A list of recordings of a work.
|
||||
class RecordingsList extends StatelessWidget {
|
||||
/// The ID of the work.
|
||||
final int workId;
|
||||
|
||||
/// Called, when the user has selected a recording.
|
||||
final void Function(RecordingInfo recordingInfo) onSelected;
|
||||
|
||||
RecordingsList({
|
||||
this.workId,
|
||||
this.onSelected,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final backend = Backend.of(context);
|
||||
|
||||
return PagedListView<RecordingInfo>(
|
||||
fetch: (page, _) async {
|
||||
return await backend.client.getRecordings(workId, page);
|
||||
},
|
||||
builder: (context, recordingInfo) => ListTile(
|
||||
title: PerformancesText(
|
||||
performanceInfos: recordingInfo.performances,
|
||||
),
|
||||
onTap: () {
|
||||
if (onSelected != null) {
|
||||
onSelected(recordingInfo);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../backend.dart';
|
||||
import 'package:musicus_common/musicus_common.dart';
|
||||
|
||||
class PlayPauseButton extends StatefulWidget {
|
||||
@override
|
||||
|
|
@ -12,7 +11,7 @@ class PlayPauseButton extends StatefulWidget {
|
|||
class _PlayPauseButtonState extends State<PlayPauseButton>
|
||||
with SingleTickerProviderStateMixin {
|
||||
AnimationController playPauseAnimation;
|
||||
BackendState backend;
|
||||
MusicusBackendState backend;
|
||||
StreamSubscription<bool> playingSubscription;
|
||||
|
||||
@override
|
||||
|
|
@ -29,14 +28,14 @@ class _PlayPauseButtonState extends State<PlayPauseButton>
|
|||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
|
||||
backend = Backend.of(context);
|
||||
playPauseAnimation.value = backend.player.playing.value ? 1.0 : 0.0;
|
||||
backend = MusicusBackend.of(context);
|
||||
playPauseAnimation.value = backend.playback.playing.value ? 1.0 : 0.0;
|
||||
|
||||
if (playingSubscription != null) {
|
||||
playingSubscription.cancel();
|
||||
}
|
||||
|
||||
playingSubscription = backend.player.playing.listen((playing) =>
|
||||
playingSubscription = backend.playback.playing.listen((playing) =>
|
||||
playing ? playPauseAnimation.forward() : playPauseAnimation.reverse());
|
||||
}
|
||||
|
||||
|
|
@ -47,7 +46,7 @@ class _PlayPauseButtonState extends State<PlayPauseButton>
|
|||
icon: AnimatedIcons.play_pause,
|
||||
progress: playPauseAnimation,
|
||||
),
|
||||
onPressed: backend.player.playPause,
|
||||
onPressed: backend.playback.playPause,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,15 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:musicus_common/musicus_common.dart';
|
||||
import 'package:musicus_database/musicus_database.dart';
|
||||
|
||||
import '../backend.dart';
|
||||
import '../music_library.dart';
|
||||
import '../screens/program.dart';
|
||||
|
||||
import 'play_pause_button.dart';
|
||||
import 'texts.dart';
|
||||
|
||||
class PlayerBar extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final backend = Backend.of(context);
|
||||
final backend = MusicusBackend.of(context);
|
||||
|
||||
return BottomAppBar(
|
||||
child: InkWell(
|
||||
|
|
@ -19,7 +17,7 @@ class PlayerBar extends StatelessWidget {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
StreamBuilder(
|
||||
stream: backend.player.normalizedPosition,
|
||||
stream: backend.playback.normalizedPosition,
|
||||
builder: (context, snapshot) => LinearProgressIndicator(
|
||||
value: snapshot.data,
|
||||
),
|
||||
|
|
@ -32,7 +30,7 @@ class PlayerBar extends StatelessWidget {
|
|||
),
|
||||
Expanded(
|
||||
child: StreamBuilder<InternalTrack>(
|
||||
stream: backend.player.currentTrack,
|
||||
stream: backend.playback.currentTrack,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.data != null) {
|
||||
final recordingId = snapshot.data.track.recordingId;
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:musicus_database/musicus_database.dart';
|
||||
|
||||
import 'texts.dart';
|
||||
|
||||
class RecordingTile extends StatelessWidget {
|
||||
final WorkInfo workInfo;
|
||||
final RecordingInfo recordingInfo;
|
||||
|
||||
RecordingTile({
|
||||
this.workInfo,
|
||||
this.recordingInfo,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
DefaultTextStyle(
|
||||
style: textTheme.subtitle1,
|
||||
child: Text(workInfo.composers
|
||||
.map((p) => '${p.firstName} ${p.lastName}')
|
||||
.join(', ')),
|
||||
),
|
||||
DefaultTextStyle(
|
||||
style: textTheme.headline6,
|
||||
child: Text(workInfo.work.title),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4.0,
|
||||
),
|
||||
DefaultTextStyle(
|
||||
style: textTheme.bodyText1,
|
||||
child: PerformancesText(
|
||||
performanceInfos: recordingInfo.performances,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:musicus_database/musicus_database.dart';
|
||||
|
||||
import '../backend.dart';
|
||||
|
||||
/// A widget showing information on a list of performances.
|
||||
class PerformancesText extends StatelessWidget {
|
||||
/// The information to show.
|
||||
final List<PerformanceInfo> performanceInfos;
|
||||
|
||||
PerformancesText({
|
||||
this.performanceInfos,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<String> performanceTexts = [];
|
||||
|
||||
for (final p in performanceInfos) {
|
||||
final buffer = StringBuffer();
|
||||
|
||||
if (p.person != null) {
|
||||
buffer.write('${p.person.firstName} ${p.person.lastName}');
|
||||
} else if (p.ensemble != null) {
|
||||
buffer.write(p.ensemble.name);
|
||||
} else {
|
||||
buffer.write('Unknown');
|
||||
}
|
||||
|
||||
if (p.role != null) {
|
||||
buffer.write(' (${p.role.name})');
|
||||
}
|
||||
|
||||
performanceTexts.add(buffer.toString());
|
||||
}
|
||||
|
||||
return Text(performanceTexts.join(', '));
|
||||
}
|
||||
}
|
||||
|
||||
class WorkText extends StatelessWidget {
|
||||
final int workId;
|
||||
|
||||
WorkText(this.workId);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final backend = Backend.of(context);
|
||||
|
||||
return StreamBuilder<Work>(
|
||||
stream: backend.db.workById(workId).watchSingle(),
|
||||
builder: (context, snapshot) => Text(snapshot.data?.title ?? '...'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ComposersText extends StatelessWidget {
|
||||
final int workId;
|
||||
|
||||
ComposersText(this.workId);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final backend = Backend.of(context);
|
||||
|
||||
return StreamBuilder<List<Person>>(
|
||||
stream: backend.db.composersByWork(workId).watch(),
|
||||
builder: (context, snapshot) => Text(snapshot.hasData
|
||||
? snapshot.data.map((p) => '${p.firstName} ${p.lastName}').join(', ')
|
||||
: '...'),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue