first commit
Build / Build for Android (push) Has been cancelled
Build / Build for iOS (push) Has been cancelled

This commit is contained in:
Zakaria
2026-05-18 14:15:38 -04:00
commit 1563409cb1
382 changed files with 45347 additions and 0 deletions
@@ -0,0 +1,40 @@
import 'package:audio_service/audio_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:get_it/get_it.dart';
import '../../models/jellyfin_models.dart';
import '../../services/music_player_background_task.dart';
import '../../screens/add_to_playlist_screen.dart';
class AddToPlaylistButton extends StatelessWidget {
const AddToPlaylistButton({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final audioHandler = GetIt.instance<MusicPlayerBackgroundTask>();
return StreamBuilder<MediaItem?>(
stream: audioHandler.mediaItem,
builder: (context, snapshot) {
if (snapshot.hasData) {
return IconButton(
onPressed: () => Navigator.of(context).pushReplacementNamed(
AddToPlaylistScreen.routeName,
arguments:
BaseItemDto.fromJson(snapshot.data!.extras!["itemJson"])
.id),
icon: const Icon(Icons.playlist_add),
tooltip: AppLocalizations.of(context)!.addToPlaylistTooltip,
);
} else {
return IconButton(
icon: const Icon(Icons.playlist_add),
onPressed: null,
tooltip: AppLocalizations.of(context)!.addToPlaylistTooltip,
);
}
},
);
}
}
@@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:audio_service/audio_service.dart';
import 'package:get_it/get_it.dart';
import '../../services/music_player_background_task.dart';
class PlaybackMode extends StatelessWidget {
const PlaybackMode({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final audioHandler = GetIt.instance<MusicPlayerBackgroundTask>();
return StreamBuilder<MediaItem?>(
stream: audioHandler.mediaItem,
builder: (context, snapshot) {
if (snapshot.hasData) {
late String onlineOrOffline;
late String transcodeOrDirect;
if (snapshot.data!.extras!["downloadedSongJson"] == null) {
onlineOrOffline = AppLocalizations.of(context)!.streaming;
} else {
onlineOrOffline = AppLocalizations.of(context)!.downloaded;
}
if (snapshot.data!.extras!["shouldTranscode"] &&
snapshot.data!.extras!["downloadedSongJson"] == null) {
transcodeOrDirect = AppLocalizations.of(context)!.transcode;
} else {
transcodeOrDirect = AppLocalizations.of(context)!.direct;
}
return Text(
"$onlineOrOffline\n$transcodeOrDirect",
style: Theme.of(context).textTheme.bodySmall,
);
} else if (snapshot.hasError) {
return Text(
AppLocalizations.of(context)!.statusError,
style: Theme.of(context).textTheme.bodySmall,
);
} else {
return Text(
AppLocalizations.of(context)!.noItem.toUpperCase(),
style: Theme.of(context).textTheme.bodySmall,
);
}
},
);
}
}
@@ -0,0 +1,146 @@
import 'package:audio_service/audio_service.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../../services/music_player_background_task.dart';
class PlayerButtons extends StatelessWidget {
const PlayerButtons({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final audioHandler = GetIt.instance<MusicPlayerBackgroundTask>();
return StreamBuilder<PlaybackState>(
stream: audioHandler.playbackState,
builder: (context, snapshot) {
final PlaybackState? playbackState = snapshot.data;
return Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
textDirection: TextDirection.ltr,
children: [
IconButton(
tooltip: playbackState?.shuffleMode == AudioServiceShuffleMode.all
? AppLocalizations.of(context)!.playbackOrderShuffledTooltip
: AppLocalizations.of(context)!.playbackOrderLinearTooltip,
icon: _getShufflingIcon(
playbackState == null
? AudioServiceShuffleMode.none
: playbackState.shuffleMode,
Theme.of(context).colorScheme.secondary,
),
onPressed: playbackState != null
? () async {
if (playbackState.shuffleMode ==
AudioServiceShuffleMode.all) {
await audioHandler
.setShuffleMode(AudioServiceShuffleMode.none);
} else {
await audioHandler
.setShuffleMode(AudioServiceShuffleMode.all);
}
}
: null,
iconSize: 20,
),
IconButton(
tooltip: AppLocalizations.of(context)!.skipToPrevious,
icon: const Icon(Icons.skip_previous),
onPressed: playbackState != null
? () async => await audioHandler.skipToPrevious()
: null,
iconSize: 36,
),
SizedBox(
height: 56,
width: 56,
child: FloatingActionButton(
tooltip: AppLocalizations.of(context)!.togglePlayback,
// We set a heroTag because otherwise the play button on AlbumScreenContent will do hero widget stuff
heroTag: "PlayerScreenFAB",
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
splashColor:
Theme.of(context).colorScheme.onPrimary.withOpacity(0.24),
onPressed: playbackState != null
? () async {
if (playbackState.playing) {
await audioHandler.pause();
} else {
await audioHandler.play();
}
}
: null,
child: Icon(
playbackState == null || playbackState.playing
? Icons.pause
: Icons.play_arrow,
size: 36,
),
),
),
IconButton(
tooltip: AppLocalizations.of(context)!.skipToNext,
icon: const Icon(Icons.skip_next),
onPressed: playbackState != null
? () async => audioHandler.skipToNext()
: null,
iconSize: 36),
IconButton(
tooltip: playbackState?.repeatMode == AudioServiceRepeatMode.all
? AppLocalizations.of(context)!.loopModeAllTooltip
: playbackState?.repeatMode == AudioServiceRepeatMode.one
? AppLocalizations.of(context)!.loopModeOneTooltip
: AppLocalizations.of(context)!.loopModeNoneTooltip,
icon: _getRepeatingIcon(
playbackState == null
? AudioServiceRepeatMode.none
: playbackState.repeatMode,
Theme.of(context).colorScheme.secondary,
),
onPressed: playbackState != null
? () async {
// Cyles from none -> all -> one
if (playbackState.repeatMode ==
AudioServiceRepeatMode.none) {
await audioHandler
.setRepeatMode(AudioServiceRepeatMode.all);
} else if (playbackState.repeatMode ==
AudioServiceRepeatMode.all) {
await audioHandler
.setRepeatMode(AudioServiceRepeatMode.one);
} else {
await audioHandler
.setRepeatMode(AudioServiceRepeatMode.none);
}
}
: null,
iconSize: 20,
),
],
);
},
);
}
Widget _getRepeatingIcon(
AudioServiceRepeatMode repeatMode, Color iconColour) {
if (repeatMode == AudioServiceRepeatMode.all) {
return Icon(Icons.repeat, color: iconColour);
} else if (repeatMode == AudioServiceRepeatMode.one) {
return Icon(Icons.repeat_one, color: iconColour);
} else {
return const Icon(Icons.repeat);
}
}
Icon _getShufflingIcon(
AudioServiceShuffleMode shuffleMode, Color iconColour) {
if (shuffleMode == AudioServiceShuffleMode.all) {
return Icon(Icons.shuffle, color: iconColour);
} else {
return const Icon(Icons.shuffle);
}
}
}
@@ -0,0 +1,288 @@
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import '../../services/music_player_background_task.dart';
import '../../services/progress_state_stream.dart';
import '../print_duration.dart';
class ProgressSlider extends StatefulWidget {
const ProgressSlider({
Key? key,
this.allowSeeking = true,
this.showBuffer = true,
this.showDuration = true,
this.showPlaceholder = true,
}) : super(key: key);
final bool allowSeeking;
final bool showBuffer;
final bool showDuration;
final bool showPlaceholder;
@override
State<ProgressSlider> createState() => _ProgressSliderState();
}
class _ProgressSliderState extends State<ProgressSlider> {
/// Value used to hold the slider's value when dragging.
double? _dragValue;
late SliderThemeData _sliderThemeData;
final _audioHandler = GetIt.instance<MusicPlayerBackgroundTask>();
@override
void didChangeDependencies() {
super.didChangeDependencies();
_sliderThemeData = SliderTheme.of(context).copyWith(
trackHeight: 4.0,
inactiveTrackColor:
Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5),
);
}
@override
Widget build(BuildContext context) {
// The slider always needs to be LTR, so we use Directionality to save
// putting TextDirection.ltr all over the place
return Directionality(
textDirection: TextDirection.ltr,
// The slider can refresh up to 60 times per second, so we wrap it in a
// RepaintBoundary to avoid more areas being repainted than necessary
child: RepaintBoundary(
child: StreamBuilder<ProgressState>(
stream: progressStateStream,
builder: (context, snapshot) {
if (snapshot.data?.mediaItem == null) {
// If nothing is playing or the AudioService isn't connected, return a
// greyed out slider with some fake numbers. We also do this if
// currentPosition is null, which sometimes happens when the app is
// closed and reopened.
return widget.showPlaceholder
? Column(
children: [
SliderTheme(
data: _sliderThemeData.copyWith(
trackShape: CustomTrackShape(),
),
child: const Slider(
value: 0,
max: 1,
onChanged: null,
),
),
if (widget.showDuration)
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"00:00",
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
color: Theme.of(context)
.textTheme
.bodySmall
?.color),
),
Text(
"00:00",
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
color: Theme.of(context)
.textTheme
.bodySmall
?.color),
),
],
),
],
)
: const SizedBox.shrink();
} else if (snapshot.hasData) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// Slider displaying playback and buffering progress.
SliderTheme(
data: widget.allowSeeking
? _sliderThemeData.copyWith(
trackShape: CustomTrackShape(),
)
: _sliderThemeData.copyWith(
thumbShape: const RoundSliderThumbShape(
enabledThumbRadius: 0),
// gets rid of both horizontal and vertical padding
overlayShape:
const RoundSliderOverlayShape(overlayRadius: 0),
trackShape: const RectangularSliderTrackShape(),
),
child: Slider(
min: 0.0,
max: snapshot.data!.mediaItem?.duration == null
? snapshot.data!.playbackState.bufferedPosition
.inMicroseconds
.toDouble()
: snapshot.data!.mediaItem!.duration!.inMicroseconds
.toDouble(),
value: (_dragValue ??
snapshot.data!.position.inMicroseconds)
.clamp(
0,
snapshot.data!.mediaItem!.duration!.inMicroseconds
.toDouble())
.toDouble(),
secondaryTrackValue: widget.showBuffer &&
snapshot.data!.mediaItem
?.extras?["downloadedSongJson"] ==
null
? snapshot.data!.playbackState.bufferedPosition
.inMicroseconds
.clamp(
0.0,
snapshot.data!.mediaItem!.duration == null
? snapshot.data!.playbackState
.bufferedPosition.inMicroseconds
: snapshot.data!.mediaItem!.duration!
.inMicroseconds,
)
.toDouble()
: 0,
onChanged: widget.allowSeeking
? (newValue) async {
// We don't actually tell audio_service to seek here
// because it would get flooded with seek requests
setState(() {
_dragValue = newValue;
});
}
: (_) {},
onChangeStart: widget.allowSeeking
? (value) {
setState(() {
_dragValue = value;
});
}
: (_) {},
onChangeEnd: widget.allowSeeking
? (newValue) async {
// Seek to the new position
await _audioHandler.seek(
Duration(microseconds: newValue.toInt()));
// Clear drag value so that the slider uses the play
// duration again.
setState(() {
_dragValue = null;
});
}
: (_) {},
),
),
if (widget.showDuration)
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
printDuration(
Duration(
microseconds: _dragValue?.toInt() ??
snapshot.data!.position.inMicroseconds),
),
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
color: Theme.of(context)
.textTheme
.bodySmall
?.color),
),
Text(
printDuration(snapshot.data!.mediaItem?.duration),
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
color: Theme.of(context)
.textTheme
.bodySmall
?.color),
),
],
),
],
);
} else {
return const Text(
"Snapshot doesn't have data and MediaItem isn't null and AudioService is connected?");
}
},
),
),
);
}
}
class PositionData {
final Duration position;
final Duration bufferedPosition;
PositionData(this.position, this.bufferedPosition);
}
/// Track shape used to remove horizontal padding.
/// https://github.com/flutter/flutter/issues/37057
class CustomTrackShape extends RoundedRectSliderTrackShape {
@override
Rect getPreferredRect({
required RenderBox parentBox,
Offset offset = Offset.zero,
required SliderThemeData sliderTheme,
bool isEnabled = false,
bool isDiscrete = false,
}) {
final double trackHeight = sliderTheme.trackHeight!;
final double trackLeft = offset.dx;
final double trackTop =
offset.dy + (parentBox.size.height - trackHeight) / 2;
final double trackWidth = parentBox.size.width;
return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
}
/// Disable additionalActiveTrackHeight
@override
void paint(
PaintingContext context,
Offset offset, {
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required Animation<double> enableAnimation,
required TextDirection textDirection,
required Offset thumbCenter,
Offset? secondaryOffset,
bool isDiscrete = false,
bool isEnabled = false,
double additionalActiveTrackHeight = 0,
}) {
super.paint(
context,
offset,
parentBox: parentBox,
sliderTheme: sliderTheme,
enableAnimation: enableAnimation,
textDirection: textDirection,
thumbCenter: thumbCenter,
secondaryOffset: secondaryOffset,
isDiscrete: isDiscrete,
isEnabled: isEnabled,
additionalActiveTrackHeight: additionalActiveTrackHeight,
);
}
}
@@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'queue_list.dart';
class QueueButton extends StatelessWidget {
const QueueButton({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return IconButton(
icon: const Icon(Icons.queue_music),
tooltip: AppLocalizations.of(context)!.queue,
onPressed: () {
showModalBottomSheet(
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24.0)),
),
context: context,
builder: (context) {
return DraggableScrollableSheet(
expand: false,
builder: (context, scrollController) {
return QueueList(
scrollController: scrollController,
);
},
);
},
);
});
}
}
+115
View File
@@ -0,0 +1,115 @@
import 'package:audio_service/audio_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:get_it/get_it.dart';
import 'package:rxdart/rxdart.dart';
import '../../services/finamp_settings_helper.dart';
import '../album_image.dart';
import '../../models/jellyfin_models.dart';
import '../../services/process_artist.dart';
import '../../services/media_state_stream.dart';
import '../../services/music_player_background_task.dart';
class _QueueListStreamState {
_QueueListStreamState(
this.queue,
this.mediaState,
);
final List<MediaItem>? queue;
final MediaState mediaState;
}
class QueueList extends StatefulWidget {
const QueueList({Key? key, required this.scrollController}) : super(key: key);
final ScrollController scrollController;
@override
State<QueueList> createState() => _QueueListState();
}
class _QueueListState extends State<QueueList> {
final _audioHandler = GetIt.instance<MusicPlayerBackgroundTask>();
List<MediaItem>? _queue;
@override
Widget build(BuildContext context) {
return StreamBuilder<_QueueListStreamState>(
// stream: AudioService.queueStream,
stream: Rx.combineLatest2<List<MediaItem>?, MediaState,
_QueueListStreamState>(_audioHandler.queue, mediaStateStream,
(a, b) => _QueueListStreamState(a, b)),
builder: (context, snapshot) {
if (snapshot.hasData) {
_queue ??= snapshot.data!.queue;
return PrimaryScrollController(
controller: widget.scrollController,
child: ReorderableListView.builder(
itemCount: snapshot.data!.queue?.length ?? 0,
onReorder: (oldIndex, newIndex) async {
setState(() {
// _queue?.insert(newIndex, _queue![oldIndex]);
// _queue?.removeAt(oldIndex);
int? smallerThanNewIndex;
if (oldIndex < newIndex) {
// When we're moving an item backwards, we need to reduce
// newIndex by 1 to account for there being a new item added
// before newIndex.
smallerThanNewIndex = newIndex - 1;
}
final item = _queue?.removeAt(oldIndex);
_queue?.insert(smallerThanNewIndex ?? newIndex, item!);
});
await _audioHandler.reorderQueue(oldIndex, newIndex);
},
itemBuilder: (context, index) {
final actualIndex =
_audioHandler.playbackState.valueOrNull?.shuffleMode ==
AudioServiceShuffleMode.all
? _audioHandler.shuffleIndices![index]
: index;
return Dismissible(
key: ValueKey(snapshot.data!.queue![actualIndex].id),
direction: FinampSettingsHelper.finampSettings.disableGesture
? DismissDirection.none
: DismissDirection.horizontal,
onDismissed: (direction) async {
await _audioHandler.removeQueueItemAt(actualIndex);
},
child: ListTile(
leading: AlbumImage(
item: snapshot.data!.queue?[actualIndex]
.extras?["itemJson"] ==
null
? null
: BaseItemDto.fromJson(snapshot
.data!.queue?[actualIndex].extras?["itemJson"]),
),
title: Text(
snapshot.data!.queue?[actualIndex].title ??
AppLocalizations.of(context)!.unknownName,
style: snapshot.data!.mediaState.mediaItem ==
snapshot.data!.queue?[actualIndex]
? TextStyle(
color: Theme.of(context).colorScheme.secondary)
: null),
subtitle: Text(processArtist(
snapshot.data!.queue?[actualIndex].artist, context)),
onTap: () async =>
await _audioHandler.skipToIndex(actualIndex),
),
);
},
),
);
} else {
return const Center(
child: CircularProgressIndicator.adaptive(),
);
}
},
);
}
}
@@ -0,0 +1,42 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:get_it/get_it.dart';
import '../../services/music_player_background_task.dart';
import 'sleep_timer_dialog.dart';
import 'sleep_timer_cancel_dialog.dart';
class SleepTimerButton extends StatelessWidget {
const SleepTimerButton({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final audioHandler = GetIt.instance<MusicPlayerBackgroundTask>();
return ValueListenableBuilder<Timer?>(
valueListenable: audioHandler.sleepTimer,
builder: (context, value, child) {
return IconButton(
icon: value == null
? const Icon(Icons.mode_night_outlined)
: const Icon(Icons.mode_night),
tooltip: AppLocalizations.of(context)!.sleepTimerTooltip,
onPressed: () async {
if (value != null) {
showDialog(
context: context,
builder: (context) => const SleepTimerCancelDialog(),
);
} else {
await showDialog(
context: context,
builder: (context) => const SleepTimerDialog(),
);
}
});
},
);
}
}
@@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:get_it/get_it.dart';
import '../../services/music_player_background_task.dart';
class SleepTimerCancelDialog extends StatelessWidget {
const SleepTimerCancelDialog({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final audioHandler = GetIt.instance<MusicPlayerBackgroundTask>();
return AlertDialog(
title: Text(AppLocalizations.of(context)!.cancelSleepTimer),
actions: [
TextButton(
child: Text(AppLocalizations.of(context)!.noButtonLabel),
onPressed: () => Navigator.of(context).pop(),
),
TextButton(
child: Text(AppLocalizations.of(context)!.yesButtonLabel),
onPressed: () {
audioHandler.clearSleepTimer();
Navigator.of(context).pop();
},
)
],
);
}
}
@@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:get_it/get_it.dart';
import '../../services/music_player_background_task.dart';
import '../../services/finamp_settings_helper.dart';
class SleepTimerDialog extends StatefulWidget {
const SleepTimerDialog({Key? key}) : super(key: key);
@override
State<SleepTimerDialog> createState() => _SleepTimerDialogState();
}
class _SleepTimerDialogState extends State<SleepTimerDialog> {
final _audioHandler = GetIt.instance<MusicPlayerBackgroundTask>();
final _textController = TextEditingController(
text: (FinampSettingsHelper.finampSettings.sleepTimerSeconds ~/ 60)
.toString());
final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(AppLocalizations.of(context)!.setSleepTimer),
content: Form(
key: _formKey,
child: Row(
children: [
Expanded(
child: TextFormField(
controller: _textController,
keyboardType: TextInputType.number,
textAlign: TextAlign.center,
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.minutes),
validator: (value) {
if (value == null || value.isEmpty) {
return AppLocalizations.of(context)!.required;
}
if (int.tryParse(value) == null) {
return AppLocalizations.of(context)!.invalidNumber;
}
return null;
},
onSaved: (value) {
final valueInt = int.parse(value!);
_audioHandler.setSleepTimer(Duration(minutes: valueInt));
FinampSettingsHelper.setSleepTimerSeconds(valueInt * 60);
},
),
),
],
),
),
actions: [
TextButton(
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
onPressed: () => Navigator.of(context).pop(),
),
TextButton(
child: Text(MaterialLocalizations.of(context).okButtonLabel),
onPressed: () {
if (_formKey.currentState?.validate() == true) {
_formKey.currentState!.save();
Navigator.of(context).pop();
}
},
)
],
);
}
}
+160
View File
@@ -0,0 +1,160 @@
import 'package:audio_service/audio_service.dart';
import 'package:finamp/models/jellyfin_models.dart';
import 'package:finamp/screens/artist_screen.dart';
import 'package:finamp/services/finamp_settings_helper.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:get_it/get_it.dart';
import '../../screens/album_screen.dart';
import '../../services/jellyfin_api_helper.dart';
import '../../services/music_player_background_task.dart';
import '../artists_text_spans.dart';
/// Creates some text that shows the song's name, album and the artist.
class SongName extends StatelessWidget {
const SongName({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final audioHandler = GetIt.instance<MusicPlayerBackgroundTask>();
final JellyfinApiHelper jellyfinApiHelper =
GetIt.instance<JellyfinApiHelper>();
final textColour =
Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.6);
return StreamBuilder<MediaItem?>(
stream: audioHandler.mediaItem,
builder: (context, snapshot) {
if (snapshot.hasData) {
final MediaItem mediaItem = snapshot.data!;
BaseItemDto songBaseItemDto =
BaseItemDto.fromJson(mediaItem.extras!["itemJson"]);
List<TextSpan> separatedArtistTextSpans = [];
if (songBaseItemDto.artistItems?.isEmpty ?? true) {
separatedArtistTextSpans = [
TextSpan(
text: AppLocalizations.of(context)!.unknownArtist,
style: TextStyle(color: textColour),
)
];
} else {
songBaseItemDto.artistItems
?.map((e) => TextSpan(
text: e.name,
style: TextStyle(color: textColour),
recognizer: TapGestureRecognizer()
..onTap = () {
// Offline artists aren't implemented yet so we return if
// offline
if (FinampSettingsHelper.finampSettings.isOffline) {
return;
}
jellyfinApiHelper.getItemById(e.id).then((artist) =>
Navigator.of(context).popAndPushNamed(
ArtistScreen.routeName,
arguments: artist));
}))
.forEach((artistTextSpan) {
separatedArtistTextSpans.add(artistTextSpan);
separatedArtistTextSpans.add(TextSpan(
text: ", ",
style: TextStyle(color: textColour),
));
});
separatedArtistTextSpans.removeLast();
}
return SongNameContent(
songBaseItemDto: songBaseItemDto,
mediaItem: mediaItem,
separatedArtistTextSpans: ArtistsTextSpans(
songBaseItemDto,
textColour,
context,
true,
));
}
return const SongNameContent(
songBaseItemDto: null,
mediaItem: null,
separatedArtistTextSpans: [],
);
},
);
}
}
class SongNameContent extends StatelessWidget {
const SongNameContent({
Key? key,
required this.songBaseItemDto,
required this.mediaItem,
required this.separatedArtistTextSpans,
}) : super(key: key);
final BaseItemDto? songBaseItemDto;
final MediaItem? mediaItem;
final List<TextSpan> separatedArtistTextSpans;
@override
Widget build(BuildContext context) {
final jellyfinApiHelper = GetIt.instance<JellyfinApiHelper>();
final textColour =
Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.6);
return Padding(
// I don't know why but 12 is the magic number that lines up with the
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: songBaseItemDto == null
? null
: () => jellyfinApiHelper
.getItemById(songBaseItemDto!.albumId as String)
.then((album) => Navigator.of(context).popAndPushNamed(
AlbumScreen.routeName,
arguments: album)),
child: Text(
mediaItem == null
? AppLocalizations.of(context)!.noAlbum
: mediaItem!.album ?? AppLocalizations.of(context)!.noAlbum,
style: TextStyle(color: textColour),
),
),
const Padding(padding: EdgeInsets.symmetric(vertical: 2)),
Text(
mediaItem == null
? AppLocalizations.of(context)!.noItem
: mediaItem!.title,
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w600),
overflow: TextOverflow.fade,
softWrap: false,
maxLines: 1,
),
const Padding(padding: EdgeInsets.symmetric(vertical: 2)),
RichText(
text: TextSpan(
children: mediaItem == null || mediaItem!.artist == null
? [
TextSpan(
text: AppLocalizations.of(context)!.noArtist,
style: TextStyle(color: textColour),
)
]
: separatedArtistTextSpans,
),
),
],
),
);
}
}