import 'package:flutter/material.dart'; import '../app/app_controller.dart'; import '../app/master_lock.dart'; import '../l10n/tr.dart'; import 'theme.dart'; import 'confirm_dialog.dart'; import 'master_prompt.dart'; import 'setting_tile.dart'; import 'vault_page.dart' show openVault; /// 124: 환경 설정 > 보안 - 최상 비밀번호 (모두 초기화하는 열쇠) · 마스터 비밀번호 (저장된 비밀번호 · 설정 · 비밀번호가 필요한 기능) /// · 마스터를 묻는 때. 이미 정한 것을 바꾸거나 끄려면 지금 마스터 (또는 최상) 를 넣는다. class SecuritySettings extends StatelessWidget { final AppController c; const SecuritySettings({super.key, required this.c}); @override Widget build(BuildContext context) { final lock = MasterLock.instance; if (lock == null) return const SizedBox(); return ListenableBuilder( listenable: Listenable.merge([lock, c]), builder: (context, _) { // 바꾸기 · 끄기 전에 지금 비밀번호 (마스터 또는 최상) Future current() => askMaster(context, c, lock, forChange: true, reason: tr('바꾸거나 끄려면 지금 마스터 비밀번호 (또는 최상 비밀번호) 를 넣으세요.')); // M0: 안전 저장소를 읽지 못함 - 마스터가 있는지 알 수 없어 바꾸지 못하게 (진짜 값을 덮어쓰지 않게) 하고 까닭 · [다시 읽기] if (lock.storeUnreadable) { return SettingTile( leading: Icon(Icons.lock_clock_outlined, color: JjColors.warning), title: Text(tr('마스터 비밀번호 - 안전 저장소를 읽지 못함')), subtitle: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(tr('이 기기의 안전 저장소를 읽지 못해 마스터 비밀번호가 있는지 알 수 없습니다. ' '그동안 저장된 비밀번호를 쓰는 기능은 열지 않고, 마스터 · 최상 비밀번호는 바꿀 수 없습니다.')), const SizedBox(height: 6), FilledButton.tonal(onPressed: () => lock.reload(), child: Text(tr('다시 읽기'))), ]), ); } return Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ SettingTile( leading: const Icon(Icons.lock_outline), title: Text(tr('마스터 비밀번호')), // 132: 버튼은 설명 아래에 (넓은 화면의 오른쪽 칸에서는 둘째 버튼이 잘려 안 보였다) subtitle: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(lock.hasMaster ? tr('정해 둠. 저장된 비밀번호 · 설정 · 비밀번호가 필요한 기능을 쓸 때 묻습니다 (앱을 끌 때까지 한 번)') : tr('정하지 않음. 정하면 저장된 비밀번호 (WebDAV · OpenSubtitles · API 키) 와 설정을 쓰기 전에 묻습니다')), const SizedBox(height: 6), Wrap(spacing: 6, runSpacing: 6, children: [ if (!lock.hasMaster) FilledButton.tonal( onPressed: () => setPasswordDialog(context, c, lock, superPassword: false), child: Text(tr('정하기')), ) else ...[ OutlinedButton( onPressed: () async { if (await current() && context.mounted) await setPasswordDialog(context, c, lock, superPassword: false); }, child: Text(tr('바꾸기')), ), OutlinedButton( onPressed: () async { if (!await current() || !context.mounted) return; final ok = await confirmAction(context, title: tr('마스터 비밀번호를 끌까요?'), body: tr('저장된 비밀번호 · 설정을 쓸 때 더는 묻지 않습니다.'), ok: tr('끄기')); if (!ok) return; await lock.clearMaster(); }, child: Text(tr('끄기')), ), ], ]), ]), ), SettingTile( enabled: lock.hasMaster, leading: const Icon(Icons.schedule), title: Text(tr('마스터 비밀번호를 묻는 때')), trailing: DropdownButton( value: lock.ask, items: [ DropdownMenuItem(value: 'never', child: Text(tr('물어보지 않기'))), DropdownMenuItem(value: 'startup', child: Text(tr('처음 시작 시'))), DropdownMenuItem(value: 'onUse', child: Text(tr('비밀번호가 저장된 기능을 쓸 때'))), ], // 129: 바꾸는 것도 지금 마스터를 넣어야 (자물쇠를 아무나 끄지 못하게) onChanged: !lock.hasMaster ? null : (v) async { if (v == lock.ask) return; if (await current()) await lock.setAsk(v!); }, ), ), SettingTile( leading: const Icon(Icons.key_outlined), title: Text(tr('최상 비밀번호')), subtitle: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(lock.hasSuper ? tr('정해 둠. 마스터 입력 창에 넣으면 초기화 창이 열립니다 (마스터 · 저장된 비밀번호 · 앱 설정 전체 중 고름)') : tr('정하지 않음. 마스터를 잊으면 저장된 비밀번호를 지우고 마스터를 초기화하는 것만 할 수 있습니다')), const SizedBox(height: 6), Wrap(spacing: 6, runSpacing: 6, children: [ if (!lock.hasSuper) FilledButton.tonal( onPressed: () async { // 마스터가 있으면 그것을 먼저 (아무나 최상을 정하지 못하게) if (lock.hasMaster && !await current()) return; if (context.mounted) await setPasswordDialog(context, c, lock, superPassword: true); }, child: Text(tr('정하기')), ) else ...[ OutlinedButton( onPressed: () async { if (await current() && context.mounted) await setPasswordDialog(context, c, lock, superPassword: true); }, child: Text(tr('바꾸기')), ), OutlinedButton( onPressed: () async { if (!await current() || !context.mounted) return; final ok = await confirmAction(context, title: tr('최상 비밀번호를 끌까요?'), body: '', ok: tr('끄기')); if (ok) await lock.clearSuper(); }, child: Text(tr('끄기')), ), ], ]), ]), ), // X-plore 비교 5-2 암호 보관함 (사용자 결정 10/21, 처음 끔) SettingTile( leading: const Icon(Icons.enhanced_encryption_outlined), title: Text(tr('암호 보관함')), subtitle: Text(tr('파일을 AES-256 으로 암호화해 보관합니다 (이름까지 숨김). 보관함 비밀번호를 잊으면 되살릴 수 없습니다')), trailing: Switch(value: c.settings.vault.enabled, onChanged: (v) => c.updateSettings((x) => x.vault.enabled = v)), ), if (c.settings.vault.enabled) ...[ ListTile( contentPadding: const EdgeInsets.only(left: 56, right: 16), title: Text(tr('보관함 열기')), subtitle: Text(tr('넣기는 파일 탐색기에서 파일 · 폴더를 길게 눌러 [보관함에 넣기]')), trailing: const Icon(Icons.chevron_right), onTap: () => openVault(context, c), ), SettingTile( title: Text(tr('보관함 자동 잠금')), subtitle: Text(tr('앱이 뒤로 간 뒤 이만큼 지나 돌아오면 다시 비밀번호를 묻습니다')), trailing: DropdownButton( value: c.settings.vault.autoLockSeconds, items: [ for (final v in {0, 30, 60, 300, 900, c.settings.vault.autoLockSeconds}.toList()..sort()) DropdownMenuItem(value: v, child: Text(v == 0 ? tr('바로') : v == 60 ? tr('1분 (기본)') : v < 60 ? trf('{0}초', [v]) : trf('{0}분', [v ~/ 60]))), ], onChanged: (v) => c.updateSettings((x) => x.vault.autoLockSeconds = v!), ), ), ], ]); }, ); } }