import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:isolate'; import 'dart:typed_data'; import 'package:archive/archive.dart'; import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/common/archive.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/plugins/app.dart'; import 'package:fl_clash/providers/providers.dart'; import 'package:fl_clash/state.dart'; import 'package:fl_clash/widgets/dialog.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path/path.dart'; import 'package:url_launcher/url_launcher.dart'; import 'common/common.dart'; import 'models/models.dart'; import 'views/profiles/override_profile.dart'; class AppController { bool lastTunEnable = false; int? lastProfileModified; final BuildContext context; final WidgetRef _ref; AppController(this.context, WidgetRef ref) : _ref = ref; setupClashConfigDebounce() { debouncer.call(FunctionTag.setupClashConfig, () async { await setupClashConfig(); }); } updateClashConfigDebounce() { debouncer.call(FunctionTag.updateClashConfig, () async { await updateClashConfig(); }); } updateGroupsDebounce() { debouncer.call(FunctionTag.updateGroups, updateGroups); } addCheckIpNumDebounce() { debouncer.call(FunctionTag.addCheckIpNum, () { _ref.read(checkIpNumProvider.notifier).add(); }); } applyProfileDebounce({ bool silence = false, }) { debouncer.call(FunctionTag.applyProfile, (silence) { applyProfile(silence: silence); }, args: [silence]); } savePreferencesDebounce() { debouncer.call(FunctionTag.savePreferences, savePreferences); } changeProxyDebounce(String groupName, String proxyName) { debouncer.call(FunctionTag.changeProxy, (String groupName, String proxyName) async { await changeProxy( groupName: groupName, proxyName: proxyName, ); await updateGroups(); }, args: [groupName, proxyName]); } restartCore() async { commonPrint.log("restart core"); await clashService?.reStart(); await _initCore(); if (_ref.read(runTimeProvider.notifier).isStart) { await globalState.handleStart(); } } updateStatus(bool isStart) async { if (isStart) { await globalState.handleStart([ updateRunTime, updateTraffic, ]); final currentLastModified = await _ref.read(currentProfileProvider)?.profileLastModified; if (currentLastModified == null || lastProfileModified == null) { addCheckIpNumDebounce(); return; } if (currentLastModified <= (lastProfileModified ?? 0)) { addCheckIpNumDebounce(); return; } applyProfileDebounce(); } else { await globalState.handleStop(); await clashCore.resetTraffic(); _ref.read(trafficsProvider.notifier).clear(); _ref.read(totalTrafficProvider.notifier).value = Traffic(); _ref.read(runTimeProvider.notifier).value = null; addCheckIpNumDebounce(); } } updateRunTime() { final startTime = globalState.startTime; if (startTime != null) { final startTimeStamp = startTime.millisecondsSinceEpoch; final nowTimeStamp = DateTime.now().millisecondsSinceEpoch; _ref.read(runTimeProvider.notifier).value = nowTimeStamp - startTimeStamp; } else { _ref.read(runTimeProvider.notifier).value = null; } } updateTraffic() async { final traffic = await clashCore.getTraffic(); _ref.read(trafficsProvider.notifier).addTraffic(traffic); _ref.read(totalTrafficProvider.notifier).value = await clashCore.getTotalTraffic(); } addProfile(Profile profile) async { _ref.read(profilesProvider.notifier).setProfile(profile); if (_ref.read(currentProfileIdProvider) != null) return; _ref.read(currentProfileIdProvider.notifier).value = profile.id; } deleteProfile(String id) async { _ref.read(profilesProvider.notifier).deleteProfileById(id); clearEffect(id); if (globalState.config.currentProfileId == id) { final profiles = globalState.config.profiles; final currentProfileId = _ref.read(currentProfileIdProvider.notifier); if (profiles.isNotEmpty) { final updateId = profiles.first.id; currentProfileId.value = updateId; } else { currentProfileId.value = null; updateStatus(false); } } } updateProviders() async { _ref.read(providersProvider.notifier).value = await clashCore.getExternalProviders(); } updateLocalIp() async { _ref.read(localIpProvider.notifier).value = null; await Future.delayed(commonDuration); _ref.read(localIpProvider.notifier).value = await utils.getLocalIpAddress(); } Future updateProfile(Profile profile) async { final newProfile = await profile.update(); _ref .read(profilesProvider.notifier) .setProfile(newProfile.copyWith(isUpdating: false)); if (profile.id == _ref.read(currentProfileIdProvider)) { applyProfileDebounce(silence: true); } } setProfile(Profile profile) { _ref.read(profilesProvider.notifier).setProfile(profile); } setProfileAndAutoApply(Profile profile) { _ref.read(profilesProvider.notifier).setProfile(profile); if (profile.id == _ref.read(currentProfileIdProvider)) { applyProfileDebounce(silence: true); } } setProfiles(List profiles) { _ref.read(profilesProvider.notifier).value = profiles; } addLog(Log log) { _ref.read(logsProvider).add(log); } updateOrAddHotKeyAction(HotKeyAction hotKeyAction) { final hotKeyActions = _ref.read(hotKeyActionsProvider); final index = hotKeyActions.indexWhere((item) => item.action == hotKeyAction.action); if (index == -1) { _ref.read(hotKeyActionsProvider.notifier).value = List.from(hotKeyActions) ..add(hotKeyAction); } else { _ref.read(hotKeyActionsProvider.notifier).value = List.from(hotKeyActions) ..[index] = hotKeyAction; } _ref.read(hotKeyActionsProvider.notifier).value = index == -1 ? (List.from(hotKeyActions)..add(hotKeyAction)) : (List.from(hotKeyActions)..[index] = hotKeyAction); } List getCurrentGroups() { return _ref.read(currentGroupsStateProvider.select((state) => state.value)); } String getRealTestUrl(String? url) { return _ref.read(getRealTestUrlProvider(url)); } int getProxiesColumns() { return _ref.read(getProxiesColumnsProvider); } addSortNum() { return _ref.read(sortNumProvider.notifier).add(); } getCurrentGroupName() { final currentGroupName = _ref.read(currentProfileProvider.select( (state) => state?.currentGroupName, )); return currentGroupName; } ProxyCardState getProxyCardState(proxyName) { return _ref.read(getProxyCardStateProvider(proxyName)); } getSelectedProxyName(groupName) { return _ref.read(getSelectedProxyNameProvider(groupName)); } updateCurrentGroupName(String groupName) { final profile = _ref.read(currentProfileProvider); if (profile == null || profile.currentGroupName == groupName) { return; } setProfile( profile.copyWith(currentGroupName: groupName), ); } Future updateClashConfig() async { final commonScaffoldState = globalState.homeScaffoldKey.currentState; if (commonScaffoldState?.mounted != true) return; await commonScaffoldState?.loadingRun(() async { await _updateClashConfig(); }); } Future _updateClashConfig() async { final updateParams = _ref.read(updateParamsProvider); final res = await _requestAdmin(updateParams.tun.enable); if (res.isError) { return; } lastTunEnable = res.data == true; final message = await clashCore.updateConfig( updateParams.copyWith.tun( enable: lastTunEnable, ), ); if (message.isNotEmpty) throw message; } Future> _requestAdmin(bool enableTun) async { if (enableTun != lastTunEnable && lastTunEnable == false) { final code = await system.authorizeCore(); switch (code) { case AuthorizeCode.none: return Result.success(enableTun); case AuthorizeCode.success: await restartCore(); return Result.error(""); case AuthorizeCode.error: enableTun = false; return Result.success(false); } } return Result.success(enableTun); } Future setupClashConfig() async { final commonScaffoldState = globalState.homeScaffoldKey.currentState; if (commonScaffoldState?.mounted != true) return; await commonScaffoldState?.loadingRun(() async { await _setupClashConfig(); }); } _setupClashConfig() async { await _ref.read(currentProfileProvider)?.checkAndUpdate(); final patchConfig = _ref.read(patchClashConfigProvider); final res = await _requestAdmin(patchConfig.tun.enable); if (res.isError) { return; } lastTunEnable = res.data == true; final realPatchConfig = patchConfig.copyWith.tun(enable: lastTunEnable); final params = await globalState.getSetupParams( pathConfig: realPatchConfig, ); final message = await clashCore.setupConfig(params); lastProfileModified = await _ref.read( currentProfileProvider.select( (state) => state?.profileLastModified, ), ); if (message.isNotEmpty) { throw message; } } Future _applyProfile() async { await clashCore.requestGc(); await setupClashConfig(); await updateGroups(); await updateProviders(); } Future applyProfile({bool silence = false}) async { if (silence) { await _applyProfile(); } else { final commonScaffoldState = globalState.homeScaffoldKey.currentState; if (commonScaffoldState?.mounted != true) return; await commonScaffoldState?.loadingRun(() async { await _applyProfile(); }); } addCheckIpNumDebounce(); } handleChangeProfile() { _ref.read(delayDataSourceProvider.notifier).value = {}; applyProfile(); _ref.read(logsProvider.notifier).value = FixedList(500); _ref.read(requestsProvider.notifier).value = FixedList(500); globalState.cacheHeightMap = {}; globalState.cacheScrollPosition = {}; } updateBrightness(Brightness brightness) { _ref.read(appBrightnessProvider.notifier).value = brightness; } autoUpdateProfiles() async { for (final profile in _ref.read(profilesProvider)) { if (!profile.autoUpdate) continue; final isNotNeedUpdate = profile.lastUpdateDate ?.add( profile.autoUpdateDuration, ) .isBeforeNow; if (isNotNeedUpdate == false || profile.type == ProfileType.file) { continue; } try { await updateProfile(profile); } catch (e) { commonPrint.log(e.toString()); } } } Future updateGroups() async { try { _ref.read(groupsProvider.notifier).value = await retry( task: () async { return await clashCore.getProxiesGroups(); }, retryIf: (res) => res.isEmpty, ); } catch (_) { _ref.read(groupsProvider.notifier).value = []; } } updateProfiles() async { for (final profile in _ref.read(profilesProvider)) { if (profile.type == ProfileType.file) { continue; } await updateProfile(profile); } } savePreferences() async { commonPrint.log("save preferences"); await preferences.saveConfig(globalState.config); } changeProxy({ required String groupName, required String proxyName, }) async { await clashCore.changeProxy( ChangeProxyParams( groupName: groupName, proxyName: proxyName, ), ); if (_ref.read(appSettingProvider).closeConnections) { clashCore.closeConnections(); } addCheckIpNumDebounce(); } handleBackOrExit() async { if (_ref.read(backBlockProvider)) { return; } if (_ref.read(appSettingProvider).minimizeOnExit) { if (system.isDesktop) { await savePreferencesDebounce(); } await system.back(); } else { await handleExit(); } } backBlock() { _ref.read(backBlockProvider.notifier).value = true; } unBackBlock() { _ref.read(backBlockProvider.notifier).value = false; } handleExit() async { Future.delayed(commonDuration, () { system.exit(); }); try { await savePreferences(); await proxy?.stopProxy(); await clashCore.shutdown(); await clashService?.destroy(); } finally { system.exit(); } } Future handleClear() async { await preferences.clearPreferences(); commonPrint.log("clear preferences"); globalState.config = Config( themeProps: defaultThemeProps, ); } autoCheckUpdate() async { if (!_ref.read(appSettingProvider).autoCheckUpdate) return; final res = await request.checkForUpdate(); checkUpdateResultHandle(data: res); } checkUpdateResultHandle({ Map? data, bool handleError = false, }) async { if (globalState.isPre) { return; } if (data != null) { final tagName = data['tag_name']; final body = data['body']; final submits = utils.parseReleaseBody(body); final textTheme = context.textTheme; final res = await globalState.showMessage( title: appLocalizations.discoverNewVersion, message: TextSpan( text: "$tagName \n", style: textTheme.headlineSmall, children: [ TextSpan( text: "\n", style: textTheme.bodyMedium, ), for (final submit in submits) TextSpan( text: "- $submit \n", style: textTheme.bodyMedium, ), ], ), confirmText: appLocalizations.goDownload, ); if (res != true) { return; } launchUrl( Uri.parse("https://github.com/$repository/releases/latest"), ); } else if (handleError) { globalState.showMessage( title: appLocalizations.checkUpdate, message: TextSpan( text: appLocalizations.checkUpdateError, ), ); } } _handlePreference() async { if (await preferences.isInit) { return; } final res = await globalState.showMessage( title: appLocalizations.tip, message: TextSpan(text: appLocalizations.cacheCorrupt), ); if (res == true) { final file = File(await appPath.sharedPreferencesPath); final isExists = await file.exists(); if (isExists) { await file.delete(); } } await handleExit(); } Future _initCore() async { final isInit = await clashCore.isInit; if (!isInit) { await clashCore.init(); await clashCore.setState( globalState.getCoreState(), ); } await applyProfile(); } init() async { FlutterError.onError = (details) { commonPrint.log(details.stack.toString()); }; updateTray(true); await _initCore(); await _initStatus(); autoLaunch?.updateStatus( _ref.read(appSettingProvider).autoLaunch, ); autoUpdateProfiles(); autoCheckUpdate(); if (!_ref.read(appSettingProvider).silentLaunch) { window?.show(); } else { window?.hide(); } await _handlePreference(); await _handlerDisclaimer(); _ref.read(initProvider.notifier).value = true; } _initStatus() async { if (Platform.isAndroid) { await globalState.updateStartTime(); } final status = globalState.isStart == true ? true : _ref.read(appSettingProvider).autoRun; await updateStatus(status); if (!status) { addCheckIpNumDebounce(); } } setDelay(Delay delay) { _ref.read(delayDataSourceProvider.notifier).setDelay(delay); } toPage(PageLabel pageLabel) { _ref.read(currentPageLabelProvider.notifier).value = pageLabel; } toProfiles() { toPage(PageLabel.profiles); } initLink() { linkManager.initAppLinksListen( (url) async { final res = await globalState.showMessage( title: "${appLocalizations.add}${appLocalizations.profile}", message: TextSpan( children: [ TextSpan(text: appLocalizations.doYouWantToPass), TextSpan( text: " $url ", style: TextStyle( color: Theme.of(context).colorScheme.primary, decoration: TextDecoration.underline, decorationColor: Theme.of(context).colorScheme.primary, ), ), TextSpan( text: "${appLocalizations.create}${appLocalizations.profile}"), ], ), ); if (res != true) { return; } addProfileFormURL(url); }, ); } Future showDisclaimer() async { return await globalState.showCommonDialog( dismissible: false, child: CommonDialog( title: appLocalizations.disclaimer, actions: [ TextButton( onPressed: () { Navigator.of(context).pop(false); }, child: Text(appLocalizations.exit), ), TextButton( onPressed: () { _ref.read(appSettingProvider.notifier).updateState( (state) => state.copyWith(disclaimerAccepted: true), ); Navigator.of(context).pop(true); }, child: Text(appLocalizations.agree), ) ], child: SelectableText( appLocalizations.disclaimerDesc, ), ), ) ?? false; } _handlerDisclaimer() async { if (_ref.read(appSettingProvider).disclaimerAccepted) { return; } final isDisclaimerAccepted = await showDisclaimer(); if (!isDisclaimerAccepted) { await handleExit(); } return; } addProfileFormURL(String url) async { if (globalState.navigatorKey.currentState?.canPop() ?? false) { globalState.navigatorKey.currentState?.popUntil((route) => route.isFirst); } toProfiles(); final commonScaffoldState = globalState.homeScaffoldKey.currentState; if (commonScaffoldState?.mounted != true) return; final profile = await commonScaffoldState?.loadingRun( () async { return await Profile.normal( url: url, ).update(); }, title: "${appLocalizations.add}${appLocalizations.profile}", ); if (profile != null) { await addProfile(profile); } } addProfileFormFile() async { final platformFile = await globalState.safeRun(picker.pickerFile); final bytes = platformFile?.bytes; if (bytes == null) { return null; } if (!context.mounted) return; globalState.navigatorKey.currentState?.popUntil((route) => route.isFirst); toProfiles(); final commonScaffoldState = globalState.homeScaffoldKey.currentState; if (commonScaffoldState?.mounted != true) return; final profile = await commonScaffoldState?.loadingRun( () async { await Future.delayed(const Duration(milliseconds: 300)); return await Profile.normal(label: platformFile?.name).saveFile(bytes); }, title: "${appLocalizations.add}${appLocalizations.profile}", ); if (profile != null) { await addProfile(profile); } } addProfileFormQrCode() async { final url = await globalState.safeRun(picker.pickerConfigQRCode); if (url == null) return; addProfileFormURL(url); } updateViewSize(Size size) { WidgetsBinding.instance.addPostFrameCallback((_) { _ref.read(viewSizeProvider.notifier).value = size; }); } setProvider(ExternalProvider? provider) { _ref.read(providersProvider.notifier).setProvider(provider); } List _sortOfName(List proxies) { return List.of(proxies) ..sort( (a, b) => utils.sortByChar( utils.getPinyin(a.name), utils.getPinyin(b.name), ), ); } List _sortOfDelay({ required List proxies, String? testUrl, }) { return List.of(proxies) ..sort( (a, b) { final aDelay = _ref.read(getDelayProvider( proxyName: a.name, testUrl: testUrl, )); final bDelay = _ref.read( getDelayProvider( proxyName: b.name, testUrl: testUrl, ), ); if (aDelay == null && bDelay == null) { return 0; } if (aDelay == null || aDelay == -1) { return 1; } if (bDelay == null || bDelay == -1) { return -1; } return aDelay.compareTo(bDelay); }, ); } List getSortProxies(List proxies, [String? url]) { return switch (_ref.read(proxiesStyleSettingProvider).sortType) { ProxiesSortType.none => proxies, ProxiesSortType.delay => _sortOfDelay( proxies: proxies, testUrl: url, ), ProxiesSortType.name => _sortOfName(proxies), }; } clearEffect(String profileId) async { final profilePath = await appPath.getProfilePath(profileId); final providersDirPath = await appPath.getProvidersDirPath(profileId); return await Isolate.run(() async { final profileFile = File(profilePath); final isExists = await profileFile.exists(); if (isExists) { profileFile.delete(recursive: true); } final providersFileDir = File(providersDirPath); final providersFileIsExists = await providersFileDir.exists(); if (providersFileIsExists) { providersFileDir.delete(recursive: true); } }); } updateTun() { _ref.read(patchClashConfigProvider.notifier).updateState( (state) => state.copyWith.tun(enable: !state.tun.enable), ); } updateSystemProxy() { _ref.read(networkSettingProvider.notifier).updateState( (state) => state.copyWith( systemProxy: !state.systemProxy, ), ); } Future> getPackages() async { if (_ref.read(isMobileViewProvider)) { await Future.delayed(commonDuration); } if (_ref.read(packagesProvider).isEmpty) { _ref.read(packagesProvider.notifier).value = await app?.getPackages() ?? []; } return _ref.read(packagesProvider); } updateStart() { updateStatus(!_ref.read(runTimeProvider.notifier).isStart); } updateCurrentSelectedMap(String groupName, String proxyName) { final currentProfile = _ref.read(currentProfileProvider); if (currentProfile != null && currentProfile.selectedMap[groupName] != proxyName) { final SelectedMap selectedMap = Map.from( currentProfile.selectedMap, )..[groupName] = proxyName; _ref.read(profilesProvider.notifier).setProfile( currentProfile.copyWith( selectedMap: selectedMap, ), ); } } updateCurrentUnfoldSet(Set value) { final currentProfile = _ref.read(currentProfileProvider); if (currentProfile == null) { return; } _ref.read(profilesProvider.notifier).setProfile( currentProfile.copyWith( unfoldSet: value, ), ); } changeMode(Mode mode) { _ref.read(patchClashConfigProvider.notifier).updateState( (state) => state.copyWith(mode: mode), ); if (mode == Mode.global) { updateCurrentGroupName(GroupName.GLOBAL.name); } addCheckIpNumDebounce(); } updateAutoLaunch() { _ref.read(appSettingProvider.notifier).updateState( (state) => state.copyWith( autoLaunch: !state.autoLaunch, ), ); } updateVisible() async { final visible = await window?.isVisible; if (visible != null && !visible) { window?.show(); } else { window?.hide(); } } updateMode() { _ref.read(patchClashConfigProvider.notifier).updateState( (state) { final index = Mode.values.indexWhere((item) => item == state.mode); if (index == -1) { return null; } final nextIndex = index + 1 > Mode.values.length - 1 ? 0 : index + 1; return state.copyWith( mode: Mode.values[nextIndex], ); }, ); } handleAddOrUpdate(WidgetRef ref, [Rule? rule]) async { final res = await globalState.showCommonDialog( child: AddRuleDialog( rule: rule, snippet: ref.read( profileOverrideStateProvider.select( (state) => state.snippet!, ), ), ), ); if (res == null) { return; } ref.read(profileOverrideStateProvider.notifier).updateState( (state) { final model = state.copyWith.overrideData!( rule: state.overrideData!.rule.updateRules( (rules) { final index = rules.indexWhere((item) => item.id == res.id); if (index == -1) { return List.from([res, ...rules]); } return List.from(rules)..[index] = res; }, ), ); return model; }, ); } Future exportLogs() async { final logsRaw = _ref.read(logsProvider).list.map( (item) => item.toString(), ); final data = await Isolate.run>(() async { final logsRawString = logsRaw.join("\n"); return utf8.encode(logsRawString); }); return await picker.saveFile( utils.logFile, Uint8List.fromList(data), ) != null; } Future> backupData() async { final homeDirPath = await appPath.homeDirPath; final profilesPath = await appPath.profilesPath; final configJson = globalState.config.toJson(); return Isolate.run>(() async { final archive = Archive(); archive.add("config.json", configJson); await archive.addDirectoryToArchive(profilesPath, homeDirPath); final zipEncoder = ZipEncoder(); return zipEncoder.encode(archive) ?? []; }); } updateTray([bool focus = false]) async { tray.update( trayState: _ref.read(trayStateProvider), ); } recoveryData( List data, RecoveryOption recoveryOption, ) async { final archive = await Isolate.run(() { final zipDecoder = ZipDecoder(); return zipDecoder.decodeBytes(data); }); final homeDirPath = await appPath.homeDirPath; final configs = archive.files.where((item) => item.name.endsWith(".json")).toList(); final profiles = archive.files.where((item) => !item.name.endsWith(".json")); final configIndex = configs.indexWhere((config) => config.name == "config.json"); if (configIndex == -1) throw "invalid backup file"; final configFile = configs[configIndex]; var tempConfig = Config.compatibleFromJson( json.decode( utf8.decode(configFile.content), ), ); for (final profile in profiles) { final filePath = join(homeDirPath, profile.name); final file = File(filePath); await file.create(recursive: true); await file.writeAsBytes(profile.content); } final clashConfigIndex = configs.indexWhere((config) => config.name == "clashConfig.json"); if (clashConfigIndex != -1) { final clashConfigFile = configs[clashConfigIndex]; tempConfig = tempConfig.copyWith( patchClashConfig: ClashConfig.fromJson( json.decode( utf8.decode( clashConfigFile.content, ), ), ), ); } _recovery( tempConfig, recoveryOption, ); } _recovery(Config config, RecoveryOption recoveryOption) { final recoveryStrategy = _ref.read(appSettingProvider.select( (state) => state.recoveryStrategy, )); final profiles = config.profiles; if (recoveryStrategy == RecoveryStrategy.override) { _ref.read(profilesProvider.notifier).value = profiles; } else { for (final profile in profiles) { _ref.read(profilesProvider.notifier).setProfile( profile, ); } } final onlyProfiles = recoveryOption == RecoveryOption.onlyProfiles; if (!onlyProfiles) { _ref.read(patchClashConfigProvider.notifier).value = config.patchClashConfig; _ref.read(appSettingProvider.notifier).value = config.appSetting; _ref.read(currentProfileIdProvider.notifier).value = config.currentProfileId; _ref.read(appDAVSettingProvider.notifier).value = config.dav; _ref.read(themeSettingProvider.notifier).value = config.themeProps; _ref.read(windowSettingProvider.notifier).value = config.windowProps; _ref.read(vpnSettingProvider.notifier).value = config.vpnProps; _ref.read(proxiesStyleSettingProvider.notifier).value = config.proxiesStyle; _ref.read(overrideDnsProvider.notifier).value = config.overrideDns; _ref.read(networkSettingProvider.notifier).value = config.networkProps; _ref.read(hotKeyActionsProvider.notifier).value = config.hotKeyActions; _ref.read(scriptStateProvider.notifier).value = config.scriptProps; } final currentProfile = _ref.read(currentProfileProvider); if (currentProfile == null) { _ref.read(currentProfileIdProvider.notifier).value = profiles.first.id; } } }