Files
MWClash/lib/fragments/profiles/profiles.dart
chen08209 676f2d058a Add windows server mode start process verify
Add linux deb dependencies

Add backup recovery strategy select

Support custom text scaling

Optimize the display of different text scale

Optimize windows setup experience

Optimize startTun performance

Optimize android tv experience

Optimize default option

Optimize computed text size

Optimize hyperOS freeform window

Add developer mode

Update core

Optimize more details
2025-05-01 00:02:29 +08:00

521 lines
15 KiB
Dart

import 'dart:ui';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/fragments/profiles/edit_profile.dart';
import 'package:fl_clash/fragments/profiles/override_profile.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'add_profile.dart';
class ProfilesFragment extends StatefulWidget {
const ProfilesFragment({super.key});
@override
State<ProfilesFragment> createState() => _ProfilesFragmentState();
}
class _ProfilesFragmentState extends State<ProfilesFragment> with PageMixin {
Function? applyConfigDebounce;
_handleShowAddExtendPage() {
showExtend(
globalState.navigatorKey.currentState!.context,
builder: (_, type) {
return AdaptiveSheetScaffold(
type: type,
body: AddProfile(
context: globalState.navigatorKey.currentState!.context,
),
title: "${appLocalizations.add}${appLocalizations.profile}",
);
},
);
}
_updateProfiles() async {
final profiles = globalState.config.profiles;
final messages = [];
final updateProfiles = profiles.map<Future>(
(profile) async {
if (profile.type == ProfileType.file) return;
globalState.appController.setProfile(
profile.copyWith(isUpdating: true),
);
try {
await globalState.appController.updateProfile(profile);
} catch (e) {
messages.add("${profile.label ?? profile.id}: $e \n");
globalState.appController.setProfile(
profile.copyWith(
isUpdating: false,
),
);
}
},
);
final titleMedium = context.textTheme.titleMedium;
await Future.wait(updateProfiles);
if (messages.isNotEmpty) {
globalState.showMessage(
title: appLocalizations.tip,
message: TextSpan(
children: [
for (final message in messages)
TextSpan(text: message, style: titleMedium)
],
),
);
}
}
@override
List<Widget> get actions => [
IconButton(
onPressed: () {
_updateProfiles();
},
icon: const Icon(Icons.sync),
),
IconButton(
onPressed: () {
final profiles = globalState.config.profiles;
showSheet(
context: context,
builder: (_, type) {
return ReorderableProfilesSheet(
type: type,
profiles: profiles,
);
},
);
},
icon: const Icon(Icons.sort),
iconSize: 26,
),
];
@override
Widget? get floatingActionButton => FloatingActionButton(
heroTag: null,
onPressed: _handleShowAddExtendPage,
child: const Icon(
Icons.add,
),
);
@override
Widget build(BuildContext context) {
return Consumer(
builder: (_, ref, __) {
ref.listenManual(
isCurrentPageProvider(PageLabel.profiles),
(prev, next) {
if (prev != next && next == true) {
initPageState();
}
},
fireImmediately: true,
);
final profilesSelectorState = ref.watch(profilesSelectorStateProvider);
if (profilesSelectorState.profiles.isEmpty) {
return NullStatus(
label: appLocalizations.nullProfileDesc,
);
}
return Align(
alignment: Alignment.topCenter,
child: SingleChildScrollView(
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: 88,
),
child: Grid(
mainAxisSpacing: 16,
crossAxisSpacing: 16,
crossAxisCount: profilesSelectorState.columns,
children: [
for (int i = 0; i < profilesSelectorState.profiles.length; i++)
GridItem(
child: ProfileItem(
key: Key(profilesSelectorState.profiles[i].id),
profile: profilesSelectorState.profiles[i],
groupValue: profilesSelectorState.currentProfileId,
onChanged: (profileId) {
ref.read(currentProfileIdProvider.notifier).value =
profileId;
},
),
),
],
),
),
);
},
);
}
}
class ProfileItem extends StatelessWidget {
final Profile profile;
final String? groupValue;
final void Function(String? value) onChanged;
const ProfileItem({
super.key,
required this.profile,
required this.groupValue,
required this.onChanged,
});
_handleDeleteProfile(BuildContext context) async {
final res = await globalState.showMessage(
title: appLocalizations.tip,
message: TextSpan(
text: appLocalizations.deleteProfileTip,
),
);
if (res != true) {
return;
}
await globalState.appController.deleteProfile(profile.id);
}
Future updateProfile() async {
final appController = globalState.appController;
if (profile.type == ProfileType.file) return;
await globalState.safeRun(silence: false, () async {
try {
appController.setProfile(
profile.copyWith(
isUpdating: true,
),
);
await appController.updateProfile(profile);
} catch (e) {
appController.setProfile(
profile.copyWith(
isUpdating: false,
),
);
rethrow;
}
});
}
_handleShowEditExtendPage(BuildContext context) {
showExtend(
context,
builder: (_, type) {
return AdaptiveSheetScaffold(
type: type,
body: EditProfile(
profile: profile,
context: context,
),
title: "${appLocalizations.edit}${appLocalizations.profile}",
);
},
);
}
List<Widget> _buildUrlProfileInfo(BuildContext context) {
final subscriptionInfo = profile.subscriptionInfo;
return [
const SizedBox(
height: 8,
),
if (subscriptionInfo != null)
SubscriptionInfoView(
subscriptionInfo: subscriptionInfo,
),
Text(
profile.lastUpdateDate?.lastUpdateTimeDesc ?? "",
style: context.textTheme.labelMedium?.toLight,
),
];
}
List<Widget> _buildFileProfileInfo(BuildContext context) {
return [
const SizedBox(
height: 8,
),
Text(
profile.lastUpdateDate?.lastUpdateTimeDesc ?? "",
style: context.textTheme.labelMedium?.toLight,
),
];
}
// _handleCopyLink(BuildContext context) async {
// await Clipboard.setData(
// ClipboardData(
// text: profile.url,
// ),
// );
// if (context.mounted) {
// context.showNotifier(appLocalizations.copySuccess);
// }
// }
_handleExportFile(BuildContext context) async {
final commonScaffoldState = context.commonScaffoldState;
final res = await commonScaffoldState?.loadingRun<bool>(
() async {
final file = await profile.getFile();
final value = await picker.saveFile(
profile.label ?? profile.id,
file.readAsBytesSync(),
);
if (value == null) return false;
return true;
},
title: appLocalizations.tip,
);
if (res == true && context.mounted) {
context.showNotifier(appLocalizations.exportSuccess);
}
}
_handlePushGenProfilePage(BuildContext context, String id) {
BaseNavigator.push(
context,
OverrideProfile(
profileId: id,
),
);
}
@override
Widget build(BuildContext context) {
return CommonCard(
isSelected: profile.id == groupValue,
onPressed: () {
onChanged(profile.id);
},
child: ListItem(
key: Key(profile.id),
horizontalTitleGap: 16,
padding: const EdgeInsets.symmetric(horizontal: 16),
trailing: SizedBox(
height: 40,
width: 40,
child: FadeThroughBox(
child: profile.isUpdating
? const Padding(
padding: EdgeInsets.all(8),
child: CircularProgressIndicator(),
)
: CommonPopupBox(
popup: CommonPopupMenu(
items: [
PopupMenuItemData(
icon: Icons.edit_outlined,
label: appLocalizations.edit,
onPressed: () {
_handleShowEditExtendPage(context);
},
),
if (profile.type == ProfileType.url) ...[
PopupMenuItemData(
icon: Icons.sync_alt_sharp,
label: appLocalizations.sync,
onPressed: () {
updateProfile();
},
),
],
PopupMenuItemData(
icon: Icons.extension_outlined,
label: appLocalizations.override,
onPressed: () {
_handlePushGenProfilePage(context, profile.id);
},
),
PopupMenuItemData(
icon: Icons.file_copy_outlined,
label: appLocalizations.exportFile,
onPressed: () {
_handleExportFile(context);
},
),
PopupMenuItemData(
icon: Icons.delete_outlined,
iconSize: 20,
label: appLocalizations.delete,
onPressed: () {
_handleDeleteProfile(context);
},
type: PopupMenuItemType.danger,
),
],
),
targetBuilder: (open) {
return IconButton(
onPressed: () {
open();
},
icon: Icon(Icons.more_vert),
);
},
),
),
),
title: Container(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
profile.label ?? profile.id,
style: context.textTheme.titleMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
...switch (profile.type) {
ProfileType.file => _buildFileProfileInfo(context),
ProfileType.url => _buildUrlProfileInfo(context),
},
],
),
],
),
),
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
),
);
}
}
class ReorderableProfilesSheet extends StatefulWidget {
final List<Profile> profiles;
final SheetType type;
const ReorderableProfilesSheet({
super.key,
required this.profiles,
required this.type,
});
@override
State<ReorderableProfilesSheet> createState() =>
_ReorderableProfilesSheetState();
}
class _ReorderableProfilesSheetState extends State<ReorderableProfilesSheet> {
late List<Profile> profiles;
@override
void initState() {
super.initState();
profiles = List.from(widget.profiles);
}
Widget proxyDecorator(
Widget child,
int index,
Animation<double> animation,
) {
final profile = profiles[index];
return AnimatedBuilder(
animation: animation,
builder: (_, Widget? child) {
final double animValue = Curves.easeInOut.transform(animation.value);
final double scale = lerpDouble(1, 1.02, animValue)!;
return Transform.scale(
scale: scale,
child: child,
);
},
child: Container(
key: Key(profile.id),
padding: const EdgeInsets.symmetric(vertical: 4),
child: CommonCard(
type: CommonCardType.filled,
child: ListTile(
contentPadding: const EdgeInsets.only(
right: 44,
left: 16,
),
title: Text(profile.label ?? profile.id),
),
),
),
);
}
@override
Widget build(BuildContext context) {
return AdaptiveSheetScaffold(
type: widget.type,
actions: [
IconButton(
onPressed: () {
Navigator.of(context).pop();
globalState.appController.setProfiles(profiles);
},
icon: Icon(
Icons.save,
),
)
],
body: Padding(
padding: EdgeInsets.only(bottom: 32),
child: ReorderableListView.builder(
buildDefaultDragHandles: false,
padding: const EdgeInsets.symmetric(
horizontal: 12,
),
proxyDecorator: proxyDecorator,
onReorder: (oldIndex, newIndex) {
setState(() {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final profile = profiles.removeAt(oldIndex);
profiles.insert(newIndex, profile);
});
},
itemBuilder: (_, index) {
final profile = profiles[index];
return Container(
key: Key(profile.id),
padding: const EdgeInsets.symmetric(vertical: 4),
child: CommonCard(
type: CommonCardType.filled,
child: ListTile(
contentPadding: const EdgeInsets.only(
right: 16,
left: 16,
),
title: Text(profile.label ?? profile.id),
trailing: ReorderableDragStartListener(
index: index,
child: const Icon(Icons.drag_handle),
),
),
),
);
},
itemCount: profiles.length,
),
),
title: appLocalizations.profilesSort,
);
}
}