mirror of
https://github.com/IllusionMan1212/lyrics-grabbr.git
synced 2026-10-24 02:12:27 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42077e17ee | ||
|
|
55d26abaae |
@@ -1,3 +1,3 @@
|
|||||||
# lyricsgrabbr
|
# 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.
|
||||||
|
|||||||
+1
-3
@@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'dart:convert' as convert;
|
|
||||||
import 'package:web_scraper/web_scraper.dart';
|
import 'package:web_scraper/web_scraper.dart';
|
||||||
|
|
||||||
class LyricsPage extends StatefulWidget {
|
class LyricsPage extends StatefulWidget {
|
||||||
@@ -87,8 +86,7 @@ class _LyricsPageState extends State<LyricsPage> {
|
|||||||
width: MediaQuery.of(context).size.width,
|
width: MediaQuery.of(context).size.width,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.all(30),
|
||||||
horizontal: 30, vertical: 30),
|
|
||||||
child: Text(
|
child: Text(
|
||||||
lyrics ?? '',
|
lyrics ?? '',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
|
|||||||
+85
-28
@@ -1,28 +1,61 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import 'notification_listener.dart';
|
import 'notification_listener.dart';
|
||||||
import 'search_result.dart';
|
import 'search_result.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'dart:convert' as convert;
|
import 'dart:convert' as convert;
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import 'settings.dart';
|
||||||
|
import 'theme_provider.dart';
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
runApp(const LyricsGrabbr());
|
runApp(const LyricsGrabbr());
|
||||||
}
|
}
|
||||||
|
|
||||||
class LyricsGrabbr extends StatelessWidget {
|
class LyricsGrabbr extends StatefulWidget {
|
||||||
const LyricsGrabbr({Key? key}) : super(key: key);
|
const LyricsGrabbr({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<LyricsGrabbr> createState() => _LyricsGrabbrState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LyricsGrabbrState extends State<LyricsGrabbr> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
return MaterialApp(
|
||||||
title: 'Lyrics Grabbr',
|
title: 'Lyrics Grabbr',
|
||||||
theme: ThemeData(
|
theme: MyThemes.lightTheme,
|
||||||
primarySwatch: Colors.purple,
|
darkTheme: MyThemes.darkTheme,
|
||||||
),
|
themeMode: themeProvider.themeMode,
|
||||||
home: const HomePage(title: 'Lyrics Grabbr'),
|
home: const HomePage(title: 'Lyrics Grabbr'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
) : Container();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class HomePage extends StatefulWidget {
|
class HomePage extends StatefulWidget {
|
||||||
@@ -90,7 +123,6 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
|||||||
),
|
),
|
||||||
content: const Text(
|
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.',
|
'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: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
@@ -178,35 +210,28 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildResults() {
|
Widget _buildResults() {
|
||||||
return ListView.builder(
|
return ListView.separated(
|
||||||
itemBuilder: (_, i) {
|
itemBuilder: (_, i) {
|
||||||
return searchResults[i];
|
return searchResults[i];
|
||||||
},
|
},
|
||||||
itemCount: searchResults.length,
|
itemCount: searchResults.length,
|
||||||
|
separatorBuilder: (BuildContext context, int index) {
|
||||||
|
return const Divider(height: 1);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
List<Widget> _buildPage() {
|
||||||
Widget build(BuildContext context) {
|
if (lastNotification != null) {
|
||||||
return Scaffold(
|
return [
|
||||||
appBar: AppBar(
|
|
||||||
title: Text(widget.title),
|
|
||||||
centerTitle: true,
|
|
||||||
),
|
|
||||||
body: Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
if (running)
|
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
child: RichText(
|
child: RichText(
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
text: lastNotification?.songTitle,
|
text: lastNotification?.songTitle,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 24.0,
|
fontSize: 24.0,
|
||||||
color: Colors.black,
|
color: Theme.of(context).textTheme.bodyLarge?.color,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
@@ -215,9 +240,9 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
|||||||
),
|
),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: lastNotification?.artist,
|
text: lastNotification?.artist,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 20.0,
|
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(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: const [
|
children: const [CircularProgressIndicator(value: null)]))
|
||||||
CircularProgressIndicator(value: null)
|
|
||||||
]))
|
|
||||||
: Flexible(
|
: Flexible(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -244,9 +267,9 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
|||||||
running
|
running
|
||||||
? 'Found ${searchResults.length} result(s)'
|
? 'Found ${searchResults.length} result(s)'
|
||||||
: "",
|
: "",
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: Colors.black87,
|
color: Theme.of(context).textTheme.bodyText1?.color,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontStyle: FontStyle.italic,
|
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(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+50
-63
@@ -1,10 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
|
||||||
|
|
||||||
import 'lyrics.dart';
|
import 'lyrics.dart';
|
||||||
|
|
||||||
class SearchResult extends StatefulWidget {
|
class SearchResult extends StatelessWidget {
|
||||||
const SearchResult(
|
const SearchResult(
|
||||||
{Key? key,
|
{Key? key,
|
||||||
required this.title,
|
required this.title,
|
||||||
@@ -31,65 +30,28 @@ class SearchResult extends StatefulWidget {
|
|||||||
id = json['id'],
|
id = json['id'],
|
||||||
super(key: key);
|
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() {
|
List<Widget> _buildOptions() {
|
||||||
return <OptionItem>[
|
return <OptionItem>[
|
||||||
OptionItem(
|
OptionItem(
|
||||||
title: "Open Artist Page in Genius",
|
title: "Open Artist Page in Genius",
|
||||||
icon: Icons.library_music,
|
icon: Icons.library_music,
|
||||||
tapFunction: () async {
|
onTap: () async {
|
||||||
if (!await launch(widget.artistUrl)) {
|
if (!await launch(artistUrl)) {
|
||||||
throw 'could not launch ${widget.artistUrl}';
|
throw 'could not launch $artistUrl';
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
OptionItem(
|
OptionItem(
|
||||||
title: "Open Song Page in Genius",
|
title: "Open Song Page in Genius",
|
||||||
icon: Icons.music_note,
|
icon: Icons.music_note,
|
||||||
tapFunction: () async {
|
onTap: () async {
|
||||||
if (!await launch(widget.url)) {
|
if (!await launch(url)) {
|
||||||
throw 'could not launch ${widget.url}';
|
throw 'could not launch $url';
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showOptionsBottomSheet() {
|
void _showOptionsBottomSheet(BuildContext context) {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
@@ -109,37 +71,31 @@ class _SearchResultState extends State<SearchResult> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Material(
|
return Material(
|
||||||
child: Container(
|
|
||||||
margin: const EdgeInsets.only(top: 0.5),
|
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => LyricsPage(
|
builder: (context) => LyricsPage(
|
||||||
title: widget.title,
|
title: title,
|
||||||
artist: widget.artist,
|
artist: artist,
|
||||||
url: widget.url,
|
url: url,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onLongPress: () => _showOptionsBottomSheet(),
|
onLongPress: () => _showOptionsBottomSheet(context),
|
||||||
child: Ink(
|
child: Ink(
|
||||||
height: 75,
|
height: 75,
|
||||||
color: Colors.white10,
|
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
margin: const EdgeInsets.only(right: 20),
|
margin: const EdgeInsets.only(right: 20),
|
||||||
child: Image.network(
|
child: Image.network(
|
||||||
widget.thumbnail,
|
thumbnail,
|
||||||
width: 75,
|
width: 75,
|
||||||
height: 75,
|
height: 75,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
errorBuilder: (context, error, stackTrace) {
|
errorBuilder: (context, error, stackTrace) {
|
||||||
Fluttertoast.showToast(
|
|
||||||
msg: 'Failed to load album art for: ${widget.title}',
|
|
||||||
toastLength: Toast.LENGTH_LONG);
|
|
||||||
return const Center(
|
return const Center(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: 75,
|
width: 75,
|
||||||
@@ -155,18 +111,18 @@ class _SearchResultState extends State<SearchResult> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
widget.title,
|
title,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.black,
|
color: Theme.of(context).textTheme.bodyText1?.color,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
widget.artist,
|
artist,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.black54,
|
color: Theme.of(context).textTheme.subtitle1?.color,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
),
|
),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
@@ -178,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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
+99
-1
@@ -191,6 +191,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.7.0"
|
version: "1.7.0"
|
||||||
|
nested:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: nested
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -198,6 +205,27 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.1"
|
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:
|
petitparser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -226,6 +254,69 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2.4"
|
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:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -357,6 +448,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.4"
|
version: "0.1.4"
|
||||||
|
win32:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: win32
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.6.1"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -372,5 +470,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "5.3.1"
|
version: "5.3.1"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.17.0-0 <3.0.0"
|
dart: ">=2.17.0 <3.0.0"
|
||||||
flutter: ">=2.10.0"
|
flutter: ">=2.10.0"
|
||||||
|
|||||||
+3
-1
@@ -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.
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# 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
|
||||||
version: 0.2.1+1
|
version: 0.3.0+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.16.1 <3.0.0"
|
sdk: ">=2.16.1 <3.0.0"
|
||||||
@@ -35,6 +35,8 @@ dependencies:
|
|||||||
url_launcher: ^6.0.20
|
url_launcher: ^6.0.20
|
||||||
web_scraper: ^0.1.4
|
web_scraper: ^0.1.4
|
||||||
fluttertoast: ^8.0.9
|
fluttertoast: ^8.0.9
|
||||||
|
shared_preferences: ^2.0.15
|
||||||
|
provider: ^6.0.3
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user