Refactor
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
import 'package:hiddify/features/per_app_proxy/data/per_app_proxy_repository.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'per_app_proxy_data_providers.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
PerAppProxyRepository perAppProxyRepository(PerAppProxyRepositoryRef ref) {
|
||||
return PerAppProxyRepositoryImpl();
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:hiddify/features/per_app_proxy/model/installed_package_info.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
|
||||
abstract interface class PerAppProxyRepository {
|
||||
TaskEither<String, List<InstalledPackageInfo>> getInstalledPackages();
|
||||
TaskEither<String, Uint8List> getPackageIcon(String packageName);
|
||||
}
|
||||
|
||||
class PerAppProxyRepositoryImpl
|
||||
with InfraLogger
|
||||
implements PerAppProxyRepository {
|
||||
final _methodChannel = const MethodChannel("app.hiddify.com/platform");
|
||||
|
||||
@override
|
||||
TaskEither<String, List<InstalledPackageInfo>> getInstalledPackages() {
|
||||
return TaskEither(
|
||||
() async {
|
||||
loggy.debug("getting installed packages info");
|
||||
final result =
|
||||
await _methodChannel.invokeMethod<String>("get_installed_packages");
|
||||
if (result == null) return left("null response");
|
||||
return right(
|
||||
(jsonDecode(result) as List).map((e) {
|
||||
return InstalledPackageInfo.fromJson(e as Map<String, dynamic>);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<String, Uint8List> getPackageIcon(String packageName) {
|
||||
return TaskEither(
|
||||
() async {
|
||||
loggy.debug("getting package [$packageName] icon");
|
||||
final result = await _methodChannel.invokeMethod<String>(
|
||||
"get_package_icon",
|
||||
{"packageName": packageName},
|
||||
);
|
||||
if (result == null) return left("null response");
|
||||
final Uint8List decoded;
|
||||
try {
|
||||
decoded = base64.decode(result);
|
||||
} catch (e) {
|
||||
return left("error parsing base64 response");
|
||||
}
|
||||
return right(decoded);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
17
lib/features/per_app_proxy/model/installed_package_info.dart
Normal file
17
lib/features/per_app_proxy/model/installed_package_info.dart
Normal file
@@ -0,0 +1,17 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'installed_package_info.freezed.dart';
|
||||
part 'installed_package_info.g.dart';
|
||||
|
||||
@freezed
|
||||
class InstalledPackageInfo with _$InstalledPackageInfo {
|
||||
@JsonSerializable(fieldRename: FieldRename.kebab)
|
||||
const factory InstalledPackageInfo({
|
||||
required String packageName,
|
||||
required String name,
|
||||
required bool isSystemApp,
|
||||
}) = _InstalledPackageInfo;
|
||||
|
||||
factory InstalledPackageInfo.fromJson(Map<String, dynamic> json) =>
|
||||
_$InstalledPackageInfoFromJson(json);
|
||||
}
|
||||
24
lib/features/per_app_proxy/model/per_app_proxy_mode.dart
Normal file
24
lib/features/per_app_proxy/model/per_app_proxy_mode.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'package:hiddify/core/localization/translations.dart';
|
||||
|
||||
enum PerAppProxyMode {
|
||||
off,
|
||||
include,
|
||||
exclude;
|
||||
|
||||
bool get enabled => this != off;
|
||||
|
||||
({String title, String message}) present(TranslationsEn t) => switch (this) {
|
||||
off => (
|
||||
title: t.settings.network.perAppProxyModes.off,
|
||||
message: t.settings.network.perAppProxyModes.offMsg,
|
||||
),
|
||||
include => (
|
||||
title: t.settings.network.perAppProxyModes.include,
|
||||
message: t.settings.network.perAppProxyModes.includeMsg,
|
||||
),
|
||||
exclude => (
|
||||
title: t.settings.network.perAppProxyModes.exclude,
|
||||
message: t.settings.network.perAppProxyModes.excludeMsg,
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hiddify/features/per_app_proxy/data/per_app_proxy_data_providers.dart';
|
||||
import 'package:hiddify/features/per_app_proxy/model/installed_package_info.dart';
|
||||
import 'package:hiddify/utils/riverpod_utils.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'per_app_proxy_notifier.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<List<InstalledPackageInfo>> installedPackagesInfo(
|
||||
InstalledPackagesInfoRef ref,
|
||||
) async {
|
||||
return ref
|
||||
.watch(perAppProxyRepositoryProvider)
|
||||
.getInstalledPackages()
|
||||
.getOrElse((err) {
|
||||
// _logger.error("error getting installed packages", err);
|
||||
throw err;
|
||||
}).run();
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<ImageProvider> packageIcon(
|
||||
PackageIconRef ref,
|
||||
String packageName,
|
||||
) async {
|
||||
ref.disposeDelay(const Duration(seconds: 10));
|
||||
final bytes = await ref
|
||||
.watch(perAppProxyRepositoryProvider)
|
||||
.getPackageIcon(packageName)
|
||||
.getOrElse((err) {
|
||||
// _logger.warning("error getting package icon", err);
|
||||
throw err;
|
||||
}).run();
|
||||
return MemoryImage(bytes);
|
||||
}
|
||||
201
lib/features/per_app_proxy/overview/per_app_proxy_page.dart
Normal file
201
lib/features/per_app_proxy/overview/per_app_proxy_page.dart
Normal file
@@ -0,0 +1,201 @@
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hiddify/core/localization/translations.dart';
|
||||
import 'package:hiddify/core/preferences/general_preferences.dart';
|
||||
import 'package:hiddify/features/per_app_proxy/model/installed_package_info.dart';
|
||||
import 'package:hiddify/features/per_app_proxy/model/per_app_proxy_mode.dart';
|
||||
import 'package:hiddify/features/per_app_proxy/overview/per_app_proxy_notifier.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:sliver_tools/sliver_tools.dart';
|
||||
|
||||
class PerAppProxyPage extends HookConsumerWidget with PresLogger {
|
||||
const PerAppProxyPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final t = ref.watch(translationsProvider);
|
||||
final localizations = MaterialLocalizations.of(context);
|
||||
|
||||
final asyncPackages = ref.watch(installedPackagesInfoProvider);
|
||||
final perAppProxyMode = ref.watch(perAppProxyModeNotifierProvider);
|
||||
final perAppProxyList = ref.watch(perAppProxyListProvider);
|
||||
|
||||
final showSystemApps = useState(true);
|
||||
final isSearching = useState(false);
|
||||
final searchQuery = useState("");
|
||||
|
||||
final filteredPackages = useMemoized(
|
||||
() {
|
||||
if (showSystemApps.value && searchQuery.value.isBlank) {
|
||||
return asyncPackages;
|
||||
}
|
||||
return asyncPackages.whenData(
|
||||
(value) {
|
||||
Iterable<InstalledPackageInfo> result = value;
|
||||
if (!showSystemApps.value) {
|
||||
result = result.filter((e) => !e.isSystemApp);
|
||||
}
|
||||
if (!searchQuery.value.isBlank) {
|
||||
result = result.filter(
|
||||
(e) => e.name
|
||||
.toLowerCase()
|
||||
.contains(searchQuery.value.toLowerCase()),
|
||||
);
|
||||
}
|
||||
return result.toList();
|
||||
},
|
||||
);
|
||||
},
|
||||
[asyncPackages, showSystemApps.value, searchQuery.value],
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: isSearching.value
|
||||
? AppBar(
|
||||
title: TextFormField(
|
||||
onChanged: (value) => searchQuery.value = value,
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(
|
||||
hintText: "${localizations.searchFieldLabel}...",
|
||||
isDense: true,
|
||||
filled: false,
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
errorBorder: InputBorder.none,
|
||||
focusedErrorBorder: InputBorder.none,
|
||||
disabledBorder: InputBorder.none,
|
||||
),
|
||||
),
|
||||
leading: IconButton(
|
||||
onPressed: () {
|
||||
searchQuery.value = "";
|
||||
isSearching.value = false;
|
||||
},
|
||||
icon: const Icon(Icons.close),
|
||||
tooltip: localizations.cancelButtonLabel,
|
||||
),
|
||||
)
|
||||
: AppBar(
|
||||
title: Text(t.settings.network.perAppProxyPageTitle),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.search),
|
||||
onPressed: () => isSearching.value = true,
|
||||
tooltip: localizations.searchFieldLabel,
|
||||
),
|
||||
PopupMenuButton(
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
child: Text(
|
||||
showSystemApps.value
|
||||
? t.settings.network.hideSystemApps
|
||||
: t.settings.network.showSystemApps,
|
||||
),
|
||||
onTap: () =>
|
||||
showSystemApps.value = !showSystemApps.value,
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Text(t.settings.network.clearSelection),
|
||||
onTap: () => ref
|
||||
.read(perAppProxyListProvider.notifier)
|
||||
.update([]),
|
||||
),
|
||||
];
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SliverPinnedHeader(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
...PerAppProxyMode.values.map(
|
||||
(e) => RadioListTile<PerAppProxyMode>(
|
||||
title: Text(e.present(t).message),
|
||||
dense: true,
|
||||
value: e,
|
||||
groupValue: perAppProxyMode,
|
||||
onChanged: (value) async {
|
||||
await ref
|
||||
.read(perAppProxyModeNotifierProvider.notifier)
|
||||
.update(e);
|
||||
if (e == PerAppProxyMode.off && context.mounted) {
|
||||
context.pop();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
switch (filteredPackages) {
|
||||
AsyncData(value: final packages) => SliverList.builder(
|
||||
itemBuilder: (context, index) {
|
||||
final package = packages[index];
|
||||
final selected =
|
||||
perAppProxyList.contains(package.packageName);
|
||||
return CheckboxListTile(
|
||||
title: Text(
|
||||
package.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text(
|
||||
package.packageName,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
value: selected,
|
||||
onChanged: (value) async {
|
||||
final List<String> newSelection;
|
||||
if (selected) {
|
||||
newSelection = perAppProxyList
|
||||
.exceptElement(package.packageName)
|
||||
.toList();
|
||||
} else {
|
||||
newSelection = [
|
||||
...perAppProxyList,
|
||||
package.packageName,
|
||||
];
|
||||
}
|
||||
await ref
|
||||
.read(perAppProxyListProvider.notifier)
|
||||
.update(newSelection);
|
||||
},
|
||||
secondary: SizedBox(
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: ref
|
||||
.watch(packageIconProvider(package.packageName))
|
||||
.when(
|
||||
data: (data) => Image(image: data),
|
||||
error: (error, _) => const Icon(Icons.error),
|
||||
loading: () => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: packages.length,
|
||||
),
|
||||
AsyncLoading() => const SliverLoadingBodyPlaceholder(),
|
||||
AsyncError(:final error) =>
|
||||
SliverErrorBodyPlaceholder(error.toString()),
|
||||
_ => const SliverToBoxAdapter(),
|
||||
},
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user