Add one-click update all profiles

Add expire show
This commit is contained in:
chen08209
2024-06-07 17:22:55 +08:00
parent d2d9bdab02
commit c65746709d
24 changed files with 450 additions and 274 deletions

View File

@@ -160,8 +160,9 @@ class ApplicationState extends State<Application> {
AppLocalizations.delegate.supportedLocales,
themeMode: state.themeMode,
theme: ThemeData(
pageTransitionsTheme: _pageTransitionsTheme,
useMaterial3: true,
fontFamily: '',
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.light,
systemColorSchemes: systemColorSchemes,
@@ -170,6 +171,7 @@ class ApplicationState extends State<Application> {
),
darkTheme: ThemeData(
useMaterial3: true,
fontFamily: '',
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.dark,

View File

@@ -35,4 +35,8 @@ extension DateTimeExtension on DateTime {
}
return appLocalizations.just;
}
}
String get show {
return toIso8601String().substring(0, 10);
}
}

View File

@@ -23,6 +23,7 @@ class Measure {
double? _bodyMediumHeight;
double? _bodySmallHeight;
double? _labelSmallHeight;
double? _labelMediumHeight;
double? _titleLargeHeight;
double? _titleMediumHeight;
@@ -56,6 +57,16 @@ class Measure {
return _labelSmallHeight!;
}
double get labelMediumHeight {
_labelMediumHeight ??= computeTextSize(
Text(
"",
style: context.textTheme.labelMedium,
),
).height;
return _labelMediumHeight!;
}
double get titleLargeHeight {
_titleLargeHeight ??= computeTextSize(
Text(

View File

@@ -14,9 +14,6 @@ class Request {
Request() {
_dio = Dio(
BaseOptions(
connectTimeout: httpTimeoutDuration,
sendTimeout: httpTimeoutDuration,
receiveTimeout: httpTimeoutDuration,
headers: {"User-Agent": coreName},
),
);
@@ -37,7 +34,7 @@ class Request {
_dio.httpClientAdapter = IOHttpClientAdapter(
createHttpClient: () {
final client = HttpClient();
if(!_isStart) return client;
if (!_isStart) return client;
client.findProxy = (url) {
return "PROXY localhost:$_port;DIRECT";
};
@@ -56,7 +53,7 @@ class Request {
),
)
.timeout(
httpTimeoutDuration,
httpTimeoutDuration * 2,
);
return response;
}
@@ -86,12 +83,14 @@ class Request {
"https://ipinfo.io/json/": IpInfo.fromIpInfoIoJson,
};
Future<IpInfo?> checkIp() async {
Future<IpInfo?> checkIp(CancelToken? cancelToken) async {
for (final source in _ipInfoSources.entries) {
try {
final response = await _dio.get<Map<String, dynamic>>(
source.key,
);
final response = await _dio
.get<Map<String, dynamic>>(source.key, cancelToken: cancelToken)
.timeout(
httpTimeoutDuration,
);
if (response.statusCode == 200 && response.data != null) {
return source.value(response.data!);
}
@@ -103,4 +102,4 @@ class Request {
}
}
final request = Request();
final request = Request();

View File

@@ -6,6 +6,11 @@ extension TextStyleExtension on TextStyle {
return copyWith(color: color?.toLight());
}
toLighter() {
return copyWith(color: color?.toLighter());
}
toSoftBold() {
return copyWith(fontWeight: FontWeight.w500);
}

View File

@@ -1,5 +1,6 @@
import 'dart:async';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@@ -96,7 +97,7 @@ class AppController {
}
}
updateProfile(String id) async {
Future<void> updateProfile(String id) async {
final profile = config.getCurrentProfileForId(id);
if (profile != null) {
final tempProfile = profile.copyWith();
@@ -135,16 +136,25 @@ class AppController {
autoUpdateProfiles() async {
for (final profile in config.profiles) {
if (!profile.autoUpdate) return;
if (!profile.autoUpdate) continue;
final isNotNeedUpdate = profile.lastUpdateDate
?.add(
profile.autoUpdateDuration,
)
.isBeforeNow;
if (isNotNeedUpdate == false ||
profile.url == null ||
profile.url!.isEmpty) continue;
await profile.update();
if (isNotNeedUpdate == false || profile.type == ProfileType.file) {
continue;
}
await updateProfile(profile.id);
}
}
updateProfiles() async {
for (final profile in config.profiles) {
if (profile.type == ProfileType.file) {
continue;
}
await updateProfile(profile.id);
}
}

View File

@@ -61,4 +61,4 @@ enum MessageType { log, tun, delay, process, now }
enum RecoveryOption {
all,
onlyProfiles,
}
}

View File

@@ -1,4 +1,5 @@
import 'package:country_flags/country_flags.dart';
import 'package:dio/dio.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
@@ -16,20 +17,29 @@ class NetworkDetection extends StatefulWidget {
class _NetworkDetectionState extends State<NetworkDetection> {
final ipInfoNotifier = ValueNotifier<IpInfo?>(null);
final timeoutNotifier = ValueNotifier<bool>(false);
bool? _preIsStart;
CancelToken? cancelToken;
_checkIp(
bool isInit,
bool isStart,
) async {
if (!isInit) return;
if (_preIsStart == false && _preIsStart == isStart) return;
if (cancelToken != null) {
cancelToken!.cancel();
cancelToken = null;
}
ipInfoNotifier.value = null;
final ipInfo = await request.checkIp();
final ipInfo = await request.checkIp(cancelToken);
if (ipInfo == null) {
timeoutNotifier.value = true;
return;
} else {
timeoutNotifier.value = false;
}
ipInfoNotifier.value = await request.checkIp();
_preIsStart = isStart;
ipInfoNotifier.value = ipInfo;
}
_checkIpContainer(Widget child) {
@@ -42,9 +52,7 @@ class _NetworkDetectionState extends State<NetworkDetection> {
);
},
builder: (_, state, __) {
_checkIp(
state.isInit,
);
_checkIp(state.isInit, state.isStart);
return child;
},
child: child,
@@ -82,15 +90,30 @@ class _NetworkDetectionState extends State<NetworkDetection> {
width: 24,
height: 24,
)
: TooltipText(
text: Text(
appLocalizations.checking,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.titleMedium,
),
: ValueListenableBuilder(
valueListenable: timeoutNotifier,
builder: (_, timeout, __) {
if (timeout) {
return Text(
appLocalizations.checkError,
style: Theme.of(context)
.textTheme
.titleMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
);
}
return TooltipText(
text: Text(
appLocalizations.checking,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.titleMedium,
),
);
},
),
),
),
@@ -126,17 +149,21 @@ class _NetworkDetectionState extends State<NetworkDetection> {
: ValueListenableBuilder(
valueListenable: timeoutNotifier,
builder: (_, timeout, __) {
if(timeout){
if (timeout) {
return Text(
appLocalizations.ipCheckError,
style: context.textTheme.bodyMedium
appLocalizations.ipCheckTimeout,
style: context.textTheme.titleLarge
?.toSoftBold(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
);
}
return const SizedBox(
child: CircularProgressIndicator(),
return Container(
padding: const EdgeInsets.all(2),
child: const AspectRatio(
aspectRatio: 1,
child: CircularProgressIndicator(),
),
);
},
),

View File

@@ -56,6 +56,9 @@ class _LogsFragmentState extends State<LogsFragment> {
);
},
icon: const Icon(Icons.search),
),
const SizedBox(
width: 8,
)
];
});
@@ -139,6 +142,9 @@ class LogsSearchDelegate extends SearchDelegate {
},
icon: const Icon(Icons.clear),
),
const SizedBox(
width: 8,
)
];
}

View File

@@ -1,4 +1,5 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
@@ -82,7 +83,7 @@ class _EditProfileState extends State<EditProfile> {
},
),
),
if (widget.profile.url != null && widget.profile.url!.isNotEmpty == true)...[
if (widget.profile.type == ProfileType.url)...[
ListItem(
title: TextFormField(
controller: urlController,

View File

@@ -17,24 +17,9 @@ enum ProfileActions {
delete,
}
class ProfilesFragment extends StatefulWidget {
class ProfilesFragment extends StatelessWidget {
const ProfilesFragment({super.key});
@override
State<ProfilesFragment> createState() => _ProfilesFragmentState();
}
class _ProfilesFragmentState extends State<ProfilesFragment> {
_handleDeleteProfile(String id) async {
globalState.appController.deleteProfile(id);
}
_handleUpdateProfile(String id) async {
context.findAncestorStateOfType<CommonScaffoldState>()?.loadingRun(
() => globalState.appController.updateProfile(id),
);
}
_handleShowAddExtendPage() {
showExtendPage(
globalState.navigatorKey.currentState!.context,
@@ -45,76 +30,6 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
);
}
_handleShowEditExtendPage(Profile profile) {
showExtendPage(
context,
body: EditProfile(
profile: profile.copyWith(),
context: context,
),
title: "${appLocalizations.edit}${appLocalizations.profile}",
);
}
_buildGrid({
required ProfilesSelectorState state,
int crossAxisCount = 1,
}) {
return SingleChildScrollView(
padding: crossAxisCount > 1
? const EdgeInsets.symmetric(horizontal: 16)
: EdgeInsets.zero,
child: Grid.baseGap(
crossAxisCount: crossAxisCount,
children: [
for (final profile in state.profiles)
GridItem(
child: ProfileItem(
profile: profile,
commonPopupMenu: CommonPopupMenu<ProfileActions>(
items: [
CommonPopupMenuItem(
action: ProfileActions.edit,
label: appLocalizations.edit,
iconData: Icons.edit,
),
if (profile.url != null)
CommonPopupMenuItem(
action: ProfileActions.update,
label: appLocalizations.update,
iconData: Icons.sync,
),
CommonPopupMenuItem(
action: ProfileActions.delete,
label: appLocalizations.delete,
iconData: Icons.delete,
),
],
onSelected: (ProfileActions? action) async {
switch (action) {
case ProfileActions.edit:
_handleShowEditExtendPage(profile);
break;
case ProfileActions.delete:
_handleDeleteProfile(profile.id);
break;
case ProfileActions.update:
_handleUpdateProfile(profile.id);
break;
case null:
break;
}
},
),
groupValue: state.currentProfileId,
onChanged: globalState.appController.changeProfile,
),
),
],
),
);
}
_getColumns(ViewMode viewMode) {
switch (viewMode) {
case ViewMode.mobile:
@@ -126,17 +41,47 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
}
}
@override
Widget build(BuildContext context) {
return FloatLayout(
floatingWidget: Container(
margin: const EdgeInsets.all(kFloatingActionButtonMargin),
child: FloatingActionButton(
_initScaffoldState(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
IconButton(
onPressed: () {
commonScaffoldState.loadingRun<void>(
() async {
await globalState.appController.updateProfiles();
},
);
},
icon: const Icon(Icons.download),
),
const SizedBox(
width: 8,
)
];
commonScaffoldState?.floatingActionButton = FloatingActionButton(
heroTag: null,
onPressed: _handleShowAddExtendPage,
child: const Icon(Icons.add),
),
),
child: const Icon(
Icons.add,
),
);
},
);
}
@override
Widget build(BuildContext context) {
return Selector<AppState, bool>(
selector: (_, appState) => appState.currentLabel == 'profiles',
builder: (_, isCurrent, child) {
if (isCurrent) {
_initScaffoldState(context);
}
return child!;
},
child: Selector2<AppState, Config, ProfilesSelectorState>(
selector: (_, appState, config) => ProfilesSelectorState(
profiles: config.profiles,
@@ -149,11 +94,32 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
label: appLocalizations.nullProfileDesc,
);
}
final columns = _getColumns(state.viewMode);
final isMobile = state.viewMode == ViewMode.mobile;
return Align(
alignment: Alignment.topCenter,
child: _buildGrid(
state: state,
crossAxisCount: _getColumns(state.viewMode),
child: SingleChildScrollView(
padding: !isMobile
? const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
)
: EdgeInsets.zero,
child: Grid(
mainAxisSpacing: isMobile ? 8 : 16,
crossAxisSpacing: 16,
crossAxisCount: columns,
children: [
for (final profile in state.profiles)
GridItem(
child: ProfileItem(
profile: profile,
groupValue: state.currentProfileId,
onChanged: globalState.appController.changeProfile,
),
),
],
),
),
);
},
@@ -162,92 +128,188 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
}
}
class ProfileItem extends StatelessWidget {
class ProfileItem extends StatefulWidget {
final Profile profile;
final String? groupValue;
final CommonPopupMenu commonPopupMenu;
final void Function(String? value) onChanged;
const ProfileItem({
super.key,
required this.profile,
required this.commonPopupMenu,
required this.groupValue,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
String useShow;
String totalShow;
double progress;
final userInfo = profile.userInfo;
if (userInfo == null) {
useShow = "Infinite";
totalShow = "Infinite";
progress = 1;
} else {
final use = userInfo.upload + userInfo.download;
final total = userInfo.total;
useShow = TrafficValue(value: use).show;
totalShow = TrafficValue(value: total).show;
progress = total == 0 ? 0.0 : use / total;
}
return ListItem.radio(
horizontalTitleGap: 16,
delegate: RadioDelegate<String?>(
value: profile.id,
groupValue: groupValue,
onChanged: onChanged,
State<ProfileItem> createState() => _ProfileItemState();
}
class _ProfileItemState extends State<ProfileItem> {
final isUpdating = ValueNotifier<bool>(false);
_handleDeleteProfile(String id) async {
globalState.appController.deleteProfile(id);
}
_handleUpdateProfile(String id) async {
isUpdating.value = true;
await globalState.safeRun<void>(() async {
await globalState.appController.updateProfile(id);
});
isUpdating.value = false;
}
_handleShowEditExtendPage(
Profile profile,
) {
showExtendPage(
context,
body: EditProfile(
profile: profile.copyWith(),
context: context,
),
padding: const EdgeInsets.symmetric(horizontal: 16),
trailing: commonPopupMenu,
title: Column(
mainAxisSize: MainAxisSize.min,
title: "${appLocalizations.edit}${appLocalizations.profile}",
);
}
_buildTitle(Profile profile) {
final textTheme = context.textTheme;
final userInfo = profile.userInfo ?? UserInfo();
final use = userInfo.upload + userInfo.download;
final total = userInfo.total;
final useShow = TrafficValue(value: use).show;
final totalShow = TrafficValue(value: total).show;
final progress = total == 0 ? 0.0 : use / total;
final expireShow = userInfo.expire == 0
? "长期有效"
: DateTime.fromMillisecondsSinceEpoch(userInfo.expire * 1000).show;
return Container(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
profile.label ?? profile.id,
style: Theme.of(context).textTheme.titleMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Flexible(
child: Text(
profile.lastUpdateDate?.lastUpdateTimeDesc ?? '',
style: Theme.of(context).textTheme.labelMedium?.toLight(),
),
),
],
),
),
Flexible(
child: Container(
margin: const EdgeInsets.symmetric(
vertical: 8,
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
profile.label ?? profile.id,
style: textTheme.titleMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
child: LinearProgressIndicator(
minHeight: 6,
value: progress,
Text(
profile.lastUpdateDate?.lastUpdateTimeDesc ?? '',
style: textTheme.labelMedium?.toLight(),
),
),
],
),
Flexible(
child: Text(
"$useShow / $totalShow",
style: Theme.of(context).textTheme.labelMedium?.toLight(),
),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
margin: const EdgeInsets.symmetric(
vertical: 8,
),
child: LinearProgressIndicator(
minHeight: 6,
value: progress,
),
),
Text(
"$useShow / $totalShow",
style: textTheme.labelMedium?.toLight(),
),
const SizedBox(
height: 2,
),
Row(
children: [
Text(
"到期时间:",
style: textTheme.labelMedium?.toLighter(),
),
const SizedBox(
width: 4,
),
Text(
expireShow,
style: textTheme.labelMedium?.toLighter(),
),
],
)
],
),
],
),
);
}
@override
Widget build(BuildContext context) {
final profile = widget.profile;
final groupValue = widget.groupValue;
final onChanged = widget.onChanged;
return Selector<AppState, ViewMode>(
selector: (_, appState) => appState.viewMode,
builder: (_, viewMode, child) {
if (viewMode == ViewMode.mobile) {
return child!;
}
return CommonCard(
child: child!,
);
},
child: ListItem.radio(
key: Key(profile.id),
horizontalTitleGap: 16,
delegate: RadioDelegate<String?>(
value: profile.id,
groupValue: groupValue,
onChanged: onChanged,
),
padding: const EdgeInsets.symmetric(horizontal: 16),
trailing: CommonPopupMenu<ProfileActions>(
items: [
CommonPopupMenuItem(
action: ProfileActions.edit,
label: appLocalizations.edit,
iconData: Icons.edit,
),
if (profile.type == ProfileType.url)
CommonPopupMenuItem(
action: ProfileActions.update,
label: appLocalizations.update,
iconData: Icons.sync,
),
CommonPopupMenuItem(
action: ProfileActions.delete,
label: appLocalizations.delete,
iconData: Icons.delete,
),
],
onSelected: (ProfileActions? action) async {
switch (action) {
case ProfileActions.edit:
_handleShowEditExtendPage(profile);
break;
case ProfileActions.delete:
_handleDeleteProfile(profile.id);
break;
case ProfileActions.update:
_handleUpdateProfile(profile.id);
break;
case null:
break;
}
},
),
title: _buildTitle(profile),
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
),
);
}
}

View File

@@ -51,6 +51,9 @@ class _ProxiesFragmentState extends State<ProxiesFragment>
selectedValue: proxiesSortType,
);
},
),
const SizedBox(
width: 8,
)
];
});
@@ -431,7 +434,7 @@ class _DelayTestButtonContainerState extends State<DelayTestButtonContainer>
_controller = AnimationController(
vsync: this,
duration: const Duration(
milliseconds: 1200,
milliseconds: 600,
),
);
_scale = Tween<double>(
@@ -443,7 +446,6 @@ class _DelayTestButtonContainerState extends State<DelayTestButtonContainer>
curve: const Interval(
0,
1,
curve: Curves.elasticInOut,
),
),
);

View File

@@ -159,5 +159,6 @@
"externalResources": "External resources",
"checking": "Checking...",
"country": "Country",
"ipCheckError": "Ip check timeout"
"checkError": "Check error",
"ipCheckTimeout": "Ip check timeout"
}

View File

@@ -159,5 +159,6 @@
"externalResources": "外部资源",
"checking": "检测中...",
"country": "区域",
"ipCheckError": "Ip检测超时"
"checkError": "检测失败",
"ipCheckTimeout": "Ip检测超时"
}

View File

@@ -76,6 +76,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Cancel filter system app"),
"cancelSelectAll":
MessageLookupByLibrary.simpleMessage("Cancel select all"),
"checkError": MessageLookupByLibrary.simpleMessage("Check error"),
"checkUpdate":
MessageLookupByLibrary.simpleMessage("Check for updates"),
"checkUpdateError": MessageLookupByLibrary.simpleMessage(
@@ -124,7 +125,7 @@ class MessageLookup extends MessageLookupByLibrary {
"hours": MessageLookupByLibrary.simpleMessage("Hours"),
"importFromURL":
MessageLookupByLibrary.simpleMessage("Import from URL"),
"ipCheckError":
"ipCheckTimeout":
MessageLookupByLibrary.simpleMessage("Ip check timeout"),
"just": MessageLookupByLibrary.simpleMessage("Just"),
"language": MessageLookupByLibrary.simpleMessage("Language"),

View File

@@ -63,6 +63,7 @@ class MessageLookup extends MessageLookupByLibrary {
"cancelFilterSystemApp":
MessageLookupByLibrary.simpleMessage("取消过滤系统应用"),
"cancelSelectAll": MessageLookupByLibrary.simpleMessage("取消全选"),
"checkError": MessageLookupByLibrary.simpleMessage("检测失败"),
"checkUpdate": MessageLookupByLibrary.simpleMessage("检查更新"),
"checkUpdateError": MessageLookupByLibrary.simpleMessage("当前应用已经是最新版了"),
"checking": MessageLookupByLibrary.simpleMessage("检测中..."),
@@ -101,7 +102,7 @@ class MessageLookup extends MessageLookupByLibrary {
"goDownload": MessageLookupByLibrary.simpleMessage("前往下载"),
"hours": MessageLookupByLibrary.simpleMessage("小时"),
"importFromURL": MessageLookupByLibrary.simpleMessage("从URL导入"),
"ipCheckError": MessageLookupByLibrary.simpleMessage("Ip检测超时"),
"ipCheckTimeout": MessageLookupByLibrary.simpleMessage("Ip检测超时"),
"just": MessageLookupByLibrary.simpleMessage("刚刚"),
"language": MessageLookupByLibrary.simpleMessage("语言"),
"light": MessageLookupByLibrary.simpleMessage("浅色"),

View File

@@ -1650,11 +1650,21 @@ class AppLocalizations {
);
}
/// `Check error`
String get checkError {
return Intl.message(
'Check error',
name: 'checkError',
desc: '',
args: [],
);
}
/// `Ip check timeout`
String get ipCheckError {
String get ipCheckTimeout {
return Intl.message(
'Ip check timeout',
name: 'ipCheckError',
name: 'ipCheckTimeout',
desc: '',
args: [],
);

View File

@@ -16,16 +16,17 @@ class UserInfo {
int upload;
int download;
int total;
int? expire;
int expire;
UserInfo({
int? upload,
int? download,
int? total,
this.expire,
int? expire,
}) : upload = upload ?? 0,
download = download ?? 0,
total = total ?? 0;
total = total ?? 0,
expire = expire ?? 0;
Map<String, dynamic> toJson() {
return _$UserInfoToJson(this);
@@ -37,10 +38,10 @@ class UserInfo {
factory UserInfo.formHString(String? info) {
if (info == null) return UserInfo();
var list = info.split(";");
final list = info.split(";");
Map<String, int?> map = {};
for (var i in list) {
var keyValue = i.trim().split("=");
for (final i in list) {
final keyValue = i.trim().split("=");
map[keyValue[0]] = int.tryParse(keyValue[1]);
}
return UserInfo(
@@ -83,7 +84,8 @@ class Profile {
autoUpdateDuration = autoUpdateDuration ?? defaultUpdateDuration,
selectedMap = selectedMap ?? {};
ProfileType get type => url == null ? ProfileType.file : ProfileType.url;
ProfileType get type =>
url == null || url?.isEmpty == true ? ProfileType.file : ProfileType.url;
Future<void> checkAndUpdate() async {
final isExists = await check();

View File

@@ -71,6 +71,7 @@ class ListItem<T> extends StatelessWidget {
final Widget title;
final Widget? subtitle;
final EdgeInsets padding;
final ListTileTitleAlignment tileTitleAlignment;
final bool? prue;
final Widget? trailing;
final Delegate delegate;
@@ -87,6 +88,7 @@ class ListItem<T> extends StatelessWidget {
this.horizontalTitleGap,
this.prue,
this.onTab,
this.tileTitleAlignment = ListTileTitleAlignment.center,
}) : delegate = const Delegate();
const ListItem.open({
@@ -99,6 +101,7 @@ class ListItem<T> extends StatelessWidget {
required OpenDelegate this.delegate,
this.horizontalTitleGap,
this.prue,
this.tileTitleAlignment = ListTileTitleAlignment.center,
}) : onTab = null;
const ListItem.next({
@@ -111,6 +114,7 @@ class ListItem<T> extends StatelessWidget {
required NextDelegate this.delegate,
this.horizontalTitleGap,
this.prue,
this.tileTitleAlignment = ListTileTitleAlignment.center,
}) : onTab = null;
const ListItem.checkbox({
@@ -122,6 +126,7 @@ class ListItem<T> extends StatelessWidget {
required CheckboxDelegate this.delegate,
this.horizontalTitleGap,
this.prue,
this.tileTitleAlignment = ListTileTitleAlignment.center,
}) : trailing = null,
onTab = null;
@@ -134,6 +139,7 @@ class ListItem<T> extends StatelessWidget {
required SwitchDelegate this.delegate,
this.horizontalTitleGap,
this.prue,
this.tileTitleAlignment = ListTileTitleAlignment.center,
}) : trailing = null,
onTab = null;
@@ -146,6 +152,7 @@ class ListItem<T> extends StatelessWidget {
required RadioDelegate<T> this.delegate,
this.horizontalTitleGap = 8,
this.prue,
this.tileTitleAlignment = ListTileTitleAlignment.center,
}) : leading = null,
onTab = null;
@@ -193,6 +200,7 @@ class ListItem<T> extends StatelessWidget {
horizontalTitleGap: horizontalTitleGap,
title: title,
subtitle: subtitle,
titleAlignment: tileTitleAlignment,
onTap: onTab,
trailing: trailing ?? this.trailing,
contentPadding: padding,

View File

@@ -49,6 +49,7 @@ class CommonScaffold extends StatefulWidget {
class CommonScaffoldState extends State<CommonScaffold> {
final ValueNotifier<List<Widget>> _actions = ValueNotifier([]);
final ValueNotifier<Widget?> _floatingActionButton = ValueNotifier(null);
final ValueNotifier<bool> _loading = ValueNotifier(false);
@@ -58,11 +59,16 @@ class CommonScaffoldState extends State<CommonScaffold> {
}
}
set floatingActionButton(Widget? actions) {
if (_floatingActionButton.value != actions) {
_floatingActionButton.value = actions;
}
}
Future<T?> loadingRun<T>(
Future<T> Function() futureFunction, {
String? title,
}) async {
if (_loading.value == true) return null;
_loading.value = true;
try {
final res = await futureFunction();
@@ -85,6 +91,7 @@ class CommonScaffoldState extends State<CommonScaffold> {
super.didUpdateWidget(oldWidget);
if (oldWidget.title != widget.title) {
_actions.value = [];
_floatingActionButton.value = null;
}
}
@@ -109,6 +116,13 @@ class CommonScaffoldState extends State<CommonScaffold> {
Widget build(BuildContext context) {
return _platformContainer(
child: Scaffold(
floatingActionButton: ValueListenableBuilder(
valueListenable: _floatingActionButton,
builder: (_, floatingActionButton, __) {
return floatingActionButton ?? Container();
},
),
floatingActionButtonAnimator: FloatingActionButtonAnimator.scaling,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight),
child: Stack(