Files
AutoTime-Tracker/lib/screens/category_management_screen.dart
2025-11-13 15:45:28 +08:00

291 lines
9.6 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../theme/app_theme.dart';
import '../services/category_service.dart';
import '../database/app_usage_dao.dart';
import '../widgets/empty_state_widget.dart';
import '../widgets/error_state_widget.dart';
class CategoryManagementScreen extends ConsumerStatefulWidget {
const CategoryManagementScreen({super.key});
@override
ConsumerState<CategoryManagementScreen> createState() => _CategoryManagementScreenState();
}
class _CategoryManagementScreenState extends ConsumerState<CategoryManagementScreen> {
final CategoryService _categoryService = CategoryService();
final AppUsageDao _appUsageDao = AppUsageDao();
final TextEditingController _searchController = TextEditingController();
String _searchQuery = '';
// 可用分类列表
final List<String> _availableCategories = ['work', 'study', 'entertainment', 'social', 'tool', 'other'];
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
Future<List<AppCategoryItem>> _loadAppCategories() async {
// 获取所有应用使用记录(最近使用的)
// 如果数据库为空返回空列表Web 平台会返回空列表)
final now = DateTime.now();
final startOfWeek = now.subtract(const Duration(days: 7));
final appUsages = await _appUsageDao.getAppUsages(
startTime: startOfWeek,
endTime: now,
);
// 如果没有数据,返回空列表
if (appUsages.isEmpty) {
return [];
}
// 获取自定义分类
final customCategories = await _categoryService.getAllCustomCategories();
// 去重并创建列表
final packageMap = <String, AppCategoryItem>{};
for (final usage in appUsages) {
if (!packageMap.containsKey(usage.packageName)) {
final currentCategory = customCategories[usage.packageName] ??
CategoryService.defaultCategories[usage.packageName] ??
'other';
packageMap[usage.packageName] = AppCategoryItem(
packageName: usage.packageName,
appName: usage.appName,
category: currentCategory,
isCustom: customCategories.containsKey(usage.packageName),
);
}
}
return packageMap.values.toList()
..sort((a, b) => a.appName.compareTo(b.appName));
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: const Text('应用分类'),
),
body: Column(
children: [
// 搜索框
Padding(
padding: const EdgeInsets.all(16),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: '搜索应用...',
prefixIcon: const Icon(Icons.search),
suffixIcon: _searchQuery.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
setState(() {
_searchQuery = '';
});
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
onChanged: (value) {
setState(() {
_searchQuery = value;
});
},
),
),
// 应用列表
Expanded(
child: FutureBuilder<List<AppCategoryItem>>(
future: _loadAppCategories(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return ErrorStateWidget.dataLoad(
message: _getErrorMessage(snapshot.error!),
onRetry: () {
setState(() {});
},
);
}
final items = snapshot.data ?? [];
final filteredItems = _searchQuery.isEmpty
? items
: items.where((item) {
return item.appName.toLowerCase().contains(_searchQuery.toLowerCase()) ||
item.packageName.toLowerCase().contains(_searchQuery.toLowerCase());
}).toList();
if (filteredItems.isEmpty) {
if (_searchQuery.isEmpty) {
return EmptyStateWidget.noApps(
onAction: () {
setState(() {});
},
);
} else {
return EmptyStateWidget.noSearchResults(query: _searchQuery);
}
}
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: filteredItems.length,
itemBuilder: (context, index) {
final item = filteredItems[index];
return _buildAppCategoryItem(context, theme, item);
},
);
},
),
),
],
),
);
}
Widget _buildAppCategoryItem(
BuildContext context,
ThemeData theme,
AppCategoryItem item,
) {
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
leading: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppTheme.getCategoryColor(item.category).withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.phone_android,
color: AppTheme.getCategoryColor(item.category),
),
),
title: Text(
item.appName,
style: theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
),
),
subtitle: Text(
item.packageName,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
),
trailing: PopupMenuButton<String>(
icon: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppTheme.getCategoryColor(item.category).withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
AppTheme.getCategoryName(item.category),
style: TextStyle(
color: AppTheme.getCategoryColor(item.category),
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
const SizedBox(width: 8),
const Icon(Icons.arrow_drop_down),
],
),
onSelected: (String category) async {
await _categoryService.setCategory(item.packageName, category);
setState(() {});
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('已将 ${item.appName} 分类为 ${AppTheme.getCategoryName(category)}'),
duration: const Duration(seconds: 2),
),
);
}
},
itemBuilder: (BuildContext context) {
return _availableCategories.map((category) {
return PopupMenuItem<String>(
value: category,
child: Row(
children: [
if (item.category == category)
const Icon(Icons.check, size: 20, color: AppTheme.primaryColor)
else
const SizedBox(width: 20),
const SizedBox(width: 8),
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: AppTheme.getCategoryColor(category),
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Text(AppTheme.getCategoryName(category)),
],
),
);
}).toList();
},
),
),
);
}
String _getErrorMessage(Object error) {
final errorString = error.toString().toLowerCase();
if (errorString.contains('permission') || errorString.contains('权限')) {
return '需要授予应用使用权限';
} else if (errorString.contains('network') || errorString.contains('网络')) {
return '网络连接失败,请检查网络';
} else if (errorString.contains('database') || errorString.contains('数据库')) {
return '数据库操作失败';
}
return '加载失败,请稍后重试';
}
}
class AppCategoryItem {
final String packageName;
final String appName;
final String category;
final bool isCustom;
AppCategoryItem({
required this.packageName,
required this.appName,
required this.category,
required this.isCustom,
});
}