// ignore_for_file: invalid_annotation_target import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:fl_clash/clash/core.dart'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'clash_config.dart'; part 'generated/profile.freezed.dart'; part 'generated/profile.g.dart'; typedef SelectedMap = Map; @freezed class SubscriptionInfo with _$SubscriptionInfo { const factory SubscriptionInfo({ @Default(0) int upload, @Default(0) int download, @Default(0) int total, @Default(0) int expire, }) = _SubscriptionInfo; factory SubscriptionInfo.fromJson(Map json) => _$SubscriptionInfoFromJson(json); factory SubscriptionInfo.formHString(String? info) { if (info == null) return const SubscriptionInfo(); final list = info.split(";"); Map map = {}; for (final i in list) { final keyValue = i.trim().split("="); map[keyValue[0]] = int.tryParse(keyValue[1]); } return SubscriptionInfo( upload: map["upload"] ?? 0, download: map["download"] ?? 0, total: map["total"] ?? 0, expire: map["expire"] ?? 0, ); } } @freezed class Profile with _$Profile { const factory Profile({ required String id, String? label, String? currentGroupName, @Default("") String url, DateTime? lastUpdateDate, required Duration autoUpdateDuration, SubscriptionInfo? subscriptionInfo, @Default(true) bool autoUpdate, @Default({}) SelectedMap selectedMap, @Default({}) Set unfoldSet, @Default(OverrideData()) OverrideData overrideData, @JsonKey(includeToJson: false, includeFromJson: false) @Default(false) bool isUpdating, }) = _Profile; factory Profile.fromJson(Map json) => _$ProfileFromJson(json); factory Profile.normal({ String? label, String url = '', }) { return Profile( label: label, url: url, id: DateTime.now().millisecondsSinceEpoch.toString(), autoUpdateDuration: defaultUpdateDuration, ); } } @freezed class OverrideData with _$OverrideData { const factory OverrideData({ @Default(false) bool enable, @Default(OverrideRule()) OverrideRule rule, }) = _OverrideData; factory OverrideData.fromJson(Map json) => _$OverrideDataFromJson(json); } extension OverrideDataExt on OverrideData { List get runningRule { if (!enable) { return []; } return rule.rules.map((item) => item.value).toList(); } } @freezed class OverrideRule with _$OverrideRule { const factory OverrideRule({ @Default(OverrideRuleType.added) OverrideRuleType type, @Default([]) List overrideRules, @Default([]) List addedRules, }) = _OverrideRule; factory OverrideRule.fromJson(Map json) => _$OverrideRuleFromJson(json); } extension OverrideRuleExt on OverrideRule { List get rules => switch (type == OverrideRuleType.override) { true => overrideRules, false => addedRules, }; OverrideRule updateRules(List Function(List rules) builder) { if (type == OverrideRuleType.added) { return copyWith(addedRules: builder(addedRules)); } return copyWith(overrideRules: builder(overrideRules)); } } extension ProfilesExt on List { Profile? getProfile(String? profileId) { final index = indexWhere((profile) => profile.id == profileId); return index == -1 ? null : this[index]; } } extension ProfileExtension on Profile { ProfileType get type => url.isEmpty == true ? ProfileType.file : ProfileType.url; bool get realAutoUpdate => url.isEmpty == true ? false : autoUpdate; Future checkAndUpdate() async { final isExists = await check(); if (!isExists) { if (url.isNotEmpty) { await update(); } } } Future check() async { final profilePath = await appPath.getProfilePath(id); return await File(profilePath!).exists(); } Future getFile() async { final path = await appPath.getProfilePath(id); final file = File(path!); final isExists = await file.exists(); if (!isExists) { await file.create(recursive: true); } return file; } Future get profileLastModified async { final file = await getFile(); return (await file.lastModified()).microsecondsSinceEpoch; } Future update() async { final response = await request.getFileResponseForUrl(url); final disposition = response.headers.value("content-disposition"); final userinfo = response.headers.value('subscription-userinfo'); return await copyWith( label: label ?? utils.getFileNameForDisposition(disposition) ?? id, subscriptionInfo: SubscriptionInfo.formHString(userinfo), ).saveFile(response.data); } Future saveFile(Uint8List bytes) async { final message = await clashCore.validateConfig(utf8.decode(bytes)); if (message.isNotEmpty) { throw message; } final file = await getFile(); await file.writeAsBytes(bytes); return copyWith(lastUpdateDate: DateTime.now()); } Future saveFileWithString(String value) async { final message = await clashCore.validateConfig(value); if (message.isNotEmpty) { throw message; } final file = await getFile(); await file.writeAsString(value); return copyWith(lastUpdateDate: DateTime.now()); } }