Compare commits
5 Commits
463cb9870f
...
1.0.0+12
| Author | SHA1 | Date | |
|---|---|---|---|
| 3df93e47d2 | |||
| 6d2a027d9b | |||
| 222d50d80d | |||
| 499bca5b1c | |||
| 252e4619f7 |
@@ -1,59 +1,61 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<true/>
|
<false/>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<true/>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>Groovy Box</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>Groovy Box</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>6.0</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>Groovy Box</string>
|
<string>6.0</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundleName</key>
|
||||||
<string>APPL</string>
|
<string>Groovy Box</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>????</string>
|
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
<string>????</string>
|
||||||
<key>LSApplicationCategoryType</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>public.app-category.music</string>
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSApplicationCategoryType</key>
|
||||||
<true/>
|
<string>public.app-category.music</string>
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<string>To provide information for RhythmBox to normalize the output audio</string>
|
<true/>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
<true/>
|
<string>To provide information for RhythmBox to normalize the output audio</string>
|
||||||
<key>UIBackgroundModes</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<array>
|
<true/>
|
||||||
<string>audio</string>
|
<key>UIBackgroundModes</key>
|
||||||
</array>
|
<array>
|
||||||
<key>UILaunchStoryboardName</key>
|
<string>audio</string>
|
||||||
<string>LaunchScreen</string>
|
</array>
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>Main</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIStatusBarHidden</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
<false/>
|
<string>Main</string>
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UIStatusBarHidden</key>
|
||||||
<array>
|
<false/>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<array>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
</array>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
<array>
|
</array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
<array>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
</array>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
</dict>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -82,34 +82,38 @@ class _PlayerScreenState extends State<PlayerScreen> {
|
|||||||
padding: const EdgeInsets.symmetric(vertical: 24),
|
padding: const EdgeInsets.symmetric(vertical: 24),
|
||||||
children: [
|
children: [
|
||||||
Obx(
|
Obx(
|
||||||
() => LimitedBox(
|
() => Center(
|
||||||
maxHeight: maxAlbumSize,
|
child: LimitedBox(
|
||||||
maxWidth: maxAlbumSize,
|
maxHeight: maxAlbumSize,
|
||||||
child: Hero(
|
maxWidth: maxAlbumSize,
|
||||||
tag: const Key('current-active-track-album-art'),
|
child: Hero(
|
||||||
child: AspectRatio(
|
tag: const Key('current-active-track-album-art'),
|
||||||
aspectRatio: 1,
|
child: AspectRatio(
|
||||||
child: ClipRRect(
|
aspectRatio: 1,
|
||||||
borderRadius:
|
child: ClipRRect(
|
||||||
const BorderRadius.all(Radius.circular(16)),
|
borderRadius: const BorderRadius.all(
|
||||||
child: _albumArt != null
|
Radius.circular(16),
|
||||||
? AutoCacheImage(
|
),
|
||||||
_albumArt!,
|
child: _albumArt != null
|
||||||
width: albumSize,
|
? AutoCacheImage(
|
||||||
height: albumSize,
|
_albumArt!,
|
||||||
)
|
width: albumSize,
|
||||||
: Container(
|
height: albumSize,
|
||||||
color: Theme.of(context)
|
fit: BoxFit.cover,
|
||||||
.colorScheme
|
)
|
||||||
.surfaceContainerHigh,
|
: Container(
|
||||||
width: 64,
|
color: Theme.of(context)
|
||||||
height: 64,
|
.colorScheme
|
||||||
child: const Center(
|
.surfaceContainerHigh,
|
||||||
child: Icon(Icons.image),
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
child: const Center(
|
||||||
|
child: Icon(Icons.image),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
).marginSymmetric(horizontal: 24),
|
||||||
).marginSymmetric(horizontal: 24),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -309,87 +313,89 @@ class _PlayerScreenState extends State<PlayerScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Gap(20),
|
const Gap(20),
|
||||||
SizedBox(
|
Center(
|
||||||
height: 40,
|
child: SizedBox(
|
||||||
child: ListView(
|
height: 40,
|
||||||
scrollDirection: Axis.horizontal,
|
child: ListView(
|
||||||
shrinkWrap: true,
|
scrollDirection: Axis.horizontal,
|
||||||
children: [
|
shrinkWrap: true,
|
||||||
TextButton.icon(
|
children: [
|
||||||
icon: const Icon(Icons.queue_music),
|
|
||||||
label: const Text(
|
|
||||||
'Queue',
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
showModalBottomSheet(
|
|
||||||
useRootNavigator: true,
|
|
||||||
isScrollControlled: true,
|
|
||||||
context: context,
|
|
||||||
builder: (context) =>
|
|
||||||
const PlayerQueuePopup(),
|
|
||||||
).then((_) {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (!isLargeScreen) const Gap(4),
|
|
||||||
if (!isLargeScreen)
|
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
icon: const Icon(Icons.lyrics),
|
icon: const Icon(Icons.queue_music),
|
||||||
label: const Text(
|
label: const Text(
|
||||||
'Lyrics',
|
'Queue',
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.fade,
|
overflow: TextOverflow.fade,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
GoRouter.of(context)
|
showModalBottomSheet(
|
||||||
.pushNamed('playerLyrics');
|
useRootNavigator: true,
|
||||||
|
isScrollControlled: true,
|
||||||
|
context: context,
|
||||||
|
builder: (context) =>
|
||||||
|
const PlayerQueuePopup(),
|
||||||
|
).then((_) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Gap(4),
|
if (!isLargeScreen) const Gap(4),
|
||||||
TextButton.icon(
|
if (!isLargeScreen)
|
||||||
icon: const Icon(Icons.merge),
|
TextButton.icon(
|
||||||
label: const Text(
|
icon: const Icon(Icons.lyrics),
|
||||||
'Sources',
|
label: const Text(
|
||||||
maxLines: 1,
|
'Lyrics',
|
||||||
overflow: TextOverflow.fade,
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
GoRouter.of(context)
|
||||||
|
.pushNamed('playerLyrics');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
TextButton.icon(
|
||||||
|
icon: const Icon(Icons.merge),
|
||||||
|
label: const Text(
|
||||||
|
'Sources',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
showModalBottomSheet(
|
||||||
|
useRootNavigator: true,
|
||||||
|
isScrollControlled: true,
|
||||||
|
context: context,
|
||||||
|
builder: (context) =>
|
||||||
|
const SiblingTracksPopup(),
|
||||||
|
).then((_) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
onPressed: () {
|
const Gap(4),
|
||||||
showModalBottomSheet(
|
TextButton.icon(
|
||||||
useRootNavigator: true,
|
label: const Text('Info'),
|
||||||
isScrollControlled: true,
|
icon: const Icon(Icons.info),
|
||||||
context: context,
|
onPressed: () {
|
||||||
builder: (context) =>
|
showModalBottomSheet(
|
||||||
const SiblingTracksPopup(),
|
useRootNavigator: true,
|
||||||
).then((_) {
|
context: context,
|
||||||
if (mounted) {
|
builder: (context) =>
|
||||||
setState(() {});
|
const SourceDetailsPopup(),
|
||||||
}
|
).then((_) {
|
||||||
});
|
if (mounted) {
|
||||||
},
|
setState(() {});
|
||||||
),
|
}
|
||||||
const Gap(4),
|
});
|
||||||
TextButton.icon(
|
},
|
||||||
label: const Text('Info'),
|
),
|
||||||
icon: const Icon(Icons.info),
|
],
|
||||||
onPressed: () {
|
),
|
||||||
showModalBottomSheet(
|
|
||||||
useRootNavigator: true,
|
|
||||||
context: context,
|
|
||||||
builder: (context) =>
|
|
||||||
const SourceDetailsPopup(),
|
|
||||||
).then((_) {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ enum CloseBehavior {
|
|||||||
enum AudioSource {
|
enum AudioSource {
|
||||||
youtube,
|
youtube,
|
||||||
piped,
|
piped,
|
||||||
netease;
|
netease,
|
||||||
|
kugou;
|
||||||
|
|
||||||
String get label => name[0].toUpperCase() + name.substring(1);
|
String get label => name[0].toUpperCase() + name.substring(1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ part of '../database.dart';
|
|||||||
enum SourceType {
|
enum SourceType {
|
||||||
youtube._('YouTube'),
|
youtube._('YouTube'),
|
||||||
youtubeMusic._('YouTube Music'),
|
youtubeMusic._('YouTube Music'),
|
||||||
netease._('Netease Music');
|
netease._('Netease Music'),
|
||||||
|
kugou._('Kugou Music');
|
||||||
|
|
||||||
final String label;
|
final String label;
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:dio/dio.dart' hide Response;
|
import 'package:dio/dio.dart' hide Response;
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:get/get.dart' hide Response;
|
import 'package:get/get.dart' hide Response;
|
||||||
@@ -6,6 +8,7 @@ import 'package:rhythm_box/providers/error_notifier.dart';
|
|||||||
import 'package:rhythm_box/services/audio_player/audio_player.dart';
|
import 'package:rhythm_box/services/audio_player/audio_player.dart';
|
||||||
import 'package:rhythm_box/services/server/active_sourced_track.dart';
|
import 'package:rhythm_box/services/server/active_sourced_track.dart';
|
||||||
import 'package:rhythm_box/services/server/sourced_track.dart';
|
import 'package:rhythm_box/services/server/sourced_track.dart';
|
||||||
|
import 'package:rhythm_box/services/sourced_track/sources/kugou.dart';
|
||||||
import 'package:rhythm_box/services/sourced_track/sources/netease.dart';
|
import 'package:rhythm_box/services/sourced_track/sources/netease.dart';
|
||||||
import 'package:shelf/shelf.dart';
|
import 'package:shelf/shelf.dart';
|
||||||
|
|
||||||
@@ -34,6 +37,13 @@ class ServerPlaybackRoutesProvider {
|
|||||||
);
|
);
|
||||||
final realUrl = resp.body['data'][0]['url'];
|
final realUrl = resp.body['data'][0]['url'];
|
||||||
url = realUrl;
|
url = realUrl;
|
||||||
|
} else if (sourcedTrack is KugouSourcedTrack) {
|
||||||
|
// Special processing for kugou to get real assets url
|
||||||
|
final resp = await GetConnect(timeout: const Duration(seconds: 30))
|
||||||
|
.get(sourcedTrack.url);
|
||||||
|
final realUrl =
|
||||||
|
KugouSourcedTrack.unescapeUrl(jsonDecode(resp.body)['url'][0]);
|
||||||
|
url = realUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
final res = await Dio().get(
|
final res = await Dio().get(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:get/get.dart';
|
|||||||
import 'package:rhythm_box/providers/error_notifier.dart';
|
import 'package:rhythm_box/providers/error_notifier.dart';
|
||||||
import 'package:rhythm_box/providers/user_preferences.dart';
|
import 'package:rhythm_box/providers/user_preferences.dart';
|
||||||
import 'package:rhythm_box/services/database/database.dart';
|
import 'package:rhythm_box/services/database/database.dart';
|
||||||
|
import 'package:rhythm_box/services/sourced_track/sources/kugou.dart';
|
||||||
import 'package:rhythm_box/services/sourced_track/sources/netease.dart';
|
import 'package:rhythm_box/services/sourced_track/sources/netease.dart';
|
||||||
import 'package:rhythm_box/services/utils.dart';
|
import 'package:rhythm_box/services/utils.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
@@ -104,18 +105,23 @@ abstract class SourcedTrack extends Track {
|
|||||||
return switch (audioSource) {
|
return switch (audioSource) {
|
||||||
AudioSource.netease =>
|
AudioSource.netease =>
|
||||||
await NeteaseSourcedTrack.fetchFromTrack(track: track),
|
await NeteaseSourcedTrack.fetchFromTrack(track: track),
|
||||||
|
AudioSource.kugou =>
|
||||||
|
await KugouSourcedTrack.fetchFromTrack(track: track),
|
||||||
AudioSource.piped =>
|
AudioSource.piped =>
|
||||||
await PipedSourcedTrack.fetchFromTrack(track: track),
|
await PipedSourcedTrack.fetchFromTrack(track: track),
|
||||||
_ => await YoutubeSourcedTrack.fetchFromTrack(track: track),
|
_ => await YoutubeSourcedTrack.fetchFromTrack(track: track),
|
||||||
};
|
};
|
||||||
} on TrackNotFoundError catch (err) {
|
} on TrackNotFoundError catch (err) {
|
||||||
Get.find<ErrorNotifier>()
|
Get.find<ErrorNotifier>().showError(
|
||||||
.showError('${err.toString()} via ${preferences.audioSource.label}');
|
'${err.toString()} via ${preferences.audioSource.label}, querying in fallback sources...',
|
||||||
|
);
|
||||||
return switch (preferences.audioSource) {
|
return switch (preferences.audioSource) {
|
||||||
AudioSource.piped ||
|
AudioSource.piped ||
|
||||||
AudioSource.youtube =>
|
AudioSource.youtube =>
|
||||||
await NeteaseSourcedTrack.fetchFromTrack(track: track),
|
await NeteaseSourcedTrack.fetchFromTrack(track: track),
|
||||||
AudioSource.netease =>
|
AudioSource.netease =>
|
||||||
|
await KugouSourcedTrack.fetchFromTrack(track: track),
|
||||||
|
AudioSource.kugou =>
|
||||||
await YoutubeSourcedTrack.fetchFromTrack(track: track),
|
await YoutubeSourcedTrack.fetchFromTrack(track: track),
|
||||||
};
|
};
|
||||||
} on HttpClientClosedException catch (_) {
|
} on HttpClientClosedException catch (_) {
|
||||||
|
|||||||
229
lib/services/sourced_track/sources/kugou.dart
Normal file
229
lib/services/sourced_track/sources/kugou.dart
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:crypto/crypto.dart';
|
||||||
|
import 'package:get/get.dart' hide Value;
|
||||||
|
import 'package:rhythm_box/providers/database.dart';
|
||||||
|
import 'package:rhythm_box/services/database/database.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:rhythm_box/services/sourced_track/enums.dart';
|
||||||
|
import 'package:rhythm_box/services/sourced_track/exceptions.dart';
|
||||||
|
import 'package:rhythm_box/services/sourced_track/models/source_info.dart';
|
||||||
|
import 'package:rhythm_box/services/sourced_track/models/source_map.dart';
|
||||||
|
import 'package:rhythm_box/services/sourced_track/sourced_track.dart';
|
||||||
|
|
||||||
|
class KugouSourceInfo extends SourceInfo {
|
||||||
|
KugouSourceInfo({
|
||||||
|
required super.id,
|
||||||
|
required super.title,
|
||||||
|
required super.artist,
|
||||||
|
required super.thumbnail,
|
||||||
|
required super.pageUrl,
|
||||||
|
required super.duration,
|
||||||
|
required super.artistUrl,
|
||||||
|
required super.album,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class KugouSourcedTrack extends SourcedTrack {
|
||||||
|
KugouSourcedTrack({
|
||||||
|
required super.source,
|
||||||
|
required super.siblings,
|
||||||
|
required super.sourceInfo,
|
||||||
|
required super.track,
|
||||||
|
});
|
||||||
|
|
||||||
|
static String unescapeUrl(String src) {
|
||||||
|
return src.replaceAll('\\/', '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getBaseUrl() {
|
||||||
|
return 'http://mobilecdn.kugou.com';
|
||||||
|
}
|
||||||
|
|
||||||
|
static GetConnect getClient() {
|
||||||
|
final client = GetConnect(
|
||||||
|
withCredentials: true,
|
||||||
|
timeout: const Duration(seconds: 30),
|
||||||
|
);
|
||||||
|
client.baseUrl = getBaseUrl();
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<KugouSourcedTrack> fetchFromTrack({
|
||||||
|
required Track track,
|
||||||
|
}) async {
|
||||||
|
final DatabaseProvider db = Get.find();
|
||||||
|
final cachedSource = await (db.database.select(db.database.sourceMatchTable)
|
||||||
|
..where((s) => s.trackId.equals(track.id!))
|
||||||
|
..limit(1)
|
||||||
|
..orderBy([
|
||||||
|
(s) =>
|
||||||
|
OrderingTerm(expression: s.createdAt, mode: OrderingMode.desc),
|
||||||
|
]))
|
||||||
|
.get()
|
||||||
|
.then((s) => s.firstOrNull);
|
||||||
|
|
||||||
|
if (cachedSource == null || cachedSource.sourceType != SourceType.kugou) {
|
||||||
|
final siblings = await fetchSiblings(track: track);
|
||||||
|
if (siblings.isEmpty) {
|
||||||
|
throw TrackNotFoundError(track);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.database.into(db.database.sourceMatchTable).insert(
|
||||||
|
SourceMatchTableCompanion.insert(
|
||||||
|
trackId: track.id!,
|
||||||
|
sourceId: siblings.first.info.id,
|
||||||
|
sourceType: const Value(SourceType.kugou),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return KugouSourcedTrack(
|
||||||
|
siblings: siblings.map((s) => s.info).skip(1).toList(),
|
||||||
|
source: siblings.first.source as SourceMap,
|
||||||
|
sourceInfo: siblings.first.info,
|
||||||
|
track: track,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return KugouSourcedTrack(
|
||||||
|
siblings: [],
|
||||||
|
source: toSourceMap(cachedSource),
|
||||||
|
sourceInfo: KugouSourceInfo(
|
||||||
|
id: cachedSource.sourceId,
|
||||||
|
artist: 'unknown',
|
||||||
|
artistUrl: '#',
|
||||||
|
pageUrl: '#',
|
||||||
|
thumbnail: '#',
|
||||||
|
title: 'unknown',
|
||||||
|
duration: Duration.zero,
|
||||||
|
album: 'unknown',
|
||||||
|
),
|
||||||
|
track: track,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static SourceMap toSourceMap(dynamic manifest) {
|
||||||
|
const baseUrl = 'http://trackercdn.kugou.com/i/v2';
|
||||||
|
|
||||||
|
final hash = manifest is SourceMatchTableData
|
||||||
|
? manifest.sourceId
|
||||||
|
: manifest?['hash'];
|
||||||
|
final key = md5.convert(utf8.encode('${hash}kgcloudv2')).toString();
|
||||||
|
final url =
|
||||||
|
'$baseUrl/song/url?key=$key&hash=$hash&appid=1005&pid=2&cmd=25&behavior=play';
|
||||||
|
|
||||||
|
return SourceMap(
|
||||||
|
m4a: SourceQualityMap(
|
||||||
|
high: url,
|
||||||
|
medium: url,
|
||||||
|
low: url,
|
||||||
|
),
|
||||||
|
weba: SourceQualityMap(
|
||||||
|
high: url,
|
||||||
|
medium: url,
|
||||||
|
low: url,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<List<SiblingType>> fetchSiblings({
|
||||||
|
required Track track,
|
||||||
|
}) async {
|
||||||
|
final query = SourcedTrack.getSearchTerm(track);
|
||||||
|
|
||||||
|
final client = getClient();
|
||||||
|
final resp = await client.get(
|
||||||
|
'/api/v3/search/song?keyword=${Uri.encodeComponent(query)}&page=1&pagesize=10',
|
||||||
|
);
|
||||||
|
final results = jsonDecode(resp.body)['data']['info'];
|
||||||
|
|
||||||
|
// We can just trust kugou music for now
|
||||||
|
// If we need to check is the result correct, refer to this code
|
||||||
|
// https://github.com/KRTirtho/spotube/blob/9b024120601c0d381edeab4460cb22f87149d0f8/lib/services/sourced_track/sources/jiosaavn.dart#L129
|
||||||
|
final matchedResults = results.map(toSiblingType).toList();
|
||||||
|
|
||||||
|
return matchedResults.cast<SiblingType>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<KugouSourcedTrack> copyWithSibling() async {
|
||||||
|
if (siblings.isNotEmpty) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
final fetchedSiblings = await fetchSiblings(track: this);
|
||||||
|
|
||||||
|
return KugouSourcedTrack(
|
||||||
|
siblings: fetchedSiblings
|
||||||
|
.where((s) => s.info.id != sourceInfo.id)
|
||||||
|
.map((s) => s.info)
|
||||||
|
.toList(),
|
||||||
|
source: source,
|
||||||
|
sourceInfo: sourceInfo,
|
||||||
|
track: this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<KugouSourcedTrack?> swapWithSibling(SourceInfo sibling) async {
|
||||||
|
if (sibling.id == sourceInfo.id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// a sibling source that was fetched from the search results
|
||||||
|
final isStepSibling = siblings.none((s) => s.id == sibling.id);
|
||||||
|
|
||||||
|
final newSourceInfo = isStepSibling
|
||||||
|
? sibling
|
||||||
|
: siblings.firstWhere((s) => s.id == sibling.id);
|
||||||
|
final newSiblings = siblings.where((s) => s.id != sibling.id).toList()
|
||||||
|
..insert(0, sourceInfo);
|
||||||
|
|
||||||
|
final info = newSourceInfo as KugouSourceInfo;
|
||||||
|
final source = toSourceMap(newSourceInfo.id);
|
||||||
|
|
||||||
|
final db = Get.find<DatabaseProvider>();
|
||||||
|
await db.database.into(db.database.sourceMatchTable).insert(
|
||||||
|
SourceMatchTableCompanion.insert(
|
||||||
|
trackId: id!,
|
||||||
|
sourceId: info.id,
|
||||||
|
sourceType: const Value(SourceType.kugou),
|
||||||
|
// Because we're sorting by createdAt in the query
|
||||||
|
// we have to update it to indicate priority
|
||||||
|
createdAt: Value(DateTime.now()),
|
||||||
|
),
|
||||||
|
mode: InsertMode.replace,
|
||||||
|
);
|
||||||
|
|
||||||
|
return KugouSourcedTrack(
|
||||||
|
siblings: newSiblings,
|
||||||
|
source: source,
|
||||||
|
sourceInfo: info,
|
||||||
|
track: this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static KugouSourceInfo toSourceInfo(dynamic item) {
|
||||||
|
return KugouSourceInfo(
|
||||||
|
id: item['hash'],
|
||||||
|
artist: item['singername'],
|
||||||
|
artistUrl: '#',
|
||||||
|
pageUrl: '#',
|
||||||
|
thumbnail: unescapeUrl(item['trans_param']['union_cover'])
|
||||||
|
.replaceFirst('/{size}', ''),
|
||||||
|
title: item['songname'],
|
||||||
|
duration: Duration(seconds: item['duration']),
|
||||||
|
album: item['album_name'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static SiblingType toSiblingType(dynamic item) {
|
||||||
|
final SiblingType sibling = (
|
||||||
|
info: toSourceInfo(item),
|
||||||
|
source: toSourceMap(item),
|
||||||
|
);
|
||||||
|
|
||||||
|
return sibling;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -93,6 +93,12 @@ class NeteaseSourcedTrack extends SourcedTrack {
|
|||||||
throw TrackNotFoundError(track);
|
throw TrackNotFoundError(track);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final client = getClient();
|
||||||
|
final checkResp = await client.get(
|
||||||
|
'/check/music?id=${siblings.first.info.id}&realIP=${await lookupRealIp()}',
|
||||||
|
);
|
||||||
|
if (checkResp.body['success'] != true) throw TrackNotFoundError(track);
|
||||||
|
|
||||||
await db.database.into(db.database.sourceMatchTable).insert(
|
await db.database.into(db.database.sourceMatchTable).insert(
|
||||||
SourceMatchTableCompanion.insert(
|
SourceMatchTableCompanion.insert(
|
||||||
trackId: track.id!,
|
trackId: track.id!,
|
||||||
@@ -111,8 +117,14 @@ class NeteaseSourcedTrack extends SourcedTrack {
|
|||||||
|
|
||||||
final client = getClient();
|
final client = getClient();
|
||||||
final resp = await client.get('/song/detail?ids=${cachedSource.sourceId}');
|
final resp = await client.get('/song/detail?ids=${cachedSource.sourceId}');
|
||||||
print(resp.body);
|
final item = (resp.body['songs'] as List<dynamic>).firstOrNull;
|
||||||
final item = resp.body['songs'][0];
|
|
||||||
|
if (item == null) throw TrackNotFoundError(track);
|
||||||
|
|
||||||
|
final checkResp = await client.get(
|
||||||
|
'/check/music?id=${item['id']}&realIP=${await lookupRealIp()}',
|
||||||
|
);
|
||||||
|
if (checkResp.body['success'] != true) throw TrackNotFoundError(track);
|
||||||
|
|
||||||
return NeteaseSourcedTrack(
|
return NeteaseSourcedTrack(
|
||||||
siblings: [],
|
siblings: [],
|
||||||
@@ -155,8 +167,10 @@ class NeteaseSourcedTrack extends SourcedTrack {
|
|||||||
final query = SourcedTrack.getSearchTerm(track);
|
final query = SourcedTrack.getSearchTerm(track);
|
||||||
|
|
||||||
final client = getClient();
|
final client = getClient();
|
||||||
final resp =
|
final resp = await client.get(
|
||||||
await client.get('/search?keywords=${Uri.encodeComponent(query)}');
|
'/search?keywords=${Uri.encodeComponent(query)}&realIP=${NeteaseSourcedTrack.lookupRealIp()}',
|
||||||
|
);
|
||||||
|
if (resp.body?['code'] == 405) throw TrackNotFoundError(track);
|
||||||
final results = resp.body['result']['songs'];
|
final results = resp.body['result']['songs'];
|
||||||
|
|
||||||
// We can just trust netease music for now
|
// We can just trust netease music for now
|
||||||
@@ -202,7 +216,7 @@ class NeteaseSourcedTrack extends SourcedTrack {
|
|||||||
|
|
||||||
final client = getClient();
|
final client = getClient();
|
||||||
final resp = await client.get('/song/detail?ids=${newSourceInfo.id}');
|
final resp = await client.get('/song/detail?ids=${newSourceInfo.id}');
|
||||||
final item = resp.body['songs'][0];
|
final item = (resp.body['songs'] as List<dynamic>).first;
|
||||||
|
|
||||||
final (:info, :source) = toSiblingType(item);
|
final (:info, :source) = toSiblingType(item);
|
||||||
|
|
||||||
@@ -227,25 +241,29 @@ class NeteaseSourcedTrack extends SourcedTrack {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static SiblingType toSiblingType(dynamic item) {
|
static NeteaseSourceInfo toSourceInfo(dynamic item) {
|
||||||
final firstArtist = item['ar'] != null ? item['ar'][0] : item['artists'][0];
|
final firstArtist = item['ar'] != null ? item['ar'][0] : item['artists'][0];
|
||||||
|
|
||||||
|
return NeteaseSourceInfo(
|
||||||
|
id: item['id'].toString(),
|
||||||
|
artist: item['ar'] != null
|
||||||
|
? item['ar'].map((x) => x['name']).join(',')
|
||||||
|
: item['artists'].map((x) => x['name']).toString(),
|
||||||
|
artistUrl: 'https://music.163.com/#/artist?id=${firstArtist['id']}',
|
||||||
|
pageUrl: 'https://music.163.com/#/song?id=${item['id']}',
|
||||||
|
thumbnail: item['al']?['picUrl'] ??
|
||||||
|
'https://p1.music.126.net/6y-UleORITEDbvrOLV0Q8A==/5639395138885805.jpg',
|
||||||
|
title: item['name'],
|
||||||
|
duration: item['dt'] != null
|
||||||
|
? Duration(milliseconds: item['dt'])
|
||||||
|
: Duration(milliseconds: item['duration']),
|
||||||
|
album: item['al']?['name'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static SiblingType toSiblingType(dynamic item) {
|
||||||
final SiblingType sibling = (
|
final SiblingType sibling = (
|
||||||
info: NeteaseSourceInfo(
|
info: toSourceInfo(item),
|
||||||
id: item['id'].toString(),
|
|
||||||
artist: item['ar'] != null
|
|
||||||
? item['ar'].map((x) => x['name']).join(',')
|
|
||||||
: item['artists'].map((x) => x['name']).toString(),
|
|
||||||
artistUrl: 'https://music.163.com/#/artist?id=${firstArtist['id']}',
|
|
||||||
pageUrl: 'https://music.163.com/#/song?id=${item['id']}',
|
|
||||||
thumbnail: item['al']?['picUrl'] ??
|
|
||||||
'https://p1.music.126.net/6y-UleORITEDbvrOLV0Q8A==/5639395138885805.jpg',
|
|
||||||
title: item['name'],
|
|
||||||
duration: item['dt'] != null
|
|
||||||
? Duration(milliseconds: item['dt'])
|
|
||||||
: Duration(milliseconds: item['duration']),
|
|
||||||
album: item['al']?['name'],
|
|
||||||
),
|
|
||||||
source: toSourceMap(item),
|
source: toSourceMap(item),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
sourceId: siblings.first.info.id,
|
sourceId: siblings.first.info.id,
|
||||||
sourceType: const Value(SourceType.youtube),
|
sourceType: const Value(SourceType.youtube),
|
||||||
),
|
),
|
||||||
|
mode: InsertMode.insertOrReplace,
|
||||||
);
|
);
|
||||||
|
|
||||||
return YoutubeSourcedTrack(
|
return YoutubeSourcedTrack(
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import 'package:rhythm_box/platform.dart';
|
|||||||
class AutoCacheImage extends StatelessWidget {
|
class AutoCacheImage extends StatelessWidget {
|
||||||
final String url;
|
final String url;
|
||||||
final double? width, height;
|
final double? width, height;
|
||||||
|
final BoxFit? fit;
|
||||||
|
|
||||||
const AutoCacheImage(this.url, {super.key, this.width, this.height});
|
const AutoCacheImage(this.url,
|
||||||
|
{super.key, this.width, this.height, this.fit});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -15,12 +17,14 @@ class AutoCacheImage extends StatelessWidget {
|
|||||||
imageUrl: url,
|
imageUrl: url,
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
|
fit: fit,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Image.network(
|
return Image.network(
|
||||||
url,
|
url,
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
|
fit: fit,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:rhythm_box/providers/audio_player.dart';
|
import 'package:rhythm_box/providers/audio_player.dart';
|
||||||
|
import 'package:rhythm_box/providers/error_notifier.dart';
|
||||||
import 'package:rhythm_box/providers/user_preferences.dart';
|
import 'package:rhythm_box/providers/user_preferences.dart';
|
||||||
import 'package:rhythm_box/services/database/database.dart';
|
import 'package:rhythm_box/services/database/database.dart';
|
||||||
import 'package:rhythm_box/services/duration.dart';
|
import 'package:rhythm_box/services/duration.dart';
|
||||||
@@ -12,6 +13,7 @@ import 'package:rhythm_box/services/server/active_sourced_track.dart';
|
|||||||
import 'package:rhythm_box/services/sourced_track/models/source_info.dart';
|
import 'package:rhythm_box/services/sourced_track/models/source_info.dart';
|
||||||
import 'package:rhythm_box/services/sourced_track/models/video_info.dart';
|
import 'package:rhythm_box/services/sourced_track/models/video_info.dart';
|
||||||
import 'package:rhythm_box/services/sourced_track/sourced_track.dart';
|
import 'package:rhythm_box/services/sourced_track/sourced_track.dart';
|
||||||
|
import 'package:rhythm_box/services/sourced_track/sources/kugou.dart';
|
||||||
import 'package:rhythm_box/services/sourced_track/sources/netease.dart';
|
import 'package:rhythm_box/services/sourced_track/sources/netease.dart';
|
||||||
import 'package:rhythm_box/services/sourced_track/sources/piped.dart';
|
import 'package:rhythm_box/services/sourced_track/sources/piped.dart';
|
||||||
import 'package:rhythm_box/services/sourced_track/sources/youtube.dart';
|
import 'package:rhythm_box/services/sourced_track/sources/youtube.dart';
|
||||||
@@ -44,6 +46,7 @@ class _SiblingTracksState extends State<SiblingTracks> {
|
|||||||
YoutubeSourceInfo: 'YouTube',
|
YoutubeSourceInfo: 'YouTube',
|
||||||
PipedSourceInfo: 'Piped',
|
PipedSourceInfo: 'Piped',
|
||||||
NeteaseSourceInfo: 'Netease',
|
NeteaseSourceInfo: 'Netease',
|
||||||
|
KugouSourceInfo: 'Kugou',
|
||||||
};
|
};
|
||||||
|
|
||||||
List<StreamSubscription>? _subscriptions;
|
List<StreamSubscription>? _subscriptions;
|
||||||
@@ -89,48 +92,54 @@ class _SiblingTracksState extends State<SiblingTracks> {
|
|||||||
final preferences = Get.find<UserPreferencesProvider>().state.value;
|
final preferences = Get.find<UserPreferencesProvider>().state.value;
|
||||||
final searchTerm = _searchTermController.text.trim();
|
final searchTerm = _searchTermController.text.trim();
|
||||||
|
|
||||||
if (preferences.audioSource == AudioSource.youtube ||
|
try {
|
||||||
preferences.audioSource == AudioSource.piped) {
|
if (preferences.audioSource == AudioSource.youtube ||
|
||||||
final resultsYt = await youtubeClient.search.search(searchTerm.trim());
|
preferences.audioSource == AudioSource.piped) {
|
||||||
|
final resultsYt = await youtubeClient.search.search(searchTerm.trim());
|
||||||
|
|
||||||
final searchResults = await Future.wait(
|
final searchResults = await Future.wait(
|
||||||
resultsYt.map(YoutubeVideoInfo.fromVideo).mapIndexed((i, video) async {
|
resultsYt
|
||||||
final siblingType = await YoutubeSourcedTrack.toSiblingType(i, video);
|
.map(YoutubeVideoInfo.fromVideo)
|
||||||
return siblingType.info;
|
.mapIndexed((i, video) async {
|
||||||
}),
|
final siblingType =
|
||||||
);
|
await YoutubeSourcedTrack.toSiblingType(i, video);
|
||||||
final activeSourceInfo = (_activeTrack! as SourcedTrack).sourceInfo;
|
return siblingType.info;
|
||||||
_siblings = List.from(
|
}),
|
||||||
searchResults
|
);
|
||||||
..removeWhere((element) => element.id == activeSourceInfo.id)
|
final activeSourceInfo = (_activeTrack! as SourcedTrack).sourceInfo;
|
||||||
..insert(
|
_siblings = List.from(
|
||||||
0,
|
searchResults
|
||||||
activeSourceInfo,
|
..removeWhere((element) => element.id == activeSourceInfo.id)
|
||||||
),
|
..insert(
|
||||||
growable: true,
|
0,
|
||||||
);
|
activeSourceInfo,
|
||||||
} else if (preferences.audioSource == AudioSource.netease) {
|
),
|
||||||
final client = NeteaseSourcedTrack.getClient();
|
growable: true,
|
||||||
final resp = await client
|
);
|
||||||
.get('/search?keywords=${Uri.encodeComponent(searchTerm)}');
|
} else if (preferences.audioSource == AudioSource.netease) {
|
||||||
final searchResults = resp.body['result']['songs']
|
final client = NeteaseSourcedTrack.getClient();
|
||||||
.map(NeteaseSourcedTrack.toSiblingType)
|
final resp = await client.get(
|
||||||
.map((x) => x.info)
|
'/search?keywords=${Uri.encodeComponent(searchTerm)}&realIP=${NeteaseSourcedTrack.lookupRealIp()}');
|
||||||
.toList();
|
final searchResults = resp.body['result']['songs']
|
||||||
|
.map(NeteaseSourcedTrack.toSourceInfo)
|
||||||
|
.toList();
|
||||||
|
|
||||||
final activeSourceInfo = (_activeTrack! as SourcedTrack).sourceInfo;
|
final activeSourceInfo = (_activeTrack! as SourcedTrack).sourceInfo;
|
||||||
_siblings = List.from(
|
_siblings = List.from(
|
||||||
searchResults
|
searchResults
|
||||||
..removeWhere((element) => element.id == activeSourceInfo.id)
|
..removeWhere((element) => element.id == activeSourceInfo.id)
|
||||||
..insert(
|
..insert(
|
||||||
0,
|
0,
|
||||||
activeSourceInfo,
|
activeSourceInfo,
|
||||||
),
|
),
|
||||||
growable: true,
|
growable: true,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Get.find<ErrorNotifier>().showError(err.toString());
|
||||||
|
} finally {
|
||||||
|
setState(() => _isSearching = false);
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() => _isSearching = false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:rhythm_box/services/duration.dart';
|
import 'package:rhythm_box/services/duration.dart';
|
||||||
import 'package:rhythm_box/services/sourced_track/sourced_track.dart';
|
import 'package:rhythm_box/services/sourced_track/sourced_track.dart';
|
||||||
|
import 'package:rhythm_box/services/sourced_track/sources/kugou.dart';
|
||||||
import 'package:rhythm_box/services/sourced_track/sources/netease.dart';
|
import 'package:rhythm_box/services/sourced_track/sources/netease.dart';
|
||||||
import 'package:rhythm_box/services/sourced_track/sources/piped.dart';
|
import 'package:rhythm_box/services/sourced_track/sources/piped.dart';
|
||||||
import 'package:rhythm_box/services/sourced_track/sources/youtube.dart';
|
import 'package:rhythm_box/services/sourced_track/sources/youtube.dart';
|
||||||
@@ -14,6 +15,7 @@ class TrackSourceDetails extends StatelessWidget {
|
|||||||
YoutubeSourceInfo: 'YouTube',
|
YoutubeSourceInfo: 'YouTube',
|
||||||
PipedSourceInfo: 'Piped',
|
PipedSourceInfo: 'Piped',
|
||||||
NeteaseSourceInfo: 'Netease',
|
NeteaseSourceInfo: 'Netease',
|
||||||
|
KugouSourceInfo: 'Kugou',
|
||||||
};
|
};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -28,6 +28,8 @@
|
|||||||
<string>MainMenu</string>
|
<string>MainMenu</string>
|
||||||
<key>LSApplicationCategoryType</key>
|
<key>LSApplicationCategoryType</key>
|
||||||
<string>public.app-category.music</string>
|
<string>public.app-category.music</string>
|
||||||
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
<false/>
|
||||||
<key>NSPrincipalClass</key>
|
<key>NSPrincipalClass</key>
|
||||||
<string>NSApplication</string>
|
<string>NSApplication</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 1.0.0+10
|
version: 1.0.0+12
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.5.0
|
sdk: ^3.5.0
|
||||||
|
|||||||
Reference in New Issue
Block a user