better gestures, more settings, stability fixes
This commit is contained in:
parent
7a1395220c
commit
f005073ad5
4 changed files with 151 additions and 75 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package vision.flux.plasmoid
|
||||
package com.randogoth.plasmoid
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
206
lib/main.dart
206
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<AppLauncherScreen> createState() => _AppLauncherScreenState();
|
||||
}
|
||||
|
||||
class _AppLauncherScreenState extends State<AppLauncherScreen> with WidgetsBindingObserver {
|
||||
|
|
@ -84,6 +85,7 @@ class _AppLauncherScreenState extends State<AppLauncherScreen> with WidgetsBindi
|
|||
List<AppInfo> apps = [];
|
||||
List<AppInfo> filteredApps = [];
|
||||
bool isLoading = true;
|
||||
bool _loadingApps = false;
|
||||
StreamSubscription<ApplicationEvent>? _appsChangeSub;
|
||||
|
||||
@override
|
||||
|
|
@ -115,84 +117,93 @@ class _AppLauncherScreenState extends State<AppLauncherScreen> with WidgetsBindi
|
|||
}
|
||||
|
||||
Future<void> _loadInstalledApps() async {
|
||||
List<Application> installedApps = await DeviceApps.getInstalledApplications(
|
||||
includeAppIcons: true,
|
||||
onlyAppsWithLaunchIntent: true,
|
||||
includeSystemApps: true,
|
||||
);
|
||||
|
||||
List<AppInfo> 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<String> 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<Application> installedApps = await DeviceApps.getInstalledApplications(
|
||||
includeAppIcons: true,
|
||||
onlyAppsWithLaunchIntent: true,
|
||||
includeSystemApps: true,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
setState(() {
|
||||
apps = loadedApps;
|
||||
filteredApps = apps;
|
||||
isLoading = false;
|
||||
});
|
||||
_filterApps();
|
||||
if (!mounted) return;
|
||||
|
||||
List<AppInfo> 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<String> 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<AppSettings>().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<AppSettings>().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<AppSettings>().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<void> _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<AppLauncherScreen> with WidgetsBindi
|
|||
}
|
||||
|
||||
Future<bool> _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<void> _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<AppLauncherScreen> 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<AppLauncherScreen> 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<AppLauncherScreen> 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<AppLauncherScreen> 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<AppLauncherScreen> with WidgetsBindi
|
|||
}
|
||||
|
||||
Widget _buildTrailingIcon(AppInfo app) {
|
||||
if (!context.read<AppSettings>().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<AppLauncherScreen> with WidgetsBindi
|
|||
onSelected: (String value) async {
|
||||
final settings = context.read<AppSettings>();
|
||||
switch (value) {
|
||||
case 'Refresh':
|
||||
_loadInstalledApps();
|
||||
break;
|
||||
case 'Toggle Tags':
|
||||
settings.updateSetting('showTags', !settings.showTags);
|
||||
break;
|
||||
|
|
@ -436,6 +459,13 @@ class _AppLauncherScreenState extends State<AppLauncherScreen> 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<AppLauncherScreen> with WidgetsBindi
|
|||
itemBuilder: (BuildContext context) {
|
||||
final settings = context.read<AppSettings>();
|
||||
return [
|
||||
PopupMenuItem<String>(
|
||||
value: 'Refresh',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.refresh),
|
||||
title: Text('Refresh App List'),
|
||||
),
|
||||
),
|
||||
PopupMenuItem<String>(
|
||||
value: 'Toggle Tags',
|
||||
child: ListTile(
|
||||
|
|
@ -458,6 +495,20 @@ class _AppLauncherScreenState extends State<AppLauncherScreen> with WidgetsBindi
|
|||
title: Text(settings.showIcons ? 'Hide Icons' : 'Show Icons'),
|
||||
),
|
||||
),
|
||||
PopupMenuItem<String>(
|
||||
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<String>(
|
||||
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<String>(
|
||||
value: 'Toggle Color Icons',
|
||||
child: ListTile(
|
||||
|
|
@ -508,18 +559,29 @@ class _AppLauncherScreenState extends State<AppLauncherScreen> 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<AppSettings>().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.
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue