From f005073ad5f54cb2b5d52b2d9345cbece37a7178 Mon Sep 17 00:00:00 2001 From: randogoth Date: Fri, 19 Jun 2026 13:32:48 +0300 Subject: [PATCH] better gestures, more settings, stability fixes --- android/app/build.gradle | 4 +- .../plasmoid/MainActivity.kt | 2 +- lib/main.dart | 208 ++++++++++++------ lib/settings.dart | 12 + 4 files changed, 151 insertions(+), 75 deletions(-) rename android/app/src/main/kotlin/com/{example => randogoth}/plasmoid/MainActivity.kt (74%) diff --git a/android/app/build.gradle b/android/app/build.gradle index d5c1046..8fe74b2 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -6,7 +6,7 @@ plugins { } android { - namespace = "vision.flux.plasmoid" + namespace = "com.randogoth.plasmoid" compileSdk = flutter.compileSdkVersion ndkVersion = flutter.ndkVersion compileSdkVersion 36 @@ -22,7 +22,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId = "vision.flux.plasmoid" + applicationId = "com.randogoth.plasmoid" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = flutter.minSdkVersion diff --git a/android/app/src/main/kotlin/com/example/plasmoid/MainActivity.kt b/android/app/src/main/kotlin/com/randogoth/plasmoid/MainActivity.kt similarity index 74% rename from android/app/src/main/kotlin/com/example/plasmoid/MainActivity.kt rename to android/app/src/main/kotlin/com/randogoth/plasmoid/MainActivity.kt index 6b58045..7bf9646 100644 --- a/android/app/src/main/kotlin/com/example/plasmoid/MainActivity.kt +++ b/android/app/src/main/kotlin/com/randogoth/plasmoid/MainActivity.kt @@ -1,4 +1,4 @@ -package vision.flux.plasmoid +package com.randogoth.plasmoid import io.flutter.embedding.android.FlutterActivity diff --git a/lib/main.dart b/lib/main.dart index 9a09125..65bc817 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:device_apps/device_apps.dart'; @@ -74,8 +73,10 @@ class AppInfo { } class AppLauncherScreen extends StatefulWidget { + const AppLauncherScreen({super.key}); + @override - _AppLauncherScreenState createState() => _AppLauncherScreenState(); + State createState() => _AppLauncherScreenState(); } class _AppLauncherScreenState extends State with WidgetsBindingObserver { @@ -84,6 +85,7 @@ class _AppLauncherScreenState extends State with WidgetsBindi List apps = []; List filteredApps = []; bool isLoading = true; + bool _loadingApps = false; StreamSubscription? _appsChangeSub; @override @@ -115,84 +117,93 @@ class _AppLauncherScreenState extends State with WidgetsBindi } Future _loadInstalledApps() async { - List installedApps = await DeviceApps.getInstalledApplications( - includeAppIcons: true, - onlyAppsWithLaunchIntent: true, - includeSystemApps: true, - ); - - List loadedApps = installedApps.map((app) { - final lastUsedMillis = SharedPreferencesService().getInt("lastUsed_${app.packageName}"); - final storedTags = SharedPreferencesService().getString("tags_${app.packageName}"); - final isPinned = SharedPreferencesService().getBool("isPinned_${app.packageName}", false); - final isDemoted = SharedPreferencesService().getBool("isDemoted_${app.packageName}", false); - - List tags = []; - - if (storedTags != null && storedTags.trim().isNotEmpty) { - tags = storedTags.split(" "); - } else 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, - isPinned: isPinned, - isDemoted: isDemoted, + if (_loadingApps) return; + _loadingApps = true; + try { + List installedApps = await DeviceApps.getInstalledApplications( + includeAppIcons: true, + onlyAppsWithLaunchIntent: true, + includeSystemApps: true, ); - }).toList(); - setState(() { - apps = loadedApps; - filteredApps = apps; - isLoading = false; - }); - _filterApps(); + if (!mounted) return; + + List loadedApps = installedApps.map((app) { + final lastUsedMillis = SharedPreferencesService().getInt("lastUsed_${app.packageName}"); + final storedTags = SharedPreferencesService().getString("tags_${app.packageName}"); + final isPinned = SharedPreferencesService().getBool("isPinned_${app.packageName}", false); + final isDemoted = SharedPreferencesService().getBool("isDemoted_${app.packageName}", false); + + List tags = []; + + if (storedTags != null && storedTags.trim().isNotEmpty) { + tags = storedTags.split(" "); + } else { + 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, + isPinned: isPinned, + isDemoted: isDemoted, + ); + }).toList(); + + setState(() { + apps = loadedApps; + isLoading = false; + }); + _filterApps(); + } finally { + _loadingApps = false; + } } void _filterApps() { String query = searchController.text.toLowerCase(); setState(() { + final hideDemoted = context.read().hideDemoted; filteredApps = apps.where((app) { + if (hideDemoted && app.isDemoted) return false; bool matchesName = app.name.toLowerCase().contains(query); bool matchesTag = app.tags.any((tag) => tag.toLowerCase().contains(query)); return matchesName || matchesTag; }).toList(); // Sort pinned apps first, then by last used or alphabetically, then demoted apps + final sortLastUsed = context.read().sortLastUsed; filteredApps.sort((a, b) { if (a.isPinned && !b.isPinned) return -1; if (!a.isPinned && b.isPinned) return 1; if (a.isDemoted && !b.isDemoted) return 1; if (!a.isDemoted && b.isDemoted) return -1; - if (context.read().sortLastUsed) { + if (sortLastUsed) { DateTime aTime = a.lastUsed ?? DateTime.fromMillisecondsSinceEpoch(0); DateTime bTime = b.lastUsed ?? DateTime.fromMillisecondsSinceEpoch(0); - return bTime.compareTo(aTime); - } else { - return a.name.compareTo(b.name); + final timeCompare = bTime.compareTo(aTime); + if (timeCompare != 0) return timeCompare; } + return a.name.compareTo(b.name); }); }); } Future _launchApp(AppInfo app) async { bool launched = await DeviceApps.openApp(app.packageName); + if (!mounted) return; if (launched) { DateTime now = DateTime.now(); await SharedPreferencesService().setInt("lastUsed_${app.packageName}", now.millisecondsSinceEpoch); - setState(() { - app.lastUsed = now; - _filterApps(); - }); + setState(() { app.lastUsed = now; }); + _filterApps(); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Could not launch ${app.name}')), @@ -201,16 +212,17 @@ class _AppLauncherScreenState extends State with WidgetsBindi } Future _handleDismiss(DismissDirection direction, AppInfo app) async { - if (direction == DismissDirection.endToStart) { - _openAppInfo(app); - } else if (direction == DismissDirection.startToEnd) { - _editAppTags(app); + if (direction == DismissDirection.startToEnd) { + await (app.isDemoted ? _toggleDemote(app) : _togglePin(app)); + } else if (direction == DismissDirection.endToStart) { + await (app.isPinned ? _togglePin(app) : _toggleDemote(app)); } return false; } Future _openAppInfo(AppInfo app) async { bool launched = await DeviceApps.openAppSettings(app.packageName); + if (!mounted) return; if (!launched) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Could not launch ${app.name} Settings')), @@ -226,19 +238,19 @@ class _AppLauncherScreenState extends State with WidgetsBindi mainAxisSize: MainAxisSize.min, children: [ ListTile( - leading: Icon(app.isPinned ? Icons.push_pin : Icons.push_pin_outlined), - title: Text(app.isPinned ? 'Unpin' : 'Pin'), - onTap: () async { - Navigator.pop(context); // Close the menu - await _togglePin(app); + leading: Icon(Icons.edit), + title: Text('Edit Tags'), + onTap: () { + Navigator.pop(context); + _editAppTags(app); }, ), ListTile( - leading: Icon(app.isDemoted ? Icons.arrow_upward : Icons.arrow_downward), - title: Text(app.isDemoted ? 'Reset' : 'Demote'), - onTap: () async { - Navigator.pop(context); // Close the menu - await _toggleDemote(app); + leading: Icon(Icons.info_outline), + title: Text('App Info'), + onTap: () { + Navigator.pop(context); + _openAppInfo(app); }, ), ], @@ -255,6 +267,9 @@ class _AppLauncherScreenState extends State with WidgetsBindi } }); await SharedPreferencesService().setBool("isPinned_${app.packageName}", app.isPinned); + if (app.isPinned) { // pinning clears demote + await SharedPreferencesService().setBool("isDemoted_${app.packageName}", false); + } _filterApps(); } @@ -266,6 +281,9 @@ class _AppLauncherScreenState extends State with WidgetsBindi } }); await SharedPreferencesService().setBool("isDemoted_${app.packageName}", app.isDemoted); + if (app.isDemoted) { // demoting clears pin + await SharedPreferencesService().setBool("isPinned_${app.packageName}", false); + } _filterApps(); } @@ -290,6 +308,7 @@ class _AppLauncherScreenState extends State with WidgetsBindi ..addAll(tagController.text.split(" ").where((t) => t.isNotEmpty)); }); await SharedPreferencesService().setString("tags_${app.packageName}", app.tags.join(" ")); + if (!mounted) return; Navigator.of(context).pop(); _filterApps(); }, @@ -376,12 +395,13 @@ class _AppLauncherScreenState extends State with WidgetsBindi } Widget _buildTrailingIcon(AppInfo app) { + if (!context.read().showStatusIcons) return SizedBox.shrink(); if (app.isPinned) { - return Icon(Icons.push_pin, color: Colors.grey); // Pin icon for pinned apps + return Icon(Icons.push_pin, color: Colors.grey); } else if (app.isDemoted) { - return Icon(Icons.disabled_by_default_outlined, color: Colors.grey); // Demote icon for demoted apps + return Icon(Icons.arrow_downward, color: Colors.grey); } - return SizedBox.shrink(); // No icon for normal apps + return SizedBox.shrink(); } @override @@ -416,6 +436,9 @@ class _AppLauncherScreenState extends State with WidgetsBindi onSelected: (String value) async { final settings = context.read(); switch (value) { + case 'Refresh': + _loadInstalledApps(); + break; case 'Toggle Tags': settings.updateSetting('showTags', !settings.showTags); break; @@ -436,6 +459,13 @@ class _AppLauncherScreenState extends State with WidgetsBindi settings.updateSetting('autoFocus', !settings.autoFocus); if (!settings.autoFocus) FocusScope.of(context).unfocus(); break; + case 'Toggle StatusIcons': + settings.updateSetting('showStatusIcons', !settings.showStatusIcons); + break; + case 'Toggle HideDemoted': + settings.updateSetting('hideDemoted', !settings.hideDemoted); + _filterApps(); + break; case 'About': _showInfoPage(context, "About", aboutContent); break; @@ -444,6 +474,13 @@ class _AppLauncherScreenState extends State with WidgetsBindi itemBuilder: (BuildContext context) { final settings = context.read(); return [ + PopupMenuItem( + value: 'Refresh', + child: ListTile( + leading: Icon(Icons.refresh), + title: Text('Refresh App List'), + ), + ), PopupMenuItem( value: 'Toggle Tags', child: ListTile( @@ -458,6 +495,20 @@ class _AppLauncherScreenState extends State with WidgetsBindi title: Text(settings.showIcons ? 'Hide Icons' : 'Show Icons'), ), ), + PopupMenuItem( + value: 'Toggle StatusIcons', + child: ListTile( + leading: Icon(settings.showStatusIcons ? Icons.label_off_outlined : Icons.label_outlined), + title: Text(settings.showStatusIcons ? 'Hide Status Icons' : 'Show Status Icons'), + ), + ), + PopupMenuItem( + value: 'Toggle HideDemoted', + child: ListTile( + leading: Icon(settings.hideDemoted ? Icons.visibility : Icons.visibility_off), + title: Text(settings.hideDemoted ? 'Show Demoted Apps' : 'Hide Demoted Apps'), + ), + ), PopupMenuItem( value: 'Toggle Color Icons', child: ListTile( @@ -508,18 +559,29 @@ class _AppLauncherScreenState extends State with WidgetsBindi final app = filteredApps[index]; return Dismissible( key: Key(app.packageName), + direction: app.isPinned + ? DismissDirection.endToStart + : app.isDemoted + ? DismissDirection.startToEnd + : DismissDirection.horizontal, confirmDismiss: (direction) => _handleDismiss(direction, app), background: Container( - color: Colors.orange, + color: app.isDemoted ? Colors.grey : Colors.teal, alignment: Alignment.centerLeft, padding: EdgeInsets.only(left: 20.0), - child: Icon(Icons.edit, color: Colors.white), + child: Icon( + app.isDemoted ? Icons.arrow_upward : Icons.push_pin, + color: Colors.white, + ), ), secondaryBackground: Container( - color: Colors.green, + color: app.isPinned ? Colors.grey : Colors.deepOrange, alignment: Alignment.centerRight, padding: EdgeInsets.only(right: 20.0), - child: Icon(Icons.info, color: Colors.white), + child: Icon( + app.isPinned ? Icons.push_pin_outlined : Icons.arrow_downward, + color: Colors.white, + ), ), child: ListTile( leading: context.read().showIcons ? _buildAppIcon(app) : null, @@ -554,12 +616,14 @@ 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. +Long-press an app and choose **Edit Tags** to assign tags. 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. + +Swipe right on an app to pin it to the top of the list (swipe right again to unpin). +Swipe left to demote it to the bottom (swipe left again to restore). +Long-press an app to edit its tags or open its system settings page. Use the menu button (plasmoid) to toggle settings such as icon visibility, color mode, theme, sorting (alphabetically or by last used), and auto-focus. --- diff --git a/lib/settings.dart b/lib/settings.dart index d23892c..62d1ddf 100644 --- a/lib/settings.dart +++ b/lib/settings.dart @@ -8,6 +8,8 @@ class AppSettings extends ChangeNotifier { bool _darkTheme = false; bool _sortLastUsed = true; bool _autoFocus = true; + bool _showStatusIcons = true; + bool _hideDemoted = false; bool get showTags => _showTags; bool get showIcons => _showIcons; @@ -15,6 +17,8 @@ class AppSettings extends ChangeNotifier { bool get darkTheme => _darkTheme; bool get sortLastUsed => _sortLastUsed; bool get autoFocus => _autoFocus; + bool get showStatusIcons => _showStatusIcons; + bool get hideDemoted => _hideDemoted; final SharedPreferencesService _prefsService = SharedPreferencesService(); @@ -29,6 +33,8 @@ class AppSettings extends ChangeNotifier { _darkTheme = _prefsService.getBool('darkTheme', false); _sortLastUsed = _prefsService.getBool('sortLastUsed', true); _autoFocus = _prefsService.getBool('autoFocus', true); + _showStatusIcons = _prefsService.getBool('showStatusIcons', true); + _hideDemoted = _prefsService.getBool('hideDemoted', false); notifyListeners(); } @@ -52,6 +58,12 @@ class AppSettings extends ChangeNotifier { case 'autoFocus': _autoFocus = value; break; + case 'showStatusIcons': + _showStatusIcons = value; + break; + case 'hideDemoted': + _hideDemoted = value; + break; } await _prefsService.setBool(key, value); notifyListeners(); // Notify listeners after updating the setting