import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; 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/read_only.dart'; import 'package:shared_preferences/shared_preferences.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()); } 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); } @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, ), ); } } class AppInfo { final Uint8List? icon; final String name; final String packageName; final List tags; DateTime? lastUsed; // New field to track last used time AppInfo({ this.icon, required this.name, required this.packageName, required this.tags, this.lastUsed, }); } 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 { final TextEditingController searchController = TextEditingController(); final FocusNode searchFocusNode = FocusNode(); List apps = []; List filteredApps = []; bool isLoading = true; @override void initState() { super.initState(); _loadInstalledApps(); searchController.addListener(_filterApps); WidgetsBinding.instance.addPostFrameCallback((_) { if (widget.settings.autoFocus) { FocusScope.of(context).requestFocus(searchFocusNode); } }); } Future _loadInstalledApps() async { SharedPreferences prefs = await SharedPreferences.getInstance(); List installedApps = await DeviceApps.getInstalledApplications( includeAppIcons: true, onlyAppsWithLaunchIntent: true, ); List loadedApps = installedApps.map((app) { final lastUsedMillis = prefs.getInt("lastUsed_${app.packageName}"); final storedTags = prefs.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()); } } } return AppInfo( icon: app is ApplicationWithIcon ? app.icon : null, name: app.appName, packageName: app.packageName, tags: tags, lastUsed: lastUsedMillis != null ? DateTime.fromMillisecondsSinceEpoch(lastUsedMillis) : null, ); }).toList(); setState(() { apps = loadedApps; filteredApps = apps; isLoading = false; }); _filterApps(); } void _filterApps() { String query = searchController.text.toLowerCase(); setState(() { filteredApps = apps.where((app) { bool matchesName = app.name.toLowerCase().contains(query); bool matchesTag = app.tags.any((tag) => tag.toLowerCase().contains(query)); return matchesName || matchesTag; }).toList(); if (widget.settings.sortAlphabetically) { filteredApps.sort((a, b) => a.name.compareTo(b.name)); print('Sorting alphabetically'); } else { 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); }); print('Sorting by last used, with fallback alphabetical'); } }); } Future _launchApp(AppInfo app) async { 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. setState(() { app.lastUsed = now; _filterApps(); }); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Could not launch ${app.name}')), ); } } Future _handleDismiss(DismissDirection direction, AppInfo app) async { if (direction == DismissDirection.endToStart) { _openAppInfo(app); } else if (direction == DismissDirection.startToEnd) { _editAppTags(app); } return false; } Future _openAppInfo(AppInfo app) async { bool launched = await DeviceApps.openAppSettings(app.packageName); if (!launched) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Could not launch ${app.name} Settings')), ); } } void _editAppTags(AppInfo app) { TextEditingController tagController = TextEditingController(text: app.tags.join(" ")); showDialog( context: context, builder: (_) => AlertDialog( title: Text('Edit Tags for ${app.name}'), content: TextField( controller: tagController, decoration: InputDecoration( hintText: 'Enter tags (e.g., #general, @context, +project)', ), ), actions: [ TextButton( onPressed: () async { setState(() { app.tags ..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(" ")); Navigator.of(context).pop(); _filterApps(); }, child: Text('Save'), ), TextButton( onPressed: () => Navigator.of(context).pop(), child: Text('Cancel'), ), ], ), ); } List _formatTags(String content) { final tagPattern = RegExp(r'([#@+][a-zA-Z0-9_]+)'); final Set tagSet = {}; // Avoid duplicate tags 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 sortedTags.map((tag) { Color tagColor = Colors.white; if (tag.startsWith('#')) tagColor = Colors.pinkAccent; if (tag.startsWith('+')) tagColor = Colors.cyan; if (tag.startsWith('@')) tagColor = Colors.orange; return TextSpan( text: "$tag ", style: TextStyle(color: tagColor, fontWeight: FontWeight.bold), ); }).toList(); } Widget _buildAppIcon(AppInfo app) { Widget iconWidget; if (app.icon != null) { iconWidget = Image.memory( app.icon!, width: 40, height: 40, fit: BoxFit.contain, ); } else { iconWidget = Icon(Icons.apps, size: 40); } if (!widget.settings.colorIcons) { iconWidget = ColorFiltered( colorFilter: ColorFilter.matrix([ 0.2126, 0.7152, 0.0722, 0, 0, 0.2126, 0.7152, 0.0722, 0, 0, 0.2126, 0.7152, 0.0722, 0, 0, 0, 0, 0, 1, 0, ]), child: iconWidget, ); } return iconWidget; } _showInfoPage(BuildContext context, String title, String content) { Navigator.push( context, MaterialPageRoute( builder: (context) => ReadOnlyPage( title: title, content: content, isDarkMode: widget.settings.darkTheme, ), ), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: TextField( focusNode: widget.settings.autoFocus ? searchFocusNode : null, controller: searchController, decoration: InputDecoration( hintText: 'Search apps...', border: InputBorder.none, ), style: TextStyle(color: Colors.white), ), actions: [ Padding( padding: EdgeInsets.symmetric(horizontal: 15), child: GestureDetector( onTap: () { if (searchController.text.isNotEmpty) { searchController.clear(); } }, child: PopupMenuButton( enabled: searchController.text.isEmpty, icon: SvgPicture.asset( "assets/plasmoid.svg", width: 30, color: Color(0xff38818a), ), onSelected: (String value) { 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, )); 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, )); 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, )); 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, )); 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, )); _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, )); 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'), ), ), 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 Color Icons', child: ListTile( leading: Icon(Icons.palette), title: Text(widget.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 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 AutoFocus', child: ListTile( leading: Icon(Icons.center_focus_strong), title: Text(widget.settings.autoFocus ? 'Disable Auto Focus' : 'Enable Auto Focus'), ), ), PopupMenuItem( value: 'About', child: ListTile( leading: Icon(Icons.info_outline), title: Text('About'), ), ), ], ), ), ), ], ), body: isLoading ? Center(child: CircularProgressIndicator()) : ListView.builder( itemCount: filteredApps.length, itemBuilder: (context, index) { final app = filteredApps[index]; return Dismissible( key: Key(app.packageName), confirmDismiss: (direction) => _handleDismiss(direction, app), background: Container( color: Colors.orange, alignment: Alignment.centerLeft, padding: EdgeInsets.only(left: 20.0), child: Icon(Icons.edit, color: Colors.white), ), secondaryBackground: Container( color: Colors.green, alignment: Alignment.centerRight, padding: EdgeInsets.only(right: 20.0), child: Icon(Icons.info, color: Colors.white), ), child: ListTile( leading: widget.settings.showIcons ? _buildAppIcon(app) : null, title: Text(app.name), subtitle: widget.settings.showTags ? RichText( text: TextSpan( style: DefaultTextStyle.of(context).style, children: _formatTags(app.tags.join(' ')), // ✅ Use stored tags ), overflow: TextOverflow.ellipsis, ) : null, onTap: () => _launchApp(app), ), ); }, ), ); } } String aboutContent = """ **Plasmoid** is a minimalist and opinionated app launcher for Android. ## Search & Launch: Open Plasmoid and start typing in the search bar to filter your apps and shared files by name or tag. Tap an app to launch it immediately. ## Tagging: Swipe right on any item to edit its tags. For shared files, a tag dialog appears immediately upon sharing. Tags help you organize items and are searchable using prefixes like @context, #general, or +project. ## Gestures & Settings: Swipe left on an app to view its settings. Use the menu button (plasmoid) to toggle settings such as icon visibility, color mode, theme, sorting (alphabetically or by last used), and auto-focus. --- Made with 💚 by randogothimport 'package:plasmoid/texts.dart'; Icon by [Game Icons.net](https://game-icons.net/). """;