mirror of
https://github.com/IllusionMan1212/lyrics-grabbr.git
synced 2026-10-24 02:12:27 +00:00
feat: implement lyrics fetching and finalize first app version
This commit is contained in:
@@ -2,7 +2,15 @@
|
|||||||
package="com.illusionware.lyricsgrabbr.lyricsgrabbr">
|
package="com.illusionware.lyricsgrabbr.lyricsgrabbr">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
<application
|
<application
|
||||||
android:label="Lyrics Grabbr"
|
android:label="Lyrics Grabbr"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
|
|||||||
-2
@@ -30,8 +30,6 @@ class NotificationStreamHandler(private val context: Context) : StreamHandler {
|
|||||||
override fun onCancel(o: Any?) {
|
override fun onCancel(o: Any?) {
|
||||||
eventSink = null
|
eventSink = null
|
||||||
Log.d("onCancel", "subscription cancelled, stopping service")
|
Log.d("onCancel", "subscription cancelled, stopping service")
|
||||||
|
|
||||||
context.unregisterReceiver(receiver)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class NotificationReceiver : BroadcastReceiver() {
|
inner class NotificationReceiver : BroadcastReceiver() {
|
||||||
|
|||||||
+105
@@ -0,0 +1,105 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'dart:convert' as convert;
|
||||||
|
|
||||||
|
class LyricsPage extends StatefulWidget {
|
||||||
|
const LyricsPage({
|
||||||
|
Key? key,
|
||||||
|
required this.title,
|
||||||
|
required this.artist,
|
||||||
|
required this.url,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final String artist;
|
||||||
|
final String url;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _LyricsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LyricsPageState extends State<LyricsPage> {
|
||||||
|
bool fetching = false;
|
||||||
|
String? lyrics;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
initPlatformState();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> initPlatformState() async {
|
||||||
|
setState(() => fetching = true);
|
||||||
|
|
||||||
|
final uri = Uri.https('api.illusionman1212.tech', '/lyrics', {
|
||||||
|
'songUrl': widget.url,
|
||||||
|
});
|
||||||
|
|
||||||
|
var response = await http.get(uri);
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final jsonRes = convert.jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
fetching = false;
|
||||||
|
lyrics = jsonRes['lyrics'];
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
print('error while GETing lyrics');
|
||||||
|
setState(() => fetching = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
widget.title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
widget.artist,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: fetching
|
||||||
|
? Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: const [CircularProgressIndicator(value: null)],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: SizedBox(
|
||||||
|
width: MediaQuery.of(context).size.width,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 30, vertical: 30),
|
||||||
|
child: Text(
|
||||||
|
lyrics ?? '',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+110
-30
@@ -2,6 +2,9 @@ import 'dart:async';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'notification_listener.dart';
|
import 'notification_listener.dart';
|
||||||
|
import 'search_result.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'dart:convert' as convert;
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
runApp(const LyricsGrabbr());
|
runApp(const LyricsGrabbr());
|
||||||
@@ -33,10 +36,11 @@ class HomePage extends StatefulWidget {
|
|||||||
|
|
||||||
class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
||||||
late AndroidNotificationListener _notifications;
|
late AndroidNotificationListener _notifications;
|
||||||
late StreamSubscription<NotificationEvent> _subscription;
|
|
||||||
BuildContext? permissionDialogCtx;
|
BuildContext? permissionDialogCtx;
|
||||||
NotificationEvent? currentlyPlaying;
|
NotificationEvent? lastNotification;
|
||||||
bool running = false;
|
bool running = false;
|
||||||
|
bool searching = false;
|
||||||
|
final List<SearchResult> searchResults = [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -85,7 +89,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
|||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
content: const Text(
|
content: const Text(
|
||||||
'This app requires permission to listen to notifications to function properly. Please grant permission in the next screen.',
|
'This app requires permission to listen to notifications to function properly. Please grant permission in the next screen. BEWARE that the only way to stop the service is to revoke the notification listening permission.',
|
||||||
style: TextStyle(color: Colors.blueGrey),
|
style: TextStyle(color: Colors.blueGrey),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
@@ -94,7 +98,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
|||||||
await AndroidNotificationListener
|
await AndroidNotificationListener
|
||||||
.openPermissionSettings();
|
.openPermissionSettings();
|
||||||
},
|
},
|
||||||
child: const Text("OK")),
|
child: const Text('OK')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -112,21 +116,74 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onData(NotificationEvent event) {
|
void onData(NotificationEvent event) {
|
||||||
|
if (event.playbackState == PlaybackState.STATE_INVALID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastNotification?.songTitle != event.songTitle &&
|
||||||
|
lastNotification?.artist != event.artist) {
|
||||||
|
searchLyrics(event.songTitle, event.artist);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
currentlyPlaying = event;
|
searchResults.clear();
|
||||||
|
lastNotification = event;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void startListening() {
|
void startListening() {
|
||||||
_notifications = AndroidNotificationListener();
|
_notifications = AndroidNotificationListener();
|
||||||
|
|
||||||
_subscription = _notifications.notificationStream.listen(onData);
|
_notifications.notificationStream.listen(onData);
|
||||||
setState(() => running = true);
|
setState(() => running = true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void stopListening() {
|
void searchLyrics(
|
||||||
setState(() => running = false);
|
String song,
|
||||||
_subscription.cancel();
|
String artist,
|
||||||
|
) async {
|
||||||
|
setState(() {
|
||||||
|
searching = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: build an httpclient to reuse between requests.
|
||||||
|
final uri = Uri.https(
|
||||||
|
'api.illusionman1212.tech',
|
||||||
|
'/lyrics/search',
|
||||||
|
{'q': '$song $artist'},
|
||||||
|
);
|
||||||
|
|
||||||
|
var response = await http.get(uri);
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final jsonRes = convert.jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
List<dynamic> results = jsonRes['all'];
|
||||||
|
|
||||||
|
for (var result in results) {
|
||||||
|
var myResult = SearchResult.fromJson(result);
|
||||||
|
setState(() {
|
||||||
|
searchResults.add(myResult);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
searching = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// TODO: display error
|
||||||
|
print('error in http get');
|
||||||
|
setState(() {
|
||||||
|
searching = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildResults() {
|
||||||
|
return ListView.builder(
|
||||||
|
itemBuilder: (_, i) {
|
||||||
|
return searchResults[i];
|
||||||
|
},
|
||||||
|
itemCount: searchResults.length,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -134,17 +191,19 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(widget.title),
|
title: Text(widget.title),
|
||||||
|
centerTitle: true,
|
||||||
),
|
),
|
||||||
body: Container(
|
body: Center(
|
||||||
padding: const EdgeInsets.all(20),
|
|
||||||
child: Center(
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
running ? RichText(
|
if (running)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: RichText(
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
text: currentlyPlaying?.songTitle,
|
text: lastNotification?.songTitle,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 24.0,
|
fontSize: 24.0,
|
||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
@@ -152,33 +211,54 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
|||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
const TextSpan(
|
const TextSpan(
|
||||||
text: "\n",
|
text: '\n',
|
||||||
),
|
),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: currentlyPlaying?.artist,
|
text: lastNotification?.artist,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 20.0,
|
fontSize: 20.0,
|
||||||
color: Color(0xFF303030),
|
color: Color(0xFF303030),
|
||||||
)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
]),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
) :
|
),
|
||||||
const Text("Listener stopped"),
|
),
|
||||||
running ? Text(
|
Container(
|
||||||
currentlyPlaying?.playbackState == PlaybackState.STATE_PLAYING ? "Spotify Playing" : "Spotify Paused",
|
child: searching
|
||||||
style: Theme.of(context).textTheme.headline6,
|
? Expanded(
|
||||||
) :
|
child: Column(
|
||||||
const Text("")
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: const [
|
||||||
|
CircularProgressIndicator(value: null)
|
||||||
|
]))
|
||||||
|
: Flexible(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 10),
|
||||||
|
child: Text(
|
||||||
|
running
|
||||||
|
? 'Found ${searchResults.length} result(s)'
|
||||||
|
: "",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
color: Colors.black87,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Flexible(child: _buildResults()),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton(
|
],
|
||||||
onPressed: running ? stopListening : startListening,
|
),
|
||||||
tooltip: running ? 'Stop listener' : "Start listener",
|
|
||||||
child: running ? const Icon(Icons.stop) : const Icon(Icons.play_arrow),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,14 +28,13 @@ class NotificationEvent {
|
|||||||
|
|
||||||
final DateTime timeStamp;
|
final DateTime timeStamp;
|
||||||
|
|
||||||
const NotificationEvent({
|
const NotificationEvent(
|
||||||
required this.packageName,
|
{required this.packageName,
|
||||||
required this.artist,
|
required this.artist,
|
||||||
required this.timeStamp,
|
required this.timeStamp,
|
||||||
required this.songTitle,
|
required this.songTitle,
|
||||||
required this.playbackState,
|
required this.playbackState,
|
||||||
required this.duration
|
required this.duration});
|
||||||
});
|
|
||||||
|
|
||||||
factory NotificationEvent.fromMap(Map<dynamic, dynamic> map) {
|
factory NotificationEvent.fromMap(Map<dynamic, dynamic> map) {
|
||||||
DateTime time = DateTime.now();
|
DateTime time = DateTime.now();
|
||||||
@@ -51,8 +50,7 @@ class NotificationEvent {
|
|||||||
timeStamp: time,
|
timeStamp: time,
|
||||||
songTitle: text,
|
songTitle: text,
|
||||||
playbackState: playbackState,
|
playbackState: playbackState,
|
||||||
duration: duration
|
duration: duration);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +66,6 @@ class AndroidNotificationListener {
|
|||||||
|
|
||||||
Stream<NotificationEvent> get notificationStream {
|
Stream<NotificationEvent> get notificationStream {
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
|
|
||||||
Stream<NotificationEvent> _notificationStream = _notificationEventChannel
|
Stream<NotificationEvent> _notificationStream = _notificationEventChannel
|
||||||
.receiveBroadcastStream()
|
.receiveBroadcastStream()
|
||||||
.map((event) => _notificationEvent(event));
|
.map((event) => _notificationEvent(event));
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
import 'lyrics.dart';
|
||||||
|
|
||||||
|
class SearchResult extends StatefulWidget {
|
||||||
|
const SearchResult(
|
||||||
|
{Key? key,
|
||||||
|
required this.title,
|
||||||
|
required this.artist,
|
||||||
|
required this.url,
|
||||||
|
required this.artistUrl,
|
||||||
|
required this.thumbnail,
|
||||||
|
required this.id})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final String artist;
|
||||||
|
final String url;
|
||||||
|
final String artistUrl;
|
||||||
|
final String thumbnail;
|
||||||
|
final int id;
|
||||||
|
|
||||||
|
SearchResult.fromJson(Map<String, dynamic> json, {Key? key})
|
||||||
|
: title = json['meta']['title'],
|
||||||
|
artist = json['meta']['primaryArtist']['name'],
|
||||||
|
url = json['url'],
|
||||||
|
artistUrl = json['meta']['primaryArtist']['url'],
|
||||||
|
thumbnail = json['resources']['thumbnail'],
|
||||||
|
id = json['id'],
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _SearchResultState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class OptionItem extends StatelessWidget {
|
||||||
|
final String title;
|
||||||
|
final IconData icon;
|
||||||
|
final void Function()? tapFunction;
|
||||||
|
|
||||||
|
const OptionItem(
|
||||||
|
{Key? key, required this.title, required this.icon, this.tapFunction})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: tapFunction,
|
||||||
|
child: Ink(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.only(right: 15),
|
||||||
|
child: Icon(icon),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SearchResultState extends State<SearchResult> {
|
||||||
|
List<Widget> _buildOptions() {
|
||||||
|
return <OptionItem>[
|
||||||
|
OptionItem(
|
||||||
|
title: "Open Artist Page in Genius",
|
||||||
|
icon: Icons.library_music,
|
||||||
|
tapFunction: () async {
|
||||||
|
if (!await launch(widget.artistUrl)) {
|
||||||
|
throw 'could not launch ${widget.artistUrl}';
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
OptionItem(
|
||||||
|
title: "Open Song Page in Genius",
|
||||||
|
icon: Icons.music_note,
|
||||||
|
tapFunction: () async {
|
||||||
|
if (!await launch(widget.url)) {
|
||||||
|
throw 'could not launch ${widget.url}';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showOptionsBottomSheet() {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxHeight:
|
||||||
|
(MediaQuery.of(context).size.height - kToolbarHeight) / 2,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: _buildOptions(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Material(
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(top: 0.5),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => LyricsPage(
|
||||||
|
title: widget.title,
|
||||||
|
artist: widget.artist,
|
||||||
|
url: widget.url,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onLongPress: () => _showOptionsBottomSheet(),
|
||||||
|
child: Ink(
|
||||||
|
height: 75,
|
||||||
|
color: Colors.black,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.only(right: 20),
|
||||||
|
child: Image.network(
|
||||||
|
widget.thumbnail,
|
||||||
|
width: 75,
|
||||||
|
height: 75,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
widget.title,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
widget.artist,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white70,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+69
-1
@@ -116,6 +116,11 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_web_plugins:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
http:
|
http:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -130,6 +135,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.0"
|
version: "4.0.0"
|
||||||
|
js:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: js
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.3"
|
||||||
lints:
|
lints:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -254,6 +266,62 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.3.0"
|
||||||
|
url_launcher:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: url_launcher
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "6.0.20"
|
||||||
|
url_launcher_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_android
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "6.0.15"
|
||||||
|
url_launcher_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_ios
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "6.0.15"
|
||||||
|
url_launcher_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_linux
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.0"
|
||||||
|
url_launcher_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_macos
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.0"
|
||||||
|
url_launcher_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_platform_interface
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.5"
|
||||||
|
url_launcher_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_web
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.9"
|
||||||
|
url_launcher_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_windows
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.0"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -277,4 +345,4 @@ packages:
|
|||||||
version: "5.3.1"
|
version: "5.3.1"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.16.1 <3.0.0"
|
dart: ">=2.16.1 <3.0.0"
|
||||||
flutter: ">=2.2.0"
|
flutter: ">=2.10.0"
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ dependencies:
|
|||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
http: ^0.13.4
|
http: ^0.13.4
|
||||||
flutter_local_notifications: ^9.3.2
|
flutter_local_notifications: ^9.3.2
|
||||||
|
url_launcher: ^6.0.20
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user