Compare commits

...

3 Commits

Author SHA1 Message Date
futpib
b5c3482a6d
Merge c2039d14fd into 69a310c78f 2026-06-05 09:34:15 -04:00
Kingkor Roy Tirtho
69a310c78f
Release 5.1.2+45 (#3049)
* chore: fix white screen issue

* fix(newpipe): fallback to muxed streams if no audio stream is available

* fix: dismiss search dropdown and keyboard on submission

Co-authored-by: Tobse <1190109+TobseF@users.noreply.github.com>

* fix: custom image helper null exception

* chore: generate changelog and bump version for release

---------

Co-authored-by: tomasalias <pasek.domi@post.cz>
Co-authored-by: Akshat-104 <akkusuryan@gmail.com>
Co-authored-by: Tobse <1190109+TobseF@users.noreply.github.com>
2026-06-05 10:49:09 +06:00
futpib
c2039d14fd fix: hide followers count when null instead of showing Infinity
The MusicBrainz metadata plugin doesn't provide follower counts, so
artist.followers is always null. Previously this caused "Infinity Followers"
to be displayed. Now the followers line is hidden when the data is unavailable.
2026-01-25 12:32:25 +00:00
9 changed files with 1449 additions and 646 deletions

3
.fvmrc
View File

@ -1,4 +1,3 @@
{ {
"flutter": "3.35.2", "flutter": "3.35.2"
"flavors": {}
} }

View File

@ -30,5 +30,6 @@
"README.md": "LICENSE,CODE_OF_CONDUCT.md,CONTRIBUTING.md,SECURITY.md,CONTRIBUTION.md,CHANGELOG.md,PRIVACY_POLICY.md", "README.md": "LICENSE,CODE_OF_CONDUCT.md,CONTRIBUTING.md,SECURITY.md,CONTRIBUTION.md,CHANGELOG.md,PRIVACY_POLICY.md",
"*.dart": "${capture}.g.dart,${capture}.freezed.dart" "*.dart": "${capture}.g.dart,${capture}.freezed.dart"
}, },
"dart.flutterSdkPath": ".fvm/versions/3.35.2" "dart.flutterSdkPath": ".fvm/versions/3.35.2",
"makefile.configureOnOpen": false
} }

File diff suppressed because it is too large Load Diff

View File

@ -33,7 +33,8 @@ extension SpotubeImageExtensions on List<SpotubeImageObject>? {
int index = 1, int index = 1,
required ImagePlaceholder placeholder, required ImagePlaceholder placeholder,
}) { }) {
final sortedImage = this?.sorted((a, b) => a.width!.compareTo(b.width!)); final sortedImage =
this?.sorted((a, b) => (a.width ?? 0).compareTo(b.width ?? 0));
return sortedImage != null && sortedImage.isNotEmpty return sortedImage != null && sortedImage.isNotEmpty
? sortedImage[ ? sortedImage[

View File

@ -192,20 +192,19 @@ class ArtistPageHeader extends HookConsumerWidget {
), ),
), ),
const Gap(5), const Gap(5),
Flexible( if (artist.followers != null)
child: AutoSizeText( Flexible(
context.l10n.followers( child: AutoSizeText(
artist.followers == null context.l10n.followers(
? double.infinity PrimitiveUtils.toReadableNumber(
: PrimitiveUtils.toReadableNumber( artist.followers!.toDouble(),
artist.followers!.toDouble(), ),
), ),
), maxLines: 1,
maxLines: 1, overflow: TextOverflow.ellipsis,
overflow: TextOverflow.ellipsis, minFontSize: 12,
minFontSize: 12, ).muted(),
).muted(), ),
),
if (constrains.mdAndUp) ...[ if (constrains.mdAndUp) ...[
const Gap(20), const Gap(20),
actions, actions,

View File

@ -59,6 +59,7 @@ class SearchPage extends HookConsumerWidget {
void onSubmitted(String value) { void onSubmitted(String value) {
ref.read(searchTermStateProvider.notifier).state = value; ref.read(searchTermStateProvider.notifier).state = value;
focusNode.unfocus();
if (value.trim().isEmpty) { if (value.trim().isEmpty) {
return; return;
} }
@ -127,61 +128,47 @@ class SearchPage extends HookConsumerWidget {
) )
.toList(); .toList();
return KeyboardListener( return AutoComplete(
focusNode: focusNode, suggestions: suggestions.length <= 2
autofocus: true, ? [
onKeyEvent: (value) { ...suggestions,
final isEnter = value.logicalKey == "Twenty One Pilots",
LogicalKeyboardKey.enter; "Linkin Park",
]
if (isEnter) { : suggestions,
onSubmitted(controller.text); completer: (suggestion) => suggestion,
focusNode.unfocus(); mode: AutoCompleteMode.replaceAll,
} child: TextField(
}, autofocus: true,
child: AutoComplete( controller: controller,
suggestions: suggestions.length <= 2 focusNode: focusNode,
? [ features: [
...suggestions, const InputFeature.leading(
"Twenty One Pilots", Icon(SpotubeIcons.search),
"Linkin Park", ),
"d4vd" InputFeature.trailing(
] AnimatedCrossFade(
: suggestions, duration:
completer: (suggestion) => suggestion, const Duration(milliseconds: 300),
mode: AutoCompleteMode.replaceAll, crossFadeState:
child: TextField( controller.text.isNotEmpty
autofocus: true, ? CrossFadeState.showFirst
controller: controller, : CrossFadeState.showSecond,
features: [ firstChild: IconButton.ghost(
const InputFeature.leading( size: ButtonSize.small,
Icon(SpotubeIcons.search), icon: const Icon(SpotubeIcons.close),
), onPressed: () {
InputFeature.trailing( controller.clear();
AnimatedCrossFade( },
duration:
const Duration(milliseconds: 300),
crossFadeState:
controller.text.isNotEmpty
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
firstChild: IconButton.ghost(
size: ButtonSize.small,
icon:
const Icon(SpotubeIcons.close),
onPressed: () {
controller.clear();
},
),
secondChild: const SizedBox.square(
dimension: 28),
), ),
) secondChild: const SizedBox.square(
], dimension: 28),
textInputAction: TextInputAction.search, ),
placeholder: Text(context.l10n.search), )
onSubmitted: onSubmitted, ],
), textInputAction: TextInputAction.search,
placeholder: Text(context.l10n.search),
onSubmitted: onSubmitted,
), ),
); );
}), }),

View File

@ -28,6 +28,26 @@ class NewPipeEngine implements YouTubeEngine {
); );
} }
AudioOnlyStreamInfo _parseVideoStream(VideoStream stream, String videoId) {
return AudioOnlyStreamInfo(
VideoId(videoId),
stream.itag,
Uri.parse(stream.content),
StreamContainer.parse(stream.mediaFormat!.mimeType.split("/").last),
FileSize.unknown,
Bitrate(stream.bitrate),
stream.codec,
switch (stream.bitrate) {
> 130 * 1024 => "high",
> 64 * 1024 => "medium",
_ => "low",
},
[],
MediaType.parse(stream.mediaFormat!.mimeType),
null,
);
}
Video _parseVideo(VideoInfo info) { Video _parseVideo(VideoInfo info) {
return Video( return Video(
VideoId(info.id), VideoId(info.id),
@ -76,6 +96,14 @@ class NewPipeEngine implements YouTubeEngine {
final streams = final streams =
video.audioStreams.map((stream) => _parseAudioStream(stream, videoId)); video.audioStreams.map((stream) => _parseAudioStream(stream, videoId));
if (streams.isEmpty) {
final videoStreams = video.videoStreams
.map((stream) => _parseVideoStream(stream, videoId));
if (videoStreams.isNotEmpty) {
return StreamManifest(videoStreams);
}
}
return StreamManifest(streams); return StreamManifest(streams);
} }
@ -93,6 +121,14 @@ class NewPipeEngine implements YouTubeEngine {
final streams = final streams =
video.audioStreams.map((stream) => _parseAudioStream(stream, videoId)); video.audioStreams.map((stream) => _parseAudioStream(stream, videoId));
if (streams.isEmpty) {
final videoStreams = video.videoStreams
.map((stream) => _parseVideoStream(stream, videoId));
if (videoStreams.isNotEmpty) {
return (_parseVideo(video), StreamManifest(videoStreams));
}
}
return (_parseVideo(video), StreamManifest(streams)); return (_parseVideo(video), StreamManifest(streams));
} }

View File

@ -486,10 +486,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: data_widget name: data_widget
sha256: "95388df890189014f702b7e93f9de6bcf7d45143a99f6288f31899f10be441ba" sha256: "4947aae3c50635496d56f94ad18de98e19015c5ebf01abee0f39a2c098c7021a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.0.2" version: "0.0.3"
dbus: dbus:
dependency: transitive dependency: transitive
description: description:
@ -947,7 +947,7 @@ packages:
description: description:
path: "." path: "."
ref: HEAD ref: HEAD
resolved-ref: ab3ff415114b7b43593e6ee718ad3d760af18350 resolved-ref: "41cbd821ce49b9343136f67d83d3117e582dad75"
url: "https://github.com/KRTirtho/flutter_new_pipe_extractor" url: "https://github.com/KRTirtho/flutter_new_pipe_extractor"
source: git source: git
version: "0.1.0" version: "0.1.0"
@ -2728,10 +2728,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: youtube_explode_dart name: youtube_explode_dart
sha256: "3d731d71df9901b1915bae806781df519cff32517e36db279f844ae619669e45" sha256: "10134a53989b2f3dc576121735aec8fc6d88784956f4a2ad1a2525b006373a76"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.5" version: "3.1.0"
yt_dlp_dart: yt_dlp_dart:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -3,7 +3,7 @@ description: Open source extensible music streaming platform and app, based on B
publish_to: "none" publish_to: "none"
version: 5.1.1+44 version: 5.1.2+45
homepage: https://spotube.krtirtho.dev homepage: https://spotube.krtirtho.dev
repository: https://github.com/KRTirtho/spotube repository: https://github.com/KRTirtho/spotube
@ -90,7 +90,7 @@ dependencies:
ref: dart-3-support ref: dart-3-support
url: https://github.com/KRTirtho/scrobblenaut.git url: https://github.com/KRTirtho/scrobblenaut.git
scroll_to_index: ^3.0.1 scroll_to_index: ^3.0.1
shadcn_flutter: ^0.0.47 shadcn_flutter: 0.0.47
shared_preferences: ^2.2.3 shared_preferences: ^2.2.3
shelf: ^1.4.1 shelf: ^1.4.1
shelf_router: ^1.1.4 shelf_router: ^1.1.4
@ -114,7 +114,7 @@ dependencies:
wikipedia_api: ^0.1.0 wikipedia_api: ^0.1.0
win32_registry: ^1.1.5 win32_registry: ^1.1.5
window_manager: ^0.4.3 window_manager: ^0.4.3
youtube_explode_dart: ^3.0.5 youtube_explode_dart: ^3.1.0
yt_dlp_dart: yt_dlp_dart:
git: git:
url: https://github.com/KRTirtho/yt_dlp_dart.git url: https://github.com/KRTirtho/yt_dlp_dart.git
@ -171,7 +171,7 @@ dev_dependencies:
pub_api_client: ^3.0.0 pub_api_client: ^3.0.0
io: ^1.0.4 io: ^1.0.4
drift_dev: ^2.21.0 drift_dev: ^2.21.0
test: ^1.25.7 test: any
auto_route_generator: ^9.0.0 auto_route_generator: ^9.0.0
dependency_overrides: dependency_overrides: