Flutter writes a Nas music player (2)
2023-01-11 02:36 ≈ 2.3kWords ≈ 14Minutes

Note

This article was automatically translated using Google

**Shout out! **If you want to do partial refresh through routing, you must write the routing yourself, don’t use the one that comes with flutter, it will make you feel crashed. Today, I switched to the global routing for the local refresh on the right side. After changing most of the code, I found that it was impossible to dynamically refresh while keeping the left and bottom Widget. I rewrote it in a fit of anger and it finally worked. But it wasted a long time. There is no way, in order to adjust the mobile terminal at that time, the left side can be used as a side-sliding window independently, decided to use ValueNotifier to hard-write the route, and then remove the current page highlight on the left side, mainly Although the Detail page can be tracked to the superior, it doesn’t matter if you want it or not. I wrote a huge 40 point headline and put it there, so I can’t miss it

At present, it can be used. The singer, album and all the logic of playing have been completed. There is also the home page after that. It is nothing more than a convenient use such as query point is playing. There are two more important points, search and lyrics. I don’t know if direct transcoding can realize complex and simple search. If not, you need to use API, which will waste query time

1. Album Waterfall

This is what I have always wanted. Since I used Jellyfin, I feel that I have watched movies for nothing before. This kind of TV curtain wall is more handsome. Waterfall has ready-made wheels that can be used [flutter_staggered_grid_view](https://pub.dev /packages/flutter_staggered_grid_view) is very simple to implement, just add it in pubspec.yaml and then flutter pub get

It itself supports a variety of presentation methods, you can go to the homepage for details, but I chose MasonryGridView, which is most suitable for a large number of pictures. It is the same as building a ListView. There are three attributes that need to be defined here.

crossAxisCount: How many columns

mainAxisSpacing: line spacing

crossAxisSpacing: Column spacing

I suggest doing a calculation, because if you fix Count, it will cause a problem. Dragging the form will make the picture very large, which is very strange

1
2
3
4
5
6
//Get the window size
Size _size = MediaQuery.of(context).size;
//The right side is the part I want to display, first calculate the width, and then divide it by the width of the image you want to present
double _rightWidth = (_size. width - 160) / 180;
// then round down
int _count = _rightWidth.truncate();

In this way, how do you drag the form? The width of the picture will only change in a small range, it will not be particularly exaggerated, and the construction is also very simple.

1
2
3
4
5
6
7
8
MasonryGridView.count(
crossAxisCount: _count,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
itemCount: _albums!.length,
itemBuilder: (context, index) {
return...
}

You can seal a combination of Widget inside, such as adding two lines of text to a picture, and the effect will come out

Of course, you can do image caching. I wanted to do it at first, but I just didn’t know whether to save it in a folder or convert it to base64 and save it in the database.

In fact, it’s okay to not do caching, and the effect of lazy loading of pictures is still possible. Use frameBuilder to achieve the fade-in effect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ClipRRect(
borderRadius: BorderRadius.circular(15),
child: Image.network(
_temURL,
fit: BoxFit. cover,
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
if (wasSynchronouslyLoaded) {
return child;
}
return AnimatedSwitcher(
child: frame != null
? child
: Image.asset("assets/images/logo.jpg"),
duration: const Duration(milliseconds: 2000),
);
},
),
)

2. Music playback

Playing a song is very simple, just setAudioSource, the trouble is the playlist, and the button operation to control the playlist. For example, the previous song is gone, but if the button is not grayed out, an error will be reported. All controls are performed using ValueNotifier. I understand it.

2.1. Set monitoring variables

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//Monitor the current resource ID. This is not only used for music playback, but also for obtaining singer pictures.
ValueNotifier<String> activeSongValue = ValueNotifier<String>("1");

//Listen to the current playlist
ValueNotifier<List> activeList = ValueNotifier<List>([]);

//Listen to the current song sequence
ValueNotifier<int> activeIndex = ValueNotifier<int>(0);

//Monitor the current song, similar to the above, just pass it to save a database query, because you need to fill in the song name, singer name and album name, otherwise you don’t need to pass it
ValueNotifier<Map> activeSong = ValueNotifier<Map>(Map());

//Monitor whether out of order, this is to monitor the out of order button for triggering
ValueNotifier<bool> isShuffleModeEnabledNotifier = ValueNotifier<bool>(false);

//Monitor whether it is the first song
ValueNotifier<bool> isFirstSongNotifier = ValueNotifier<bool>(true);

//Monitor whether it is the last song, this is to block the button of the previous song and the next song to avoid error reporting
ValueNotifier<bool> isLastSongNotifier = ValueNotifier<bool>(true);

2.2. Triggering

After clicking the song in the song list, pass the id, song List and the index of the current song in the list, and then seal a Map

The main purpose is to get the picture URL of the song. This is not in the original Map. You need to go to the background to create a URL with the server address and identity information. Otherwise, it is more convenient to use the Songs instance directly.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
onTap: () async {
activeSongValue.value = _tem.id;
//List of album songs where the song is located
activeList.value = _songs!;
// current song queue
activeIndex. value = index;
// Assemble the current song
Map_activeSong = new Map();
String _url = await getCoverArt(_tem.id);
_activeSong["artist"] = _tem.artist;
_activeSong["url"] = _url;
_activeSong["title"] = _tem.title;
_activeSong["album"] = _tem.album;
activeSong.value = _activeSong;
}

2.3. Receive and generate playlist

What is received here is activeSongValue, why not use activeList directly, because if you receive activeList, click other songs in the current list, and its default value will not change

1
2
3
4
5
6
7
ValueListenableBuilder<String>(
valueListenable: activeSongValue,
builder: ((context, value, child) {
if (value != "1") {
setAudioSource();
}
return...

If it is the default value 1, it means that you have not selected music

When the value changes, start generating the playlist

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Future<void> setAudioSource() async {
//1. Declaration
final playlist = ConcatenatingAudioSource(
useLazyPreparation: true,
shuffleOrder: DefaultShuffleOrder(),
children: await _getPlayList(),
);
//3. Add a playlist, by the way, set which song is currently playing
await _player.setAudioSource(playlist,
initialIndex: activeIndex.value, initialPosition: Duration.zero);
//4. Start playing
_player.play();
}

_getPlayList() async {
List<AudioSource> children = [];
List_songs = activeList. value;
Take out the value passed when the trigger is triggered
for (var element in _songs) {
Songs_song = element;
//2. Go to the database to spell the address of the music file
String _url = await getSongStreamUrl(_song.id);
children.add(AudioSource.uri(Uri.parse(_url), tag: _song.id)); //Pay attention to setting the tag here, which can be used for later monitoring
}
return children;
}

2.4. Monitor playback changes

When playing the next song, you need to replace the information of the song name and cover. At this time, you need to inject monitoring to operate

Just Audio has a number of different event streams, all of which can be listened to:

  • sequenceStream: This is the playlist in the order it was added, whenever the playlist changes, a new list will be generated here
  • shuffleModeEnabledStream: ShuffleModeEnabledStream listener, open and close this mode, is a Boolean value
  • shuffleIndicesStream: This is a list of shuffle integers pointing to the sequenceStream of items in the playlist (it does not shuffle itself). shuffle When this method is called on the audio player, a new list will be generated here.
  • currentIndexStream: This stream notifies the index of the current song when the song first starts. This integer points to an audio source sequenceStream in the playlist
  • loopModeStream:By default all songs in the list are played at once. However, you can also choose to repeat a single song or even a playlist, which is the loop mode, LoopMode This stream will generate a new value every time the loop mode changes

Well, just look at the above. For monitoring, you can do it with one, that is, sequenceStateStream. Here, as long as one of the above five streams generates a new value, sequenceStateStream will generate a combined value type SequenceState, one top five, happy

First add this method call in initState, then..

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
void _listenForChangesInSequenceState() {
//when
//Triggered when playing to the next song
_player.sequenceStateStream.listen((sequenceState) async {
//Roll when there is nothing
if (sequenceState == null) return;

// Update the information of the currently playing song
final currentItem = sequenceState. currentSource;
//Get the id put in the tag when generating the playlist
final_title = currentItem?.tag as String?;
//Go to the database to check the information to get the song information
final _tem = await getSong(_title. toString());
//Update the current song id
activeSongValue.value = _title.toString();

// Assemble the current song
Map_activeSong = new Map();
String _url = await getCoverArt(_tem["id"]);
_activeSong["artist"] = _tem["artist"];
_activeSong["url"] = _url;
_activeSong["title"] = _tem["title"];
_activeSong["album"] = _tem["album"];
activeSong.value = _activeSong;

// The out-of-order monitoring here is useless for me, I wrote one myself (mainly I wrote the out-of-order logic first, and then made the playlist, I am too lazy to change it)
// isShuffleModeEnabledNotifier.value = sequenceState.shuffleModeEnabled;

// get the playlist
final playlist = sequenceState. effectiveSequence;
//Judge the previous song and the next song, and then pass the value to the button for status judgment
if (playlist.isEmpty || currentItem == null) {
isFirstSongNotifier. value = true;
isLastSongNotifier. value = true;
} else {
isFirstSongNotifier.value = playlist.first == currentItem;
isLastSongNotifier.value = playlist.last == currentItem;
}
});
}

2.5. Fast Forward and Fast Rewind

In fact, many people have canceled these two buttons now, because there is a progress bar, that is so convenient, but I have written the buttons before, so I will write them, it is a pity to delete them

1
2
3
4
5
6
7
8
9
10
11
12
IconButton(
icon: const Icon(
Icons. fast_rewind,
color: kTextColor,
),
onPressed: () {
if (widget. player. position. inSeconds - 15 > 0) {
widget.player.seek(
Duration(seconds: widget. player. position. inSeconds - 15));
}
},
),

Here you need to make a judgment. If your current position minus the position you want to cancel is less than 0, don’t operate to avoid error reporting. It doesn’t matter if you move forward.

2.6. Head up and down

Here you need to monitor the previously set values of isFirstSongNotifier and isLastSongNotifier. If you don’t filter, click on the first song when playing the first song, and click on the next song when you are playing the last song. there will be problems

1
2
3
4
5
6
7
8
9
10
11
12
13
ValueListenableBuilder<bool>(
valueListenable: isLastSongNotifier,
builder: (_, isLast, __) {
return IconButton(
icon: Icon(
Icons. skip_next,
color: isLast ? badgeDark : kTextColor,
),
onPressed: () {
(isLast) ? null : widget. player. seekToNext();
},
);
}),

Make a judgment, if there is a problem, just gray the button, and then pass a null in onPressed

2.7. Single loop, all loop, random play, stop, pause

The difficulty lies in the above. After everything is written, the button part is relatively simple

Single loop:player.setLoopMode(LoopMode.one);

**Loop all: ** player.setLoopMode(LoopMode.all);

Play in random order: player.setShuffleModeEnabled(true);

3. Monospace font

Since numbers are added before and after the progress bar of the song, an old-fashioned problem arises, that is, when the number goes from 1 to 0, the progress bar behind will be displaced, so the numbers here need to use monospaced fonts.

I used two fonts for the whole APP, one is NotoSansSC, which supports Chinese, Japanese, Korean and English, and it is very happy to use. It was dragged from the backup font of Jellyfin I set at that time. At that time, it was also used to display Chinese, Japanese, Korean and English subtitles, and it can be used here as well.

However, there is a problem with this font. It has two versions. The normal version can display both numbers and English, but the Mono version, which is a monospaced font, will display English and numbers in half-width, and it will become thinner. It looks like The head is very big, so I found a small monospaced font from Google Fonts, which is specially used for the number in front of the progress bar to avoid displacement

  1. Declare Asset

    Still declared in pubspec.yaml

1
2
3
4
5
6
7
8
9
10
# To add assets to your application, add an assets section, like this:
assets:
- assets/images/
fonts:
- family: NotoSansSC
fonts:
- asset: assets/fonts/NotoSansSC-Regular.otf
- family: ChivoMono
fonts:
- asset: assets/fonts/ChivoMono-Regular.ttf
  1. Topical use
1
2
3
4
5
Text(
formatSongDuration(widget. position),
style: TextStyle(
color: borderColor, fontFamily: 'ChivoMono', fontSize: 12),
)
  1. Global use
1
2
3
4
5
6
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'XiuMusic',
theme: ThemeData(fontFamily: 'NotoSansSC'),
home: MainScreen(),
);

4. Todo List

I have been playing with this thing for a week. When I am under a lot of pressure, writing code really relieves pressure, so I am not in a hurry to finish it, take it slowly

Todo List was updated directly on Github

In fact, it is already available, and those who are interested can come down and play, XiuMusic