diff --git a/lib/main.dart b/lib/main.dart index 3b14d0b..51d7952 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,101 +5,47 @@ import 'package:device_apps/device_apps.dart'; import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:plasmoid/preferences.dart'; import 'package:plasmoid/read_only.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +import 'package:plasmoid/settings.dart'; +import 'package:provider/provider.dart'; import 'day.dart'; import 'night.dart'; -void main() { - LicenseRegistry.addLicense(() async* { - final license = await rootBundle.loadString('google_fonts/OFL.txt'); - yield LicenseEntryWithLineBreaks(['google_fonts'], license); - }); - runApp(MyApp()); +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await SharedPreferencesService().initialize(); + runApp( + ChangeNotifierProvider( + create: (_) => AppSettings(), + child: MyApp(), + ), + ); } -class AppSettings { - bool showTags; - bool showIcons; - bool colorIcons; // true = color icons; false = monochrome icons - bool darkTheme; - bool sortAlphabetically; - bool autoFocus; // new - - AppSettings({ - this.showTags = true, - this.showIcons = true, - this.colorIcons = true, - this.darkTheme = false, - this.sortAlphabetically = false, - this.autoFocus = true, - }); -} - -class MyApp extends StatefulWidget { - @override - _MyAppState createState() => _MyAppState(); -} - -class _MyAppState extends State { - AppSettings settings = AppSettings(); - - @override - void initState() { - super.initState(); - _loadSettings(); - } - - Future _loadSettings() async { - SharedPreferences prefs = await SharedPreferences.getInstance(); - setState(() { - settings.showTags = prefs.getBool('showTags') ?? true; - settings.showIcons = prefs.getBool('showIcons') ?? true; - settings.colorIcons = prefs.getBool('colorIcons') ?? true; - settings.darkTheme = prefs.getBool('darkTheme') ?? false; - settings.sortAlphabetically = prefs.getBool('sortAlphabetically') ?? false; - settings.autoFocus = prefs.getBool('autoFocus') ?? true; - }); - } - - Future _updateSetting(String key, bool value) async { - SharedPreferences prefs = await SharedPreferences.getInstance(); - await prefs.setBool(key, value); - } - - void _updateSettings(AppSettings newSettings) { - setState(() { - settings = newSettings; - }); - _updateSetting('showTags', settings.showTags); - _updateSetting('showIcons', settings.showIcons); - _updateSetting('colorIcons', settings.colorIcons); - _updateSetting('darkTheme', settings.darkTheme); - _updateSetting('sortAlphabetically', settings.sortAlphabetically); - _updateSetting('autoFocus', settings.autoFocus); - } - +class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( - title: 'Plasmoid Launcher', - theme: ThemeData( - brightness: Brightness.light, - scaffoldBackgroundColor: Color(0xffF6F4F1), - appBarTheme: AppBarTheme(backgroundColor: Color(0xffF6F4F1)), - textTheme: GoogleFonts.ibmPlexMonoTextTheme(dayText), - ), - darkTheme: ThemeData( - brightness: Brightness.dark, - scaffoldBackgroundColor: Color(0xFF121212), - appBarTheme: AppBarTheme(backgroundColor: Color(0xFF121212)), - textTheme: GoogleFonts.ibmPlexMonoTextTheme(nightText), - ), - themeMode: settings.darkTheme ? ThemeMode.dark : ThemeMode.light, - home: AppLauncherScreen( - settings: settings, - onSettingsChanged: _updateSettings, - ), + return Consumer( + builder: (context, settings, child) { + return MaterialApp( + title: 'Plasmoid Launcher', + theme: ThemeData( + brightness: Brightness.light, + scaffoldBackgroundColor: Color(0xffF6F4F1), + appBarTheme: AppBarTheme(backgroundColor: Color(0xffF6F4F1)), + textTheme: GoogleFonts.ibmPlexMonoTextTheme(dayText), + ), + darkTheme: ThemeData( + brightness: Brightness.dark, + scaffoldBackgroundColor: Color(0xFF121212), + appBarTheme: AppBarTheme(backgroundColor: Color(0xFF121212)), + textTheme: GoogleFonts.ibmPlexMonoTextTheme(nightText), + ), + themeMode: settings.darkTheme ? ThemeMode.dark : ThemeMode.light, + home: AppLauncherScreen(), + ); + }, ); } } @@ -121,16 +67,11 @@ class AppInfo { } class AppLauncherScreen extends StatefulWidget { - final AppSettings settings; - final Function(AppSettings) onSettingsChanged; - - AppLauncherScreen({required this.settings, required this.onSettingsChanged}); - @override _AppLauncherScreenState createState() => _AppLauncherScreenState(); } -class _AppLauncherScreenState extends State { +class _AppLauncherScreenState extends State with WidgetsBindingObserver { final TextEditingController searchController = TextEditingController(); final FocusNode searchFocusNode = FocusNode(); List apps = []; @@ -140,38 +81,47 @@ class _AppLauncherScreenState extends State { @override void initState() { super.initState(); + WidgetsBinding.instance.addObserver(this); _loadInstalledApps(); searchController.addListener(_filterApps); WidgetsBinding.instance.addPostFrameCallback((_) { - if (widget.settings.autoFocus) { + if (context.read().autoFocus) { FocusScope.of(context).requestFocus(searchFocusNode); } }); } + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + _loadInstalledApps(); + } + } + Future _loadInstalledApps() async { - SharedPreferences prefs = await SharedPreferences.getInstance(); List installedApps = await DeviceApps.getInstalledApplications( includeAppIcons: true, onlyAppsWithLaunchIntent: true, - includeSystemApps: true + includeSystemApps: true, ); List loadedApps = installedApps.map((app) { - final lastUsedMillis = prefs.getInt("lastUsed_${app.packageName}"); - final storedTags = prefs.getString("tags_${app.packageName}"); + final lastUsedMillis = SharedPreferencesService().getInt("lastUsed_${app.packageName}"); + final storedTags = SharedPreferencesService().getString("tags_${app.packageName}"); List tags = []; if (storedTags != null && storedTags.trim().isNotEmpty) { - // Use the stored user-edited tags. tags = storedTags.split(" "); - } else { - // Fall back to using the auto-generated category tag. - if (app.category != null) { - String categoryStr = app.category.toString().split('.').last; - if (categoryStr.toLowerCase() != 'undefined') { - tags.add('@' + categoryStr.toLowerCase()); - } + } else if (app.category != null) { + String categoryStr = app.category.toString().split('.').last; + if (categoryStr.toLowerCase() != 'undefined') { + tags.add('@${categoryStr.toLowerCase()}'); } } @@ -180,9 +130,7 @@ class _AppLauncherScreenState extends State { name: app.appName, packageName: app.packageName, tags: tags, - lastUsed: lastUsedMillis != null - ? DateTime.fromMillisecondsSinceEpoch(lastUsedMillis) - : null, + lastUsed: lastUsedMillis != null ? DateTime.fromMillisecondsSinceEpoch(lastUsedMillis) : null, ); }).toList(); @@ -203,24 +151,14 @@ class _AppLauncherScreenState extends State { return matchesName || matchesTag; }).toList(); - if (widget.settings.sortAlphabetically) { - filteredApps.sort((a, b) => a.name.compareTo(b.name)); - print('Sorting alphabetically'); - } else { + if (context.read().sortLastUsed) { filteredApps.sort((a, b) { - // If both have a lastUsed timestamp, sort descending. - if (a.lastUsed != null && b.lastUsed != null) { - int cmp = b.lastUsed!.compareTo(a.lastUsed!); - if (cmp != 0) return cmp; - return a.name.compareTo(b.name); - } - // If one has a timestamp, that one comes first. - if (a.lastUsed != null) return -1; - if (b.lastUsed != null) return 1; - // Both null: fallback alphabetical. - return a.name.compareTo(b.name); + DateTime aTime = a.lastUsed ?? DateTime.fromMillisecondsSinceEpoch(0); + DateTime bTime = b.lastUsed ?? DateTime.fromMillisecondsSinceEpoch(0); + return bTime.compareTo(aTime); }); - print('Sorting by last used, with fallback alphabetical'); + } else { + filteredApps.sort((a, b) => a.name.compareTo(b.name)); } }); } @@ -229,9 +167,7 @@ class _AppLauncherScreenState extends State { bool launched = await DeviceApps.openApp(app.packageName); if (launched) { DateTime now = DateTime.now(); - SharedPreferences prefs = await SharedPreferences.getInstance(); - await prefs.setInt("lastUsed_${app.packageName}", now.millisecondsSinceEpoch); - // Update the lastUsed timestamp and re-sort immediately. + await SharedPreferencesService().setInt("lastUsed_${app.packageName}", now.millisecondsSinceEpoch); setState(() { app.lastUsed = now; _filterApps(); @@ -262,8 +198,7 @@ class _AppLauncherScreenState extends State { } void _editAppTags(AppInfo app) { - TextEditingController tagController = - TextEditingController(text: app.tags.join(" ")); + TextEditingController tagController = TextEditingController(text: app.tags.join(" ")); showDialog( context: context, builder: (_) => AlertDialog( @@ -282,9 +217,7 @@ class _AppLauncherScreenState extends State { ..clear() ..addAll(tagController.text.split(" ").where((t) => t.isNotEmpty)); }); - // Persist the updated tags. - SharedPreferences prefs = await SharedPreferences.getInstance(); - await prefs.setString("tags_${app.packageName}", app.tags.join(" ")); + await SharedPreferencesService().setString("tags_${app.packageName}", app.tags.join(" ")); Navigator.of(context).pop(); _filterApps(); }, @@ -301,20 +234,19 @@ class _AppLauncherScreenState extends State { List _formatTags(String content) { final tagPattern = RegExp(r'([#@+][a-zA-Z0-9_]+)'); - final Set tagSet = {}; // Avoid duplicate tags + final Set tagSet = {}; for (final match in tagPattern.allMatches(content)) { tagSet.add(match.group(0)!); } - // Sort tags: +tags before @tags before #tags final sortedTags = tagSet.toList() ..sort((a, b) { if (a.startsWith('+') && !b.startsWith('+')) return -1; if (b.startsWith('+') && !a.startsWith('+')) return 1; if (a.startsWith('@') && !b.startsWith('@')) return -1; if (b.startsWith('@') && !a.startsWith('@')) return 1; - return a.compareTo(b); // Default alphabetical order + return a.compareTo(b); }); return sortedTags.map((tag) { @@ -343,7 +275,7 @@ class _AppLauncherScreenState extends State { iconWidget = Icon(Icons.apps, size: 40); } - if (!widget.settings.colorIcons) { + if (!context.read().colorIcons) { iconWidget = ColorFiltered( colorFilter: ColorFilter.matrix([ 0.2126, 0.7152, 0.0722, 0, 0, @@ -358,14 +290,14 @@ class _AppLauncherScreenState extends State { return iconWidget; } - _showInfoPage(BuildContext context, String title, String content) { + void _showInfoPage(BuildContext context, String title, String content) { Navigator.push( context, MaterialPageRoute( builder: (context) => ReadOnlyPage( title: title, content: content, - isDarkMode: widget.settings.darkTheme, + isDarkMode: context.read().darkTheme, ), ), ); @@ -376,7 +308,7 @@ class _AppLauncherScreenState extends State { return Scaffold( appBar: AppBar( title: TextField( - focusNode: widget.settings.autoFocus ? searchFocusNode : null, + focusNode: context.read().autoFocus ? searchFocusNode : null, controller: searchController, decoration: InputDecoration( hintText: 'Search apps...', @@ -400,144 +332,88 @@ class _AppLauncherScreenState extends State { width: 30, color: Color(0xff38818a), ), - onSelected: (String value) { + onSelected: (String value) async { + final settings = context.read(); switch (value) { case 'Toggle Tags': - widget.onSettingsChanged(AppSettings( - showTags: !widget.settings.showTags, - showIcons: widget.settings.showIcons, - colorIcons: widget.settings.colorIcons, - darkTheme: widget.settings.darkTheme, - sortAlphabetically: widget.settings.sortAlphabetically, - autoFocus: widget.settings.autoFocus, - )); + settings.updateSetting('showTags', !settings.showTags); break; case 'Toggle Icons': - widget.onSettingsChanged(AppSettings( - showTags: widget.settings.showTags, - showIcons: !widget.settings.showIcons, - colorIcons: widget.settings.colorIcons, - darkTheme: widget.settings.darkTheme, - sortAlphabetically: widget.settings.sortAlphabetically, - autoFocus: widget.settings.autoFocus, - )); + settings.updateSetting('showIcons', !settings.showIcons); break; case 'Toggle Color Icons': - widget.onSettingsChanged(AppSettings( - showTags: widget.settings.showTags, - showIcons: widget.settings.showIcons, - colorIcons: !widget.settings.colorIcons, - darkTheme: widget.settings.darkTheme, - sortAlphabetically: widget.settings.sortAlphabetically, - autoFocus: widget.settings.autoFocus, - )); + settings.updateSetting('colorIcons', !settings.colorIcons); break; case 'Toggle Theme': - widget.onSettingsChanged(AppSettings( - showTags: widget.settings.showTags, - showIcons: widget.settings.showIcons, - colorIcons: widget.settings.colorIcons, - darkTheme: !widget.settings.darkTheme, - sortAlphabetically: widget.settings.sortAlphabetically, - autoFocus: widget.settings.autoFocus, - )); + settings.updateSetting('darkTheme', !settings.darkTheme); break; case 'Toggle Sorting': - widget.onSettingsChanged(AppSettings( - showTags: widget.settings.showTags, - showIcons: widget.settings.showIcons, - colorIcons: widget.settings.colorIcons, - darkTheme: widget.settings.darkTheme, - sortAlphabetically: !widget.settings.sortAlphabetically, - autoFocus: widget.settings.autoFocus, - )); + settings.updateSetting('sortLastUsed', !settings.sortLastUsed); _filterApps(); break; case 'Toggle AutoFocus': - widget.onSettingsChanged(AppSettings( - showTags: widget.settings.showTags, - showIcons: widget.settings.showIcons, - colorIcons: widget.settings.colorIcons, - darkTheme: widget.settings.darkTheme, - sortAlphabetically: widget.settings.sortAlphabetically, - autoFocus: !widget.settings.autoFocus, - )); - if (!widget.settings.autoFocus) FocusScope.of(context).unfocus(); + settings.updateSetting('autoFocus', !settings.autoFocus); + if (!settings.autoFocus) FocusScope.of(context).unfocus(); break; case 'About': _showInfoPage(context, "About", aboutContent); break; } }, - itemBuilder: (BuildContext context) => [ - PopupMenuItem( - value: 'Toggle Tags', - child: ListTile( - leading: Icon(widget.settings.showTags - ? Icons.short_text - : Icons.tag), - title: Text(widget.settings.showTags - ? 'Hide Tags' - : 'Show Tags'), + itemBuilder: (BuildContext context) { + final settings = context.read(); + return [ + PopupMenuItem( + value: 'Toggle Tags', + child: ListTile( + leading: Icon(settings.showTags ? Icons.short_text : Icons.tag), + title: Text(settings.showTags ? 'Hide Tags' : 'Show Tags'), + ), ), - ), - PopupMenuItem( - value: 'Toggle Icons', - child: ListTile( - leading: Icon(widget.settings.showIcons - ? Icons.apps - : Icons.apps_outlined), - title: Text(widget.settings.showIcons - ? 'Hide Icons' - : 'Show Icons'), + PopupMenuItem( + value: 'Toggle Icons', + child: ListTile( + leading: Icon(settings.showIcons ? Icons.apps : Icons.apps_outlined), + title: Text(settings.showIcons ? 'Hide Icons' : 'Show Icons'), + ), ), - ), - PopupMenuItem( - value: 'Toggle Color Icons', - child: ListTile( - leading: Icon(Icons.palette), - title: Text(widget.settings.colorIcons - ? 'Use Monochrome Icons' - : 'Use Color Icons'), + PopupMenuItem( + value: 'Toggle Color Icons', + child: ListTile( + leading: Icon(Icons.palette), + title: Text(settings.colorIcons ? 'Use Monochrome Icons' : 'Use Color Icons'), + ), ), - ), - PopupMenuItem( - value: 'Toggle Theme', - child: ListTile( - leading: Icon(widget.settings.darkTheme - ? Icons.light_mode - : Icons.dark_mode), - title: Text(widget.settings.darkTheme - ? 'Switch to Light Mode' - : 'Switch to Dark Mode'), + PopupMenuItem( + value: 'Toggle Theme', + child: ListTile( + leading: Icon(settings.darkTheme ? Icons.light_mode : Icons.dark_mode), + title: Text(settings.darkTheme ? 'Switch to Light Mode' : 'Switch to Dark Mode'), + ), ), - ), - PopupMenuItem( - value: 'Toggle Sorting', - child: ListTile( - leading: Icon(widget.settings.sortAlphabetically - ? Icons.access_time - : Icons.sort_by_alpha), - title: Text(widget.settings.sortAlphabetically - ? 'Sort Alphabetically' - : 'Sort by Last Used'), + PopupMenuItem( + value: 'Toggle Sorting', + child: ListTile( + leading: Icon(settings.sortLastUsed ? Icons.access_time : Icons.sort_by_alpha), + title: Text(settings.sortLastUsed ? 'Sort Alphabetically' : 'Sort by Last Used'), + ), ), - ), - PopupMenuItem( - value: 'Toggle AutoFocus', - child: ListTile( - leading: Icon(Icons.center_focus_strong), - title: Text(widget.settings.autoFocus ? 'Disable Auto Focus' : 'Enable Auto Focus'), + PopupMenuItem( + value: 'Toggle AutoFocus', + child: ListTile( + leading: Icon(Icons.center_focus_strong), + title: Text(settings.autoFocus ? 'Disable Auto Focus' : 'Enable Auto Focus'), + ), ), - ), - PopupMenuItem( - value: 'About', - child: ListTile( - leading: Icon(Icons.info_outline), - title: Text('About'), + PopupMenuItem( + value: 'About', + child: ListTile( + leading: Icon(Icons.info_outline), + title: Text('About'), + ), ), - ), - ], + ]; + }, ), ), ), @@ -565,18 +441,16 @@ class _AppLauncherScreenState extends State { child: Icon(Icons.info, color: Colors.white), ), child: ListTile( - leading: - widget.settings.showIcons ? _buildAppIcon(app) : null, + leading: context.read().showIcons ? _buildAppIcon(app) : null, title: Text(app.name), - subtitle: - widget.settings.showTags + subtitle: context.read().showTags ? RichText( - text: TextSpan( - style: DefaultTextStyle.of(context).style, - children: _formatTags(app.tags.join(' ')), // ✅ Use stored tags - ), - overflow: TextOverflow.ellipsis, - ) + text: TextSpan( + style: DefaultTextStyle.of(context).style, + children: _formatTags(app.tags.join(' ')), + ), + overflow: TextOverflow.ellipsis, + ) : null, onTap: () => _launchApp(app), ), diff --git a/lib/preferences.dart b/lib/preferences.dart new file mode 100644 index 0000000..91f535a --- /dev/null +++ b/lib/preferences.dart @@ -0,0 +1,20 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +class SharedPreferencesService { + static final SharedPreferencesService _instance = SharedPreferencesService._internal(); + factory SharedPreferencesService() => _instance; + SharedPreferencesService._internal(); + + Future initialize() async { + _prefs = await SharedPreferences.getInstance(); + } + + late SharedPreferences _prefs; + + bool getBool(String key, bool defaultValue) => _prefs.getBool(key) ?? defaultValue; + Future setBool(String key, bool value) => _prefs.setBool(key, value); + int? getInt(String key) => _prefs.getInt(key); + Future setInt(String key, int value) => _prefs.setInt(key, value); + String? getString(String key) => _prefs.getString(key); + Future setString(String key, String value) => _prefs.setString(key, value); +} \ No newline at end of file diff --git a/lib/settings.dart b/lib/settings.dart new file mode 100644 index 0000000..d23892c --- /dev/null +++ b/lib/settings.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:plasmoid/preferences.dart'; + +class AppSettings extends ChangeNotifier { + bool _showTags = true; + bool _showIcons = true; + bool _colorIcons = true; + bool _darkTheme = false; + bool _sortLastUsed = true; + bool _autoFocus = true; + + bool get showTags => _showTags; + bool get showIcons => _showIcons; + bool get colorIcons => _colorIcons; + bool get darkTheme => _darkTheme; + bool get sortLastUsed => _sortLastUsed; + bool get autoFocus => _autoFocus; + + final SharedPreferencesService _prefsService = SharedPreferencesService(); + + AppSettings() { + _loadSettings(); + } + + Future _loadSettings() async { + _showTags = _prefsService.getBool('showTags', true); + _showIcons = _prefsService.getBool('showIcons', true); + _colorIcons = _prefsService.getBool('colorIcons', true); + _darkTheme = _prefsService.getBool('darkTheme', false); + _sortLastUsed = _prefsService.getBool('sortLastUsed', true); + _autoFocus = _prefsService.getBool('autoFocus', true); + notifyListeners(); + } + + Future updateSetting(String key, bool value) async { + switch (key) { + case 'showTags': + _showTags = value; + break; + case 'showIcons': + _showIcons = value; + break; + case 'colorIcons': + _colorIcons = value; + break; + case 'darkTheme': + _darkTheme = value; + break; + case 'sortLastUsed': + _sortLastUsed = value; + break; + case 'autoFocus': + _autoFocus = value; + break; + } + await _prefsService.setBool(key, value); + notifyListeners(); // Notify listeners after updating the setting + } +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 1f9e914..9feffa7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -328,6 +328,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" path: dependency: transitive description: @@ -424,6 +432,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.1" + provider: + dependency: "direct main" + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.dev" + source: hosted + version: "6.1.2" scrollable_positioned_list: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 65d8954..72dea53 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: google_fonts: ^6.2.1 flutter_launcher_icons: ^0.14.3 flutter_code_editor: ^0.3.2 + provider: ^6.1.2 dev_dependencies: flutter_test: