Optimize package icon loading and connections polling
This commit is contained in:
+39
-7
@@ -61,14 +61,46 @@ class App {
|
||||
false;
|
||||
}
|
||||
|
||||
Future<ImageProvider?> getPackageIcon(String packageName) async {
|
||||
final path = await methodChannel.invokeMethod<String>('getPackageIcon', {
|
||||
'packageName': packageName,
|
||||
});
|
||||
if (path == null) {
|
||||
return null;
|
||||
final Map<String, ImageProvider?> _packageIcons = {};
|
||||
final Map<String, Future<ImageProvider?>> _packageIconTasks = {};
|
||||
|
||||
bool hasPackageIcon(String packageName) {
|
||||
return _packageIcons.containsKey(packageName);
|
||||
}
|
||||
|
||||
ImageProvider? getCachedPackageIcon(String packageName) {
|
||||
return _packageIcons[packageName];
|
||||
}
|
||||
|
||||
Future<ImageProvider?> getPackageIcon(String packageName) {
|
||||
if (packageName.isEmpty) {
|
||||
return Future.value(null);
|
||||
}
|
||||
return FileImage(File(path));
|
||||
if (_packageIcons.containsKey(packageName)) {
|
||||
return Future.value(_packageIcons[packageName]);
|
||||
}
|
||||
return _packageIconTasks[packageName] ??= _loadPackageIcon(packageName);
|
||||
}
|
||||
|
||||
Future<ImageProvider?> _loadPackageIcon(String packageName) async {
|
||||
ImageProvider? icon;
|
||||
try {
|
||||
final path = await methodChannel.invokeMethod<String>('getPackageIcon', {
|
||||
'packageName': packageName,
|
||||
});
|
||||
icon = path == null ? null : FileImage(File(path));
|
||||
} catch (error) {
|
||||
commonPrint.log('getPackageIcon error: $error');
|
||||
}
|
||||
_packageIcons[packageName] = icon;
|
||||
_packageIconTasks.remove(packageName);
|
||||
return icon;
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
void clearPackageIconCache() {
|
||||
_packageIcons.clear();
|
||||
_packageIconTasks.clear();
|
||||
}
|
||||
|
||||
Future<bool?> tip(String? message) async {
|
||||
|
||||
+1
-19
@@ -454,25 +454,7 @@ class PackageListItem extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListItem.checkbox(
|
||||
leading: SizedBox(
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: FutureBuilder<ImageProvider?>(
|
||||
future: app?.getPackageIcon(package.packageName),
|
||||
builder: (_, snapshot) {
|
||||
if (!snapshot.hasData && snapshot.data == null) {
|
||||
return Container();
|
||||
} else {
|
||||
return Image(
|
||||
image: snapshot.data!,
|
||||
gaplessPlayback: true,
|
||||
width: 48,
|
||||
height: 48,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
leading: PackageIcon(packageName: package.packageName, size: 48),
|
||||
title: Text(
|
||||
package.label,
|
||||
style: const TextStyle(overflow: TextOverflow.ellipsis),
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/core/controller.dart';
|
||||
import 'package:fl_clash/core/method.dart';
|
||||
@@ -12,26 +10,30 @@ import 'package:super_sliver_list/super_sliver_list.dart';
|
||||
import 'item.dart';
|
||||
|
||||
class ConnectionsView extends ConsumerStatefulWidget {
|
||||
const ConnectionsView({super.key});
|
||||
final Future<List<TrackerInfo>> Function()? connectionsReader;
|
||||
|
||||
const ConnectionsView({super.key, @visibleForTesting this.connectionsReader});
|
||||
|
||||
@override
|
||||
ConsumerState<ConnectionsView> createState() => _ConnectionsViewState();
|
||||
}
|
||||
|
||||
class _ConnectionsViewState extends ConsumerState<ConnectionsView> {
|
||||
class _ConnectionsViewState extends ConsumerState<ConnectionsView>
|
||||
with WidgetsBindingObserver, ActivePollingMixin<ConnectionsView> {
|
||||
final _connectionsStateNotifier = ValueNotifier<TrackerInfosState>(
|
||||
const TrackerInfosState(),
|
||||
);
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
Timer? timer;
|
||||
@override
|
||||
Duration get pollInterval => const Duration(seconds: 1);
|
||||
|
||||
List<Widget> _buildActions() {
|
||||
return [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
coreController.closeConnections();
|
||||
await _updateConnections();
|
||||
await _refreshConnections();
|
||||
},
|
||||
icon: const Icon(Icons.delete_sweep_outlined),
|
||||
),
|
||||
@@ -50,46 +52,53 @@ class _ConnectionsViewState extends ConsumerState<ConnectionsView> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _updateConnectionsTask() async {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
if (mounted) {
|
||||
await _updateConnections();
|
||||
timer = Timer(const Duration(seconds: 1), () async {
|
||||
_updateConnectionsTask();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_updateConnectionsTask();
|
||||
Future<void> poll(PollGuard isCurrent) async {
|
||||
final trackerInfos = await _readConnections();
|
||||
if (trackerInfos == null || !isCurrent()) {
|
||||
return;
|
||||
}
|
||||
_applyConnections(trackerInfos);
|
||||
}
|
||||
|
||||
Future<void> _updateConnections() async {
|
||||
Future<void> _refreshConnections() async {
|
||||
final trackerInfos = await _readConnections();
|
||||
if (trackerInfos == null || !mounted) {
|
||||
return;
|
||||
}
|
||||
_applyConnections(trackerInfos);
|
||||
}
|
||||
|
||||
Future<List<TrackerInfo>?> _readConnections() async {
|
||||
try {
|
||||
_connectionsStateNotifier.value = _connectionsStateNotifier.value
|
||||
.copyWith(trackerInfos: await coreController.getConnections());
|
||||
final connectionsReader = widget.connectionsReader;
|
||||
return connectionsReader != null
|
||||
? await connectionsReader()
|
||||
: await coreController.getConnections();
|
||||
} catch (error) {
|
||||
commonPrint.log(
|
||||
'updateConnections error: $error',
|
||||
logLevel: coreFailureLogLevel(error),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void _applyConnections(List<TrackerInfo> trackerInfos) {
|
||||
_connectionsStateNotifier.value = _connectionsStateNotifier.value.copyWith(
|
||||
trackerInfos: trackerInfos,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleBlockConnection(String id) async {
|
||||
await coreController.closeConnection(id);
|
||||
await _updateConnections();
|
||||
await _refreshConnections();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
timer?.cancel();
|
||||
_connectionsStateNotifier.dispose();
|
||||
_scrollController.dispose();
|
||||
timer = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -111,36 +120,32 @@ class _ConnectionsViewState extends ConsumerState<ConnectionsView> {
|
||||
illustration: const ConnectionEmptyIllustration(),
|
||||
);
|
||||
}
|
||||
final items = connections
|
||||
.map<Widget>(
|
||||
(trackerInfo) => TrackerInfoItem(
|
||||
key: Key(trackerInfo.id),
|
||||
trackerInfo: trackerInfo,
|
||||
onClickKeyword: (value) {
|
||||
context.commonScaffoldState?.addKeyword(value);
|
||||
},
|
||||
trailing: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
style: IconButton.styleFrom(minimumSize: Size.zero),
|
||||
icon: const Icon(Icons.block),
|
||||
onPressed: () {
|
||||
_handleBlockConnection(trackerInfo.id);
|
||||
},
|
||||
),
|
||||
detailTitle: appLocalizations.details(
|
||||
appLocalizations.connection,
|
||||
),
|
||||
),
|
||||
)
|
||||
.separated(const Divider(height: 0))
|
||||
.toList();
|
||||
return SuperListView.builder(
|
||||
return SuperListView.separated(
|
||||
controller: _scrollController,
|
||||
itemBuilder: (context, index) {
|
||||
return items[index];
|
||||
},
|
||||
itemCount: connections.length,
|
||||
separatorBuilder: (_, _) => const Divider(height: 0),
|
||||
itemBuilder: (_, index) {
|
||||
final trackerInfo = connections[index];
|
||||
return TrackerInfoItem(
|
||||
key: Key(trackerInfo.id),
|
||||
trackerInfo: trackerInfo,
|
||||
onClickKeyword: (value) {
|
||||
context.commonScaffoldState?.addKeyword(value);
|
||||
},
|
||||
trailing: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
style: IconButton.styleFrom(minimumSize: Size.zero),
|
||||
icon: const Icon(Icons.block),
|
||||
onPressed: () {
|
||||
_handleBlockConnection(trackerInfo.id);
|
||||
},
|
||||
),
|
||||
detailTitle: appLocalizations.details(
|
||||
appLocalizations.connection,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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/plugins/app.dart';
|
||||
import 'package:fl_clash/providers/config.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
@@ -26,10 +25,6 @@ class TrackerInfoItem extends ConsumerWidget {
|
||||
return globalState.measure.bodySmallHeight + 20;
|
||||
}
|
||||
|
||||
Future<ImageProvider?> _getPackageIcon(TrackerInfo connection) async {
|
||||
return await app?.getPackageIcon(connection.metadata.process);
|
||||
}
|
||||
|
||||
String _getSourceText(BuildContext context, TrackerInfo trackerInfo) {
|
||||
final progress = trackerInfo.progressText.isNotEmpty
|
||||
? '${trackerInfo.progressText} · '
|
||||
@@ -121,20 +116,9 @@ class TrackerInfoItem extends ConsumerWidget {
|
||||
margin: const EdgeInsets.only(top: 4),
|
||||
width: 42,
|
||||
height: 42,
|
||||
child: FutureBuilder<ImageProvider?>(
|
||||
future: _getPackageIcon(trackerInfo),
|
||||
builder: (_, snapshot) {
|
||||
if (!snapshot.hasData && snapshot.data == null) {
|
||||
return Container();
|
||||
} else {
|
||||
return Image(
|
||||
image: snapshot.data!,
|
||||
gaplessPlayback: true,
|
||||
width: 42,
|
||||
height: 42,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: PackageIcon(
|
||||
packageName: trackerInfo.metadata.process,
|
||||
size: 42,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -118,21 +118,6 @@ class _RequestsViewState extends ConsumerState<RequestsView> {
|
||||
label: appLocalizations.nullTip(appLocalizations.requests),
|
||||
);
|
||||
}
|
||||
final items = requests
|
||||
.map<Widget>(
|
||||
(trackerInfo) => TrackerInfoItem(
|
||||
key: Key(trackerInfo.id),
|
||||
trackerInfo: trackerInfo,
|
||||
onClickKeyword: (value) {
|
||||
context.commonScaffoldState?.addKeyword(value);
|
||||
},
|
||||
detailTitle: appLocalizations.details(
|
||||
appLocalizations.request,
|
||||
),
|
||||
),
|
||||
)
|
||||
.separated(const Divider(height: 0))
|
||||
.toList();
|
||||
return Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: CommonScrollBar(
|
||||
@@ -146,15 +131,26 @@ class _RequestsViewState extends ConsumerState<RequestsView> {
|
||||
_requestsStateNotifier.value = _requestsStateNotifier.value
|
||||
.copyWith(autoScrollToEnd: false);
|
||||
},
|
||||
child: SuperListView.builder(
|
||||
child: SuperListView.separated(
|
||||
reverse: true,
|
||||
shrinkWrap: true,
|
||||
physics: const NextClampingScrollPhysics(),
|
||||
controller: _scrollController,
|
||||
itemCount: requests.length,
|
||||
separatorBuilder: (_, _) => const Divider(height: 0),
|
||||
itemBuilder: (_, index) {
|
||||
return items[index];
|
||||
final trackerInfo = requests[index];
|
||||
return TrackerInfoItem(
|
||||
key: Key(trackerInfo.id),
|
||||
trackerInfo: trackerInfo,
|
||||
onClickKeyword: (value) {
|
||||
context.commonScaffoldState?.addKeyword(value);
|
||||
},
|
||||
detailTitle: appLocalizations.details(
|
||||
appLocalizations.request,
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: items.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/core/controller.dart';
|
||||
import 'package:fl_clash/core/method.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/providers/app.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
@@ -20,85 +20,31 @@ class MemoryInfo extends StatefulWidget {
|
||||
State<MemoryInfo> createState() => _MemoryInfoState();
|
||||
}
|
||||
|
||||
class _MemoryInfoState extends State<MemoryInfo> with WidgetsBindingObserver {
|
||||
Timer? _timer;
|
||||
bool _isForeground = false;
|
||||
bool _isUpdating = false;
|
||||
int _updateGeneration = 0;
|
||||
class _MemoryInfoState extends State<MemoryInfo>
|
||||
with WidgetsBindingObserver, ActivePollingMixin<MemoryInfo> {
|
||||
@override
|
||||
Duration get pollInterval => const Duration(seconds: 2);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_isForeground =
|
||||
WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted && _isForeground) {
|
||||
_startUpdating();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_isForeground = false;
|
||||
_stopUpdating();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
final isForeground = state == AppLifecycleState.resumed;
|
||||
if (_isForeground == isForeground) {
|
||||
return;
|
||||
}
|
||||
_isForeground = isForeground;
|
||||
if (isForeground) {
|
||||
_startUpdating();
|
||||
} else {
|
||||
_stopUpdating();
|
||||
}
|
||||
}
|
||||
|
||||
void _startUpdating() {
|
||||
if (!mounted || !_isForeground || _isUpdating) {
|
||||
return;
|
||||
}
|
||||
_isUpdating = true;
|
||||
final generation = ++_updateGeneration;
|
||||
unawaited(_updateMemory(generation));
|
||||
}
|
||||
|
||||
void _stopUpdating() {
|
||||
_isUpdating = false;
|
||||
_updateGeneration++;
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
}
|
||||
|
||||
Future<void> _updateMemory(int generation) async {
|
||||
final memoryReader = widget.memoryReader;
|
||||
final memory = memoryReader != null
|
||||
? await memoryReader()
|
||||
: await _readMemory();
|
||||
if (!mounted ||
|
||||
!_isForeground ||
|
||||
!_isUpdating ||
|
||||
generation != _updateGeneration) {
|
||||
Future<void> poll(PollGuard isCurrent) async {
|
||||
final memory = await _readMemory();
|
||||
if (memory == null || !isCurrent()) {
|
||||
return;
|
||||
}
|
||||
_memoryStateNotifier.value = memory;
|
||||
_timer = Timer(const Duration(seconds: 2), () {
|
||||
_timer = null;
|
||||
if (mounted &&
|
||||
_isForeground &&
|
||||
_isUpdating &&
|
||||
generation == _updateGeneration) {
|
||||
unawaited(_updateMemory(generation));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<num?> _readMemory() async {
|
||||
try {
|
||||
final memoryReader = widget.memoryReader;
|
||||
return memoryReader != null ? await memoryReader() : await _readTotal();
|
||||
} catch (error) {
|
||||
commonPrint.log(
|
||||
'updateMemory error: $error',
|
||||
logLevel: coreFailureLogLevel(error),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -156,7 +102,7 @@ class _MemoryInfoState extends State<MemoryInfo> with WidgetsBindingObserver {
|
||||
}
|
||||
}
|
||||
|
||||
Future<num> _readMemory() async {
|
||||
Future<num> _readTotal() async {
|
||||
final rss = ProcessInfo.currentRss;
|
||||
final coreConnected =
|
||||
globalState.container.read(coreStatusProvider) == CoreStatus.connected;
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fl_clash/common/print.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/widgets/inherited.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
typedef PollGuard = bool Function();
|
||||
|
||||
mixin ActivePollingMixin<T extends StatefulWidget>
|
||||
on State<T>, WidgetsBindingObserver {
|
||||
Timer? _pollTimer;
|
||||
bool _isForeground = false;
|
||||
bool _isPageActive = true;
|
||||
bool _isPolling = false;
|
||||
int _pollGeneration = 0;
|
||||
|
||||
Duration get pollInterval;
|
||||
|
||||
bool get pollOnStart => true;
|
||||
|
||||
Future<void> poll(PollGuard isCurrent);
|
||||
|
||||
bool get canPoll => mounted && _isForeground && _isPageActive;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
final lifecycleState = WidgetsBinding.instance.lifecycleState;
|
||||
_isForeground =
|
||||
lifecycleState == null || lifecycleState == AppLifecycleState.resumed;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
_syncPolling();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final isPageActive = PageActivityScope.isActiveOf(context);
|
||||
if (_isPageActive == isPageActive) {
|
||||
return;
|
||||
}
|
||||
_isPageActive = isPageActive;
|
||||
_syncPolling();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
final isForeground = state == AppLifecycleState.resumed;
|
||||
if (_isForeground == isForeground) {
|
||||
return;
|
||||
}
|
||||
_isForeground = isForeground;
|
||||
_syncPolling();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_isForeground = false;
|
||||
stopPolling();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void startPolling() {
|
||||
if (!canPoll || _isPolling) {
|
||||
return;
|
||||
}
|
||||
_isPolling = true;
|
||||
final generation = ++_pollGeneration;
|
||||
if (pollOnStart) {
|
||||
unawaited(_runPoll(generation));
|
||||
return;
|
||||
}
|
||||
_schedulePoll(generation);
|
||||
}
|
||||
|
||||
void stopPolling() {
|
||||
_isPolling = false;
|
||||
_pollGeneration++;
|
||||
_pollTimer?.cancel();
|
||||
_pollTimer = null;
|
||||
}
|
||||
|
||||
void restartPolling() {
|
||||
stopPolling();
|
||||
_syncPolling();
|
||||
}
|
||||
|
||||
void _syncPolling() {
|
||||
if (canPoll) {
|
||||
startPolling();
|
||||
} else {
|
||||
stopPolling();
|
||||
}
|
||||
}
|
||||
|
||||
bool _isCurrentPoll(int generation) =>
|
||||
canPoll && _isPolling && generation == _pollGeneration;
|
||||
|
||||
void _schedulePoll(int generation) {
|
||||
_pollTimer = Timer(pollInterval, () {
|
||||
_pollTimer = null;
|
||||
if (_isCurrentPoll(generation)) {
|
||||
unawaited(_runPoll(generation));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _runPoll(int generation) async {
|
||||
try {
|
||||
await poll(() => _isCurrentPoll(generation));
|
||||
} catch (error) {
|
||||
commonPrint.log(
|
||||
'$runtimeType poll error: $error',
|
||||
logLevel: LogLevel.warning,
|
||||
);
|
||||
} finally {
|
||||
if (_isCurrentPoll(generation)) {
|
||||
_schedulePoll(generation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-24
@@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fl_clash/widgets/active_polling.dart';
|
||||
import 'package:fl_clash/widgets/inherited.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -16,40 +15,31 @@ class TickBuilder extends StatefulWidget {
|
||||
State<TickBuilder> createState() => _TickBuilderState();
|
||||
}
|
||||
|
||||
class _TickBuilderState extends State<TickBuilder> {
|
||||
Timer? _timer;
|
||||
class _TickBuilderState extends State<TickBuilder>
|
||||
with WidgetsBindingObserver, ActivePollingMixin<TickBuilder> {
|
||||
int _tick = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_startTimer();
|
||||
Duration get pollInterval => widget.duration;
|
||||
|
||||
@override
|
||||
bool get pollOnStart => false;
|
||||
|
||||
@override
|
||||
Future<void> poll(PollGuard isCurrent) async {
|
||||
setState(() {
|
||||
_tick++;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant TickBuilder oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.duration != widget.duration) {
|
||||
_startTimer();
|
||||
restartPolling();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _startTimer() {
|
||||
_timer?.cancel();
|
||||
_timer = Timer.periodic(widget.duration, (_) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_tick++;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.builder(context, _tick);
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'dart:io';
|
||||
import 'package:fl_clash/common/cache.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/database/database.dart';
|
||||
import 'package:fl_clash/plugins/app.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
@@ -125,6 +126,72 @@ class _ImageCacheWidgetState extends State<ImageCacheWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
class PackageIcon extends StatefulWidget {
|
||||
final String packageName;
|
||||
final double size;
|
||||
|
||||
const PackageIcon({super.key, required this.packageName, required this.size});
|
||||
|
||||
@override
|
||||
State<PackageIcon> createState() => _PackageIconState();
|
||||
}
|
||||
|
||||
class _PackageIconState extends State<PackageIcon> {
|
||||
ImageProvider? _icon;
|
||||
int _generation = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadIcon();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant PackageIcon oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.packageName != widget.packageName) {
|
||||
_loadIcon();
|
||||
}
|
||||
}
|
||||
|
||||
void _loadIcon() {
|
||||
final generation = ++_generation;
|
||||
final packageName = widget.packageName;
|
||||
final currentApp = app;
|
||||
if (currentApp == null || packageName.isEmpty) {
|
||||
_icon = null;
|
||||
return;
|
||||
}
|
||||
if (currentApp.hasPackageIcon(packageName)) {
|
||||
_icon = currentApp.getCachedPackageIcon(packageName);
|
||||
return;
|
||||
}
|
||||
_icon = null;
|
||||
currentApp.getPackageIcon(packageName).then((icon) {
|
||||
if (!mounted || generation != _generation || icon == null) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_icon = icon;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final icon = _icon;
|
||||
if (icon == null) {
|
||||
return SizedBox(width: widget.size, height: widget.size);
|
||||
}
|
||||
return Image(
|
||||
image: icon,
|
||||
gaplessPlayback: true,
|
||||
width: widget.size,
|
||||
height: widget.size,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CommonImage extends StatelessWidget {
|
||||
final File data;
|
||||
final bool isSvg;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export 'activate_box.dart';
|
||||
export 'active_polling.dart';
|
||||
export 'animate_grid.dart';
|
||||
export 'animated_visibility.dart';
|
||||
export 'builder.dart';
|
||||
|
||||
Reference in New Issue
Block a user