This commit is contained in:
problematicconsumer
2023-12-01 12:56:24 +03:30
parent 9c165e178b
commit ed614988a2
181 changed files with 3092 additions and 2341 deletions

View File

@@ -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();
}

View File

@@ -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);
},
);
}
}

View 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);
}

View 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,
),
};
}

View File

@@ -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);
}

View 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(),
},
],
),
);
}
}