Fix tray action issues Fix get profile redirect client ua issues Fix proxy card delay view issues Add Russian, Japanese adaptation Fix some issues
526 lines
15 KiB
Dart
526 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/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() {
|
|
showExtendPage(
|
|
globalState.navigatorKey.currentState!.context,
|
|
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(
|
|
title: appLocalizations.profilesSort,
|
|
context: context,
|
|
body: SizedBox(
|
|
height: 400,
|
|
child: ReorderableProfiles(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) {
|
|
showExtendPage(
|
|
context,
|
|
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);
|
|
}
|
|
}
|
|
|
|
// _handlePushCustomPage(BuildContext context, String id) {
|
|
// BaseNavigator.push(
|
|
// context,
|
|
// CustomProfile(
|
|
// profileId: id,
|
|
// ),
|
|
// );
|
|
// }
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final key = GlobalKey<CommonPopupBoxState>();
|
|
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: FadeBox(
|
|
child: profile.isUpdating
|
|
? const Padding(
|
|
padding: EdgeInsets.all(8),
|
|
child: CircularProgressIndicator(),
|
|
)
|
|
: CommonPopupBox(
|
|
key: key,
|
|
popup: CommonPopupMenu(
|
|
items: [
|
|
ActionItemData(
|
|
icon: Icons.edit_outlined,
|
|
label: appLocalizations.edit,
|
|
onPressed: () {
|
|
_handleShowEditExtendPage(context);
|
|
},
|
|
),
|
|
if (profile.type == ProfileType.url) ...[
|
|
ActionItemData(
|
|
icon: Icons.sync_alt_sharp,
|
|
label: appLocalizations.sync,
|
|
onPressed: () {
|
|
updateProfile();
|
|
},
|
|
),
|
|
// ActionItemData(
|
|
// icon: Icons.copy,
|
|
// label: appLocalizations.copyLink,
|
|
// onPressed: () {
|
|
// _handleCopyLink(context);
|
|
// },
|
|
// ),
|
|
],
|
|
// ActionItemData(
|
|
// icon: Icons.extension_outlined,
|
|
// label: "自定义",
|
|
// onPressed: () {
|
|
// _handlePushCustomPage(context, profile.id);
|
|
// },
|
|
// ),
|
|
ActionItemData(
|
|
icon: Icons.file_copy_outlined,
|
|
label: appLocalizations.exportFile,
|
|
onPressed: () {
|
|
_handleExportFile(context);
|
|
},
|
|
),
|
|
ActionItemData(
|
|
icon: Icons.delete_outlined,
|
|
iconSize: 20,
|
|
label: appLocalizations.delete,
|
|
onPressed: () {
|
|
_handleDeleteProfile(context);
|
|
},
|
|
type: ActionType.danger,
|
|
),
|
|
],
|
|
),
|
|
target: IconButton(
|
|
onPressed: () {
|
|
key.currentState?.pop();
|
|
},
|
|
icon: Icon(Icons.more_vert),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
title: Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
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 ReorderableProfiles extends StatefulWidget {
|
|
final List<Profile> profiles;
|
|
|
|
const ReorderableProfiles({
|
|
super.key,
|
|
required this.profiles,
|
|
});
|
|
|
|
@override
|
|
State<ReorderableProfiles> createState() => _ReorderableProfilesState();
|
|
}
|
|
|
|
class _ReorderableProfilesState extends State<ReorderableProfiles> {
|
|
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 Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Expanded(
|
|
flex: 1,
|
|
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,
|
|
),
|
|
),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
vertical: 16,
|
|
horizontal: 24,
|
|
),
|
|
child: FilledButton.tonal(
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
globalState.appController.setProfiles(profiles);
|
|
},
|
|
style: ButtonStyle(
|
|
padding: WidgetStateProperty.all(
|
|
const EdgeInsets.symmetric(vertical: 8),
|
|
),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text(
|
|
appLocalizations.confirm,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|