6 Commits
Author SHA1 Message Date
Illusion e69b5e6848 fix: move to new api domain 2022-10-02 00:32:36 +03:00
Illusion 42077e17ee feat/fix: themes with some fixes 2022-08-07 06:26:54 +03:00
Illusion 55d26abaae chore: update readme 2022-08-06 02:42:48 +03:00
Illusion 197be6330b fix: show toast error and fallback icon when album art fails to load 2022-08-06 02:25:55 +03:00
Illusion 100157a8d4 feat/fix: bump to 0.2.0
* remove spotify-only restriction.
* only support players that use mediasession and media notifications.
* don't capture non-mediasession notifications.
2022-08-06 00:55:18 +03:00
Illusion 3bf7750e51 fix: better looking result item, and client-side lyrics fetching 2022-08-03 23:06:20 +03:00
9 changed files with 623 additions and 210 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
# lyricsgrabbr
App to listen to Spotify notifications and provide lyrics for currently playing song.
App that listens to your notifications and provides lyrics for the currently playing song.
@@ -56,21 +56,19 @@ class NotificationListener : NotificationListenerService() {
}
override fun onNotificationPosted(sbn: StatusBarNotification) {
if (sbn.packageName == SPOTIFY_PACKAGE) {
sendNotificationBroadcast(sbn, applicationContext)
}
}
private fun getStopServiceNotificationAction(): PendingIntent {
val intent = Intent(MainActivity.ACTION_NOTIFICATION_LISTENER_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
val pendingIntent = PendingIntent.getActivity(this, 0, intent, 0)
val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE)
return pendingIntent
}
private fun startForegroundService() {
val notification: Notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
.setContentTitle("Service Running")
.setContentText("Play some songs on spotify to get lyrics")
.setContentText("Play some songs on a supported player to get lyrics")
.setSmallIcon(R.drawable.ic_notification)
.setShowWhen(false)
.addAction(R.drawable.ic_notification, "Stop Service", getStopServiceNotificationAction())
@@ -103,19 +101,21 @@ class NotificationListener : NotificationListenerService() {
private fun getCurrentNotifications(context: Context) {
val sbns = activeNotifications
for (sbn in sbns) {
if (sbn.packageName == SPOTIFY_PACKAGE) {
sendNotificationBroadcast(sbn, context)
}
}
}
private fun sendNotificationBroadcast(sbn: StatusBarNotification, context: Context) {
// get the media session stuff
val mediaSessionToken = sbn.notification.extras.getParcelable<MediaSession.Token>(Notification.EXTRA_MEDIA_SESSION)
val mediaCtrl = mediaSessionToken?.let { MediaController(context, it) }
val duration = mediaCtrl?.metadata?.getLong(MediaMetadata.METADATA_KEY_DURATION)?.div(1000)
val playbackState = mediaCtrl?.playbackState?.state
val albumArt = mediaCtrl?.metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART)
if (mediaSessionToken == null) {
return
}
val mediaCtrl = mediaSessionToken.let { MediaController(context, it) }
val duration = mediaCtrl.metadata?.getLong(MediaMetadata.METADATA_KEY_DURATION)?.div(1000)
val playbackState = mediaCtrl.playbackState?.state
val albumArt = mediaCtrl.metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART)
// Retrieve package name to set as title.
val packageName = sbn.packageName
@@ -134,7 +134,7 @@ class NotificationListener : NotificationListenerService() {
if (playbackState == PlaybackState.STATE_PAUSED) {
val notification: Notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
.setContentTitle("Service Running")
.setContentText("Play some songs on spotify to get lyrics")
.setContentText("Play some songs on a supported player to get lyrics")
.setSmallIcon(R.drawable.ic_notification)
.setShowWhen(false)
.addAction(R.drawable.ic_notification, "Stop Service", getStopServiceNotificationAction())
@@ -173,7 +173,6 @@ class NotificationListener : NotificationListenerService() {
const val NOTIFICATION_PACKAGE_DURATION = "package_duration"
const val NOTIFICATION_ID = 12251999
const val NOTIFICATION_CHANNEL_ID = "Lyrics"
const val SPOTIFY_PACKAGE = "com.spotify.music"
const val START_FOREGROUND_SERVICE_ACTION = "START_SERVICE"
}
}
+12 -15
View File
@@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert' as convert;
import 'package:web_scraper/web_scraper.dart';
class LyricsPage extends StatefulWidget {
const LyricsPage({
@@ -31,21 +30,20 @@ class _LyricsPageState extends State<LyricsPage> {
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>;
final webScraper = WebScraper('https://genius.com');
String path = widget.url.split('genius.com/')[1];
String fullLyrics = '';
if (await webScraper.loadWebPage('/$path?react=1')) {
webScraper.loadFromString(webScraper.getPageContent().replaceAll('<br>', '\n'));
var element = webScraper.getElement("div[data-lyrics-container]", ['data-lyrics-container']);
for (var section in element) {
fullLyrics += section['title'] + '\n';
}
setState(() {
lyrics = fullLyrics;
fetching = false;
lyrics = jsonRes['lyrics'];
});
} else {
print('error while GETing lyrics');
setState(() => fetching = false);
}
}
@@ -88,8 +86,7 @@ class _LyricsPageState extends State<LyricsPage> {
width: MediaQuery.of(context).size.width,
child: SingleChildScrollView(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 30, vertical: 30),
padding: const EdgeInsets.all(30),
child: Text(
lyrics ?? '',
style: const TextStyle(
+88 -31
View File
@@ -1,28 +1,61 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'notification_listener.dart';
import 'search_result.dart';
import 'package:http/http.dart' as http;
import 'dart:convert' as convert;
import 'package:shared_preferences/shared_preferences.dart';
import 'settings.dart';
import 'theme_provider.dart';
Future<void> main() async {
runApp(const LyricsGrabbr());
}
class LyricsGrabbr extends StatelessWidget {
class LyricsGrabbr extends StatefulWidget {
const LyricsGrabbr({Key? key}) : super(key: key);
@override
State<LyricsGrabbr> createState() => _LyricsGrabbrState();
}
class _LyricsGrabbrState extends State<LyricsGrabbr> {
@override
Widget build(BuildContext context) {
return FutureBuilder<SharedPreferences>(
future: SharedPreferences.getInstance(),
builder: (_, snapshot) {
final prefTheme = snapshot.data?.getString("theme");
var themeMode = ThemeMode.system;
if (prefTheme == AppTheme.Dark.name) {
themeMode = ThemeMode.dark;
} else if (prefTheme == AppTheme.Light.name) {
themeMode = ThemeMode.light;
} else if (prefTheme == AppTheme.System.name) {
themeMode = ThemeMode.system;
}
return snapshot.hasData ? ChangeNotifierProvider(
create: (context) => ThemeProvider(themeMode),
builder: (context, _) {
final themeProvider = Provider.of<ThemeProvider>(context);
return MaterialApp(
title: 'Lyrics Grabbr',
theme: ThemeData(
primarySwatch: Colors.purple,
),
theme: MyThemes.lightTheme,
darkTheme: MyThemes.darkTheme,
themeMode: themeProvider.themeMode,
home: const HomePage(title: 'Lyrics Grabbr'),
);
}
) : Container();
}
);
}
}
class HomePage extends StatefulWidget {
@@ -44,14 +77,14 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
@override
void initState() {
WidgetsBinding.instance?.addObserver(this);
WidgetsBinding.instance.addObserver(this);
super.initState();
initPlatformState();
}
@override
void dispose() {
WidgetsBinding.instance?.removeObserver(this);
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@@ -90,7 +123,6 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
),
content: const Text(
'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),
),
actions: [
TextButton(
@@ -148,7 +180,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
// TODO: build an httpclient to reuse between requests.
final uri = Uri.https(
'api.illusionman1212.tech',
'api.illusionman1212.com',
'/lyrics/search',
{'q': '$song $artist'},
);
@@ -178,35 +210,28 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
}
Widget _buildResults() {
return ListView.builder(
return ListView.separated(
itemBuilder: (_, i) {
return searchResults[i];
},
itemCount: searchResults.length,
separatorBuilder: (BuildContext context, int index) {
return const Divider(height: 1);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
centerTitle: true,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (running)
List<Widget> _buildPage() {
if (lastNotification != null) {
return [
Container(
padding: const EdgeInsets.all(20),
child: RichText(
text: TextSpan(
text: lastNotification?.songTitle,
style: const TextStyle(
style: TextStyle(
fontSize: 24.0,
color: Colors.black,
color: Theme.of(context).textTheme.bodyLarge?.color,
fontWeight: FontWeight.w700,
),
children: [
@@ -215,9 +240,9 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
),
TextSpan(
text: lastNotification?.artist,
style: const TextStyle(
style: TextStyle(
fontSize: 20.0,
color: Color(0xFF303030),
color: Theme.of(context).textTheme.bodyMedium?.color,
),
)
]),
@@ -230,9 +255,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: const [
CircularProgressIndicator(value: null)
]))
children: const [CircularProgressIndicator(value: null)]))
: Flexible(
child: Column(
mainAxisSize: MainAxisSize.min,
@@ -244,9 +267,9 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
running
? 'Found ${searchResults.length} result(s)'
: "",
style: const TextStyle(
style: TextStyle(
fontSize: 15,
color: Colors.black87,
color: Theme.of(context).textTheme.bodyText1?.color,
fontWeight: FontWeight.bold,
fontStyle: FontStyle.italic,
),
@@ -257,8 +280,42 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
),
),
),
];
}
return [
const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Text("No supported player is running",
style: TextStyle(fontSize: 24)),
),
];
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
centerTitle: true,
actions: [
IconButton(
icon: const Icon(Icons.settings),
tooltip: "Settings",
splashRadius: 20,
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const SettingsPage()));
},
),
],
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: _buildPage(),
),
),
);
}
+58 -59
View File
@@ -3,7 +3,7 @@ import 'package:url_launcher/url_launcher.dart';
import 'lyrics.dart';
class SearchResult extends StatefulWidget {
class SearchResult extends StatelessWidget {
const SearchResult(
{Key? key,
required this.title,
@@ -30,65 +30,28 @@ class SearchResult extends StatefulWidget {
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}';
onTap: () async {
if (!await launch(artistUrl)) {
throw 'could not launch $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}';
onTap: () async {
if (!await launch(url)) {
throw 'could not launch $url';
}
})
];
}
void _showOptionsBottomSheet() {
void _showOptionsBottomSheet(BuildContext context) {
showModalBottomSheet(
context: context,
builder: (context) {
@@ -108,33 +71,38 @@ class _SearchResultState extends State<SearchResult> {
@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,
title: title,
artist: artist,
url: url,
),
),
);
},
onLongPress: () => _showOptionsBottomSheet(),
onLongPress: () => _showOptionsBottomSheet(context),
child: Ink(
height: 75,
color: Colors.black,
child: Row(
children: [
Container(
margin: const EdgeInsets.only(right: 20),
child: Image.network(
widget.thumbnail,
thumbnail,
width: 75,
height: 75,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return const Center(
child: SizedBox(
width: 75,
height: 75,
child: Icon(Icons.broken_image, size: 40)),
);
},
),
),
Flexible(
@@ -143,18 +111,18 @@ class _SearchResultState extends State<SearchResult> {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
widget.title,
style: const TextStyle(
color: Colors.white,
title,
style: TextStyle(
color: Theme.of(context).textTheme.bodyText1?.color,
fontSize: 18,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
Text(
widget.artist,
style: const TextStyle(
color: Colors.white70,
artist,
style: TextStyle(
color: Theme.of(context).textTheme.subtitle1?.color,
fontSize: 14,
),
overflow: TextOverflow.ellipsis,
@@ -166,6 +134,37 @@ class _SearchResultState extends State<SearchResult> {
),
),
),
);
}
}
class OptionItem extends StatelessWidget {
final String title;
final IconData icon;
final void Function()? onTap;
const OptionItem(
{Key? key, required this.title, required this.icon, this.onTap})
: super(key: key);
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Ink(
padding: const EdgeInsets.all(20),
child: Row(
children: [
Container(
margin: const EdgeInsets.only(right: 15),
child: Icon(icon),
),
Text(
title,
style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600),
),
],
),
),
);
}
+188
View File
@@ -0,0 +1,188 @@
// ignore_for_file: constant_identifier_names
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'theme_provider.dart';
enum AppTheme {
Dark,
Light,
System
}
class Setting extends StatelessWidget {
final String title;
final String? desc;
final void Function() onTap;
const Setting({
Key? key,
required this.title,
this.desc,
required this.onTap,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Material(
child: InkWell(
onTap: onTap,
child: Ink(
child: Container(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: const TextStyle(fontSize: 16)),
if (desc != null)
Text(desc!,
style: const TextStyle(color: Colors.grey)),
]),
),
],
),
),
),
),
);
}
}
class SettingsPage extends StatefulWidget {
const SettingsPage({Key? key}) : super(key: key);
@override
State<SettingsPage> createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> {
final Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
late SharedPreferences prefs;
String? activeTheme;
@override
void initState() {
super.initState();
initPlatformState();
}
Future<void> initPlatformState() async {
prefs = await _prefs;
setState(() {
activeTheme = prefs.getString("theme") ?? AppTheme.System.name;
});
}
Widget _buildSettingsGroup(String groupTitle, List<Setting> items) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(groupTitle,
style: const TextStyle(color: Colors.deepPurple)),
),
...items,
],
);
}
List<Widget> _buildSettingsList(BuildContext context) {
return [
_buildSettingsGroup("Customization", [
Setting(
title: "Theme",
desc: activeTheme,
onTap: () {
final provider = Provider.of<ThemeProvider>(context, listen: false);
showDialog(
context: context,
builder: (BuildContext buildCtx) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(2)),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 20),
child: Text("Theme",
style: Theme.of(context).textTheme.headline6),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
children: [
RadioListTile(
value: AppTheme.System.name,
groupValue: activeTheme,
title: Text("Use System Default", style: Theme.of(context).textTheme.bodyText1),
onChanged: (String? value) => setState(() {
provider.changeTheme(ThemeMode.system);
activeTheme = AppTheme.System.name;
prefs.setString("theme", AppTheme.System.name);
Navigator.pop(context);
})),
RadioListTile(
value: AppTheme.Dark.name,
groupValue: activeTheme,
title: Text("Dark", style: Theme.of(context).textTheme.bodyText1),
onChanged: (String? value) => setState(() {
provider.changeTheme(ThemeMode.dark);
activeTheme = AppTheme.Dark.name;
prefs.setString("theme", AppTheme.Dark.name);
Navigator.pop(context);
})),
RadioListTile(
value: AppTheme.Light.name,
groupValue: activeTheme,
title: Text("Light", style: Theme.of(context).textTheme.bodyText1),
onChanged: (String? value) => setState(() {
provider.changeTheme(ThemeMode.light);
activeTheme = AppTheme.Light.name;
prefs.setString("theme", AppTheme.Light.name);
Navigator.pop(context);
})),
],
),
),
],
),
),
);
});
})
])
];
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Settings"),
),
body: ListView.separated(
itemBuilder: (_, i) {
return _buildSettingsList(context)[i];
},
separatorBuilder: (_, __) {
return const SizedBox(height: 15);
},
itemCount: 1,
padding: const EdgeInsets.symmetric(vertical: 20),
),
);
}
}
+43
View File
@@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
class ThemeProvider extends ChangeNotifier {
ThemeMode themeMode;
ThemeProvider(this.themeMode);
bool get isDarkMode => themeMode == ThemeMode.dark;
void changeTheme(ThemeMode mode) {
themeMode = mode;
notifyListeners();
}
}
class MyThemes {
static final darkTheme = ThemeData(
primarySwatch: Colors.deepPurple,
dialogTheme: const DialogTheme(backgroundColor: Color(0xFF202020)),
unselectedWidgetColor: Colors.white,
canvasColor: Colors.black,
bottomSheetTheme: const BottomSheetThemeData(backgroundColor: Color(0xFF202020)),
dividerColor: Colors.white,
iconTheme: const IconThemeData(color: Colors.white),
textTheme: const TextTheme(
bodyText1: TextStyle(color: Colors.white, fontSize: 16),
bodyText2: TextStyle(color: Colors.white),
subtitle1: TextStyle(color: Colors.grey),
subtitle2: TextStyle(color: Colors.white),
headline6: TextStyle(color: Colors.white),
),
);
static final lightTheme = ThemeData(
primarySwatch: Colors.purple,
dividerColor: Colors.black,
textTheme: const TextTheme(
bodyText1: TextStyle(fontSize: 16),
subtitle1: TextStyle(color: Colors.black54),
),
);
}
+135 -9
View File
@@ -49,7 +49,14 @@ packages:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.15.0"
version: "1.16.0"
csslib:
dependency: transitive
description:
name: csslib
url: "https://pub.dartlang.org"
source: hosted
version: "0.17.2"
dbus:
dependency: transitive
description:
@@ -63,7 +70,7 @@ packages:
name: fake_async
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
version: "1.3.0"
ffi:
dependency: transitive
description:
@@ -121,6 +128,20 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
fluttertoast:
dependency: "direct main"
description:
name: fluttertoast
url: "https://pub.dartlang.org"
source: hosted
version: "8.0.9"
html:
dependency: transitive
description:
name: html
url: "https://pub.dartlang.org"
source: hosted
version: "0.15.0"
http:
dependency: "direct main"
description:
@@ -141,7 +162,7 @@ packages:
name: js
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.3"
version: "0.6.4"
lints:
dependency: transitive
description:
@@ -162,7 +183,7 @@ packages:
name: material_color_utilities
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.3"
version: "0.1.4"
meta:
dependency: transitive
description:
@@ -170,13 +191,41 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.0"
nested:
dependency: transitive
description:
name: nested
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
path:
dependency: transitive
description:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0"
version: "1.8.1"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.7"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.4"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.7"
petitparser:
dependency: transitive
description:
@@ -205,6 +254,69 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "4.2.4"
provider:
dependency: "direct main"
description:
name: provider
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.3"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.15"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.12"
shared_preferences_ios:
dependency: transitive
description:
name: shared_preferences_ios
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
shared_preferences_macos:
dependency: transitive
description:
name: shared_preferences_macos
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.4"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.4"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
sky_engine:
dependency: transitive
description: flutter
@@ -216,7 +328,7 @@ packages:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.1"
version: "1.8.2"
stack_trace:
dependency: transitive
description:
@@ -251,7 +363,7 @@ packages:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.8"
version: "0.4.9"
timezone:
dependency: transitive
description:
@@ -328,7 +440,21 @@ packages:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
version: "2.1.2"
web_scraper:
dependency: "direct main"
description:
name: web_scraper
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.4"
win32:
dependency: transitive
description:
name: win32
url: "https://pub.dartlang.org"
source: hosted
version: "2.6.1"
xdg_directories:
dependency: transitive
description:
@@ -344,5 +470,5 @@ packages:
source: hosted
version: "5.3.1"
sdks:
dart: ">=2.16.1 <3.0.0"
dart: ">=2.17.0 <3.0.0"
flutter: ">=2.10.0"
+6 -2
View File
@@ -1,5 +1,5 @@
name: lyricsgrabbr
description: A new Flutter project.
description: App that listens to your notifications and provides lyrics for the currently playing song.
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
@@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.1.1+1
version: 0.3.1+1
environment:
sdk: ">=2.16.1 <3.0.0"
@@ -33,6 +33,10 @@ dependencies:
http: ^0.13.4
flutter_local_notifications: ^9.3.2
url_launcher: ^6.0.20
web_scraper: ^0.1.4
fluttertoast: ^8.0.9
shared_preferences: ^2.0.15
provider: ^6.0.3
dev_dependencies:
flutter_test: