WIP M5 1묶음 (정지 - 사용자 지시 "M 시리즈는 모두 정지"): 경고 표시 · 브라우저 상태 → AppSession (c.session)
- 정지 때 상태: analyze 0, root · shizuku · browser_page 시험 16 개 2회 통과, 전체 확인 (step_check) 은 도는 중에 끊음 (끝나지 않음) - modular 에는 넣지 않음 (관리자 지시) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>m5_wip
parent
e82c34a0b8
commit
f2ad0133b6
|
|
@ -14,6 +14,7 @@ import '../services/subtitle_provider.dart';
|
||||||
import '../services/media_tool.dart';
|
import '../services/media_tool.dart';
|
||||||
import '../core/shizuku.dart' show PrivilegedFs;
|
import '../core/shizuku.dart' show PrivilegedFs;
|
||||||
import '../services/platform_services.dart';
|
import '../services/platform_services.dart';
|
||||||
|
import 'app_session.dart';
|
||||||
import '../services/system_usage.dart';
|
import '../services/system_usage.dart';
|
||||||
import 'settings.dart';
|
import 'settings.dart';
|
||||||
import 'cleanup.dart' show removeUpdateLeftovers;
|
import 'cleanup.dart' show removeUpdateLeftovers;
|
||||||
|
|
@ -60,6 +61,9 @@ class AppController extends ChangeNotifier {
|
||||||
// ───────── 모듈 ─────────
|
// ───────── 모듈 ─────────
|
||||||
|
|
||||||
late final _logBook = AppLog(now: services.clock);
|
late final _logBook = AppLog(now: services.clock);
|
||||||
|
|
||||||
|
/// 앱을 켜 둔 동안만 기억하는 화면 상태 (설계 M5 - 예전 화면 static)
|
||||||
|
final session = AppSession();
|
||||||
late final _jobs = JobQueue(onChanged: notifyListeners, log: _log);
|
late final _jobs = JobQueue(onChanged: notifyListeners, log: _log);
|
||||||
late final _files = SubtitleFiles(services.storage, _tool);
|
late final _files = SubtitleFiles(services.storage, _tool);
|
||||||
late final _probe = VideoProbe(storage: services.storage, tool: _tool, files: _files, log: _log, fileAccess: () => fileAccess);
|
late final _probe = VideoProbe(storage: services.storage, tool: _tool, files: _files, log: _log, fileAccess: () => fileAccess);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
/// 약속 (설계 M5 AppSession): 앱을 켜 둔 동안만 기억하는 화면 상태 - 저장하지 않고, 앱 (AppController) 하나에 하나.
|
||||||
|
/// 예전에는 화면 클래스의 static 이라 앱 전체가 하나를 함께 썼고, 시험은 그것을 손으로 되돌려야 했다 (새 AppController 면 새로 시작).
|
||||||
|
/// 입력 · 출력: 값을 읽고 쓰기만 한다. 오류 없음
|
||||||
|
library;
|
||||||
|
|
||||||
|
class AppSession {
|
||||||
|
/// 시스템 폴더 (Shizuku) · 루트 경고를 이번 실행에서 보였는지 (관리자 결정 10/21: 앱을 켠 뒤 처음 열 때 한 번)
|
||||||
|
bool shizukuWarned = false, rootWarned = false;
|
||||||
|
|
||||||
|
/// 브라우저: 마지막으로 본 주소 (다시 열 때 그 주소로)
|
||||||
|
String? browserLastUrl;
|
||||||
|
|
||||||
|
/// 브라우저 오른쪽 작업 현황 (화면 분할) 의 열림 (null = 아직 정하지 않음 - 작업 중이면 열림) · 너비
|
||||||
|
bool? browserWorkOpen;
|
||||||
|
double browserWorkWidth = 360;
|
||||||
|
}
|
||||||
|
|
@ -59,9 +59,6 @@ class BrowserPage extends StatefulWidget {
|
||||||
/// 열려 있는 브라우저 화면 수
|
/// 열려 있는 브라우저 화면 수
|
||||||
static int _open = 0;
|
static int _open = 0;
|
||||||
|
|
||||||
/// 마지막으로 보던 주소: MKV 화면으로 갔다가 브라우저를 다시 열면 홈이 아니라 이 주소로
|
|
||||||
static String? _lastUrl;
|
|
||||||
|
|
||||||
/// 마지막으로 붙은 웹뷰 (통합 테스트에서 페이지 내용을 읽는다)
|
/// 마지막으로 붙은 웹뷰 (통합 테스트에서 페이지 내용을 읽는다)
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
static WebNav? debugNav;
|
static WebNav? debugNav;
|
||||||
|
|
@ -89,7 +86,7 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
|
||||||
/// 207: Chrome 엔진 [다시 시도] 때마다 새로
|
/// 207: Chrome 엔진 [다시 시도] 때마다 새로
|
||||||
int _cefTry = 0;
|
int _cefTry = 0;
|
||||||
|
|
||||||
late String _url = widget.initialUrl ?? BrowserPage._lastUrl ?? widget.c.settings.browser.homeUrl;
|
late String _url = widget.initialUrl ?? widget.c.session.browserLastUrl ?? widget.c.settings.browser.homeUrl;
|
||||||
String _title = '';
|
String _title = '';
|
||||||
double _progress = 1;
|
double _progress = 1;
|
||||||
|
|
||||||
|
|
@ -132,17 +129,15 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
|
||||||
bool _hasVideo = false, _canBack = false, _canFwd = false, _panel = false;
|
bool _hasVideo = false, _canBack = false, _canFwd = false, _panel = false;
|
||||||
WebNav? _nav;
|
WebNav? _nav;
|
||||||
|
|
||||||
/// 오른쪽 작업 현황 (화면 분할). 앱을 켜 둔 동안은 열림 여부 · 너비를 기억.
|
/// 오른쪽 작업 현황 (화면 분할). 앱을 켜 둔 동안은 열림 여부 · 너비를 기억 (c.session - 설계 M5)
|
||||||
static bool? _workOpen;
|
late bool _work = widget.c.session.browserWorkOpen ?? widget.c.busy;
|
||||||
static double _workWidth = 360;
|
void _toggleWork() => setState(() => widget.c.session.browserWorkOpen = _work = !_work);
|
||||||
late bool _work = _workOpen ?? widget.c.busy;
|
|
||||||
void _toggleWork() => setState(() => _workOpen = _work = !_work);
|
|
||||||
late final _address = TextEditingController(text: _url);
|
late final _address = TextEditingController(text: _url);
|
||||||
final _addressFocus = FocusNode();
|
final _addressFocus = FocusNode();
|
||||||
late final BrowserHost _host = BrowserHost(
|
late final BrowserHost _host = BrowserHost(
|
||||||
onUrl: (u) {
|
onUrl: (u) {
|
||||||
if (!mounted || u.isEmpty) return;
|
if (!mounted || u.isEmpty) return;
|
||||||
BrowserPage._lastUrl = u;
|
widget.c.session.browserLastUrl = u;
|
||||||
if (u != _url) _tr.resetFails(); // 다른 페이지: 번역을 다시 시도
|
if (u != _url) _tr.resetFails(); // 다른 페이지: 번역을 다시 시도
|
||||||
setState(() => _url = u);
|
setState(() => _url = u);
|
||||||
if (!_addressFocus.hasFocus) _address.text = u;
|
if (!_addressFocus.hasFocus) _address.text = u;
|
||||||
|
|
@ -431,10 +426,10 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
|
||||||
if (_work) ...[
|
if (_work) ...[
|
||||||
SplitHandle(onDrag: (dx) => setState(() {
|
SplitHandle(onDrag: (dx) => setState(() {
|
||||||
final max = MediaQuery.sizeOf(context).width - 400;
|
final max = MediaQuery.sizeOf(context).width - 400;
|
||||||
_workWidth = (_workWidth - dx).clamp(260.0, max < 260 ? 260.0 : max);
|
widget.c.session.browserWorkWidth = (widget.c.session.browserWorkWidth - dx).clamp(260.0, max < 260 ? 260.0 : max);
|
||||||
})),
|
})),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: _workWidth,
|
width: widget.c.session.browserWorkWidth,
|
||||||
child: WorkPanel(
|
child: WorkPanel(
|
||||||
c: widget.c,
|
c: widget.c,
|
||||||
downloads: widget.downloads,
|
downloads: widget.downloads,
|
||||||
|
|
@ -480,4 +475,3 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,7 @@ class ExplorerAccess extends ChangeNotifier {
|
||||||
/// 드라이브를 열 수 있는지 (화면을 멈추지 않는 비동기)
|
/// 드라이브를 열 수 있는지 (화면을 멈추지 않는 비동기)
|
||||||
final Future<bool> Function(String path) probe;
|
final Future<bool> Function(String path) probe;
|
||||||
|
|
||||||
/// 이번 실행에서 경고를 보였는지 (관리자 결정 10/21: 앱을 켠 뒤 처음 열 때 한 번)
|
// 이번 실행에서 경고를 보였는지는 ctl.c.session (설계 M5 AppSession - 예전 static)
|
||||||
static bool shizukuWarned = false, rootWarned = false;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
|
@ -84,7 +83,7 @@ class ExplorerAccess extends ChangeNotifier {
|
||||||
|
|
||||||
/// X-plore 비교 5-2: 루트 탭 - 이번 실행에서 처음이면 경고, su 가 있고 허락을 받았을 때만 "/" 로 (관리자 3a 결정)
|
/// X-plore 비교 5-2: 루트 탭 - 이번 실행에서 처음이면 경고, su 가 있고 허락을 받았을 때만 "/" 로 (관리자 3a 결정)
|
||||||
Future<void> openRoot(BuildContext context, ExplorerPane pane) async {
|
Future<void> openRoot(BuildContext context, ExplorerPane pane) async {
|
||||||
if (!rootWarned) {
|
if (!ctl.c.session.rootWarned) {
|
||||||
// 공용 확인 창: 첫 초점 [취소] (Enter 로 바로 열리지 않음)
|
// 공용 확인 창: 첫 초점 [취소] (Enter 로 바로 열리지 않음)
|
||||||
final ok = await confirmDialog(
|
final ok = await confirmDialog(
|
||||||
context,
|
context,
|
||||||
|
|
@ -96,7 +95,7 @@ class ExplorerAccess extends ChangeNotifier {
|
||||||
cancelFirst: true,
|
cancelFirst: true,
|
||||||
);
|
);
|
||||||
if (!ok || !context.mounted) return;
|
if (!ok || !context.mounted) return;
|
||||||
rootWarned = true;
|
ctl.c.session.rootWarned = true;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
final st = await PrivilegedFs.current.root.status();
|
final st = await PrivilegedFs.current.root.status();
|
||||||
|
|
@ -121,7 +120,7 @@ class ExplorerAccess extends ChangeNotifier {
|
||||||
|
|
||||||
/// X-plore 비교 5-2: 시스템 폴더 탭 - 이번 실행에서 처음이면 경고, Shizuku 가 켜져 있고 허락을 받았을 때만 Android 폴더로
|
/// X-plore 비교 5-2: 시스템 폴더 탭 - 이번 실행에서 처음이면 경고, Shizuku 가 켜져 있고 허락을 받았을 때만 Android 폴더로
|
||||||
Future<void> openShizuku(BuildContext context, ExplorerPane pane) async {
|
Future<void> openShizuku(BuildContext context, ExplorerPane pane) async {
|
||||||
if (!shizukuWarned) {
|
if (!ctl.c.session.shizukuWarned) {
|
||||||
final ok = await showDialog<bool>(
|
final ok = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
|
|
@ -137,7 +136,7 @@ class ExplorerAccess extends ChangeNotifier {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (ok != true || !context.mounted) return;
|
if (ok != true || !context.mounted) return;
|
||||||
shizukuWarned = true;
|
ctl.c.session.shizukuWarned = true;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
final st = await PrivilegedFs.current.shizuku.status();
|
final st = await PrivilegedFs.current.shizuku.status();
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,6 @@ export '../core/name_clash.dart' show NameClash, clashCompareText, fileDateText;
|
||||||
/// 202 · 221: 창 위 줄을 [← → ★] · 탭 · [WebDAV · 경로 입력] 으로 펴는 가장 작은 창 폭. 이보다 좁으면 끝 버튼은 [⋮] 하나 -
|
/// 202 · 221: 창 위 줄을 [← → ★] · 탭 · [WebDAV · 경로 입력] 으로 펴는 가장 작은 창 폭. 이보다 좁으면 끝 버튼은 [⋮] 하나 -
|
||||||
/// 폰 세로 (약 400) 에서 탭 칸이 130px 남짓이라 두 번째 저장소 (SD 카드) 가 잘려 보였다
|
/// 폰 세로 (약 400) 에서 탭 칸이 130px 남짓이라 두 번째 저장소 (SD 카드) 가 잘려 보였다
|
||||||
|
|
||||||
|
|
||||||
class ExplorerPage extends StatefulWidget {
|
class ExplorerPage extends StatefulWidget {
|
||||||
final AppController c;
|
final AppController c;
|
||||||
final bool rsync;
|
final bool rsync;
|
||||||
|
|
@ -67,12 +66,6 @@ class ExplorerPage extends StatefulWidget {
|
||||||
const ExplorerPage(
|
const ExplorerPage(
|
||||||
{super.key, required this.c, this.rsync = false, this.clock = DateTime.now, this.device = const RealExplorerDevice()});
|
{super.key, required this.c, this.rsync = false, this.clock = DateTime.now, this.device = const RealExplorerDevice()});
|
||||||
|
|
||||||
/// 이번 실행에서 시스템 폴더 · 루트 경고를 보였는지 ([ExplorerAccess])
|
|
||||||
static bool get shizukuWarnedThisRun => ExplorerAccess.shizukuWarned;
|
|
||||||
static set shizukuWarnedThisRun(bool v) => ExplorerAccess.shizukuWarned = v;
|
|
||||||
static bool get rootWarnedThisRun => ExplorerAccess.rootWarned;
|
|
||||||
static set rootWarnedThisRun(bool v) => ExplorerAccess.rootWarned = v;
|
|
||||||
|
|
||||||
static const routeName = 'explorer';
|
static const routeName = 'explorer';
|
||||||
static const rsyncRouteName = 'rsync';
|
static const rsyncRouteName = 'rsync';
|
||||||
static int _open = 0, _openRsync = 0;
|
static int _open = 0, _openRsync = 0;
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,6 @@ void main() {
|
||||||
SuRoot.forgetGrant();
|
SuRoot.forgetGrant();
|
||||||
// 설계 M3: 실제 SuRoot 에 가짜 su 프로세스만 넘기고, PrivilegedFs 하나로 건다 (Shizuku 쪽은 그대로)
|
// 설계 M3: 실제 SuRoot 에 가짜 su 프로세스만 넘기고, PrivilegedFs 하나로 건다 (Shizuku 쪽은 그대로)
|
||||||
PrivilegedFs.use(PrivilegedFs(shizuku: was.shizuku, root: SuRoot(start: su.start), tempDir: () async => tmp.path));
|
PrivilegedFs.use(PrivilegedFs(shizuku: was.shizuku, root: SuRoot(start: su.start), tempDir: () async => tmp.path));
|
||||||
ExplorerPage.rootWarnedThisRun = false;
|
|
||||||
});
|
});
|
||||||
tearDown(() {
|
tearDown(() {
|
||||||
PrivilegedFs.use(was);
|
PrivilegedFs.use(was);
|
||||||
|
|
@ -174,9 +173,10 @@ void main() {
|
||||||
t.view.devicePixelRatio = 1;
|
t.view.devicePixelRatio = 1;
|
||||||
addTearDown(t.view.reset);
|
addTearDown(t.view.reset);
|
||||||
device.volumes = () => [('/storage/emulated/0', 'T:')];
|
device.volumes = () => [('/storage/emulated/0', 'T:')];
|
||||||
ExplorerPage.rootWarnedThisRun = true;
|
|
||||||
su.hasSu = false;
|
su.hasSu = false;
|
||||||
await t.runAsync(() => t.pumpWidget(MaterialApp(home: ExplorerPage(key: UniqueKey(), device: device, c: ctl()))));
|
// 경고는 이번 실행에서 이미 보였음 (설계 M5 - 앱 세션 c.session)
|
||||||
|
final c = ctl()..session.rootWarned = true;
|
||||||
|
await t.runAsync(() => t.pumpWidget(MaterialApp(home: ExplorerPage(key: UniqueKey(), device: device, c: c))));
|
||||||
await wait(t, () => find.text('루트 (읽기 전용)').evaluate().isNotEmpty);
|
await wait(t, () => find.text('루트 (읽기 전용)').evaluate().isNotEmpty);
|
||||||
await t.tap(find.text('루트 (읽기 전용)').first);
|
await t.tap(find.text('루트 (읽기 전용)').first);
|
||||||
await wait(t, () => find.textContaining('su 가 없습니다').evaluate().isNotEmpty);
|
await wait(t, () => find.textContaining('su 가 없습니다').evaluate().isNotEmpty);
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,6 @@ void main() {
|
||||||
fake = _Fake();
|
fake = _Fake();
|
||||||
// 설계 M3: 가짜 Shizuku 뒤쪽 · 시험 임시 폴더를 PrivilegedFs 하나로 건다 (루트 쪽은 그대로)
|
// 설계 M3: 가짜 Shizuku 뒤쪽 · 시험 임시 폴더를 PrivilegedFs 하나로 건다 (루트 쪽은 그대로)
|
||||||
PrivilegedFs.use(PrivilegedFs(shizuku: fake, root: was.root, tempDir: () async => tmp.path));
|
PrivilegedFs.use(PrivilegedFs(shizuku: fake, root: was.root, tempDir: () async => tmp.path));
|
||||||
ExplorerPage.shizukuWarnedThisRun = false;
|
|
||||||
});
|
});
|
||||||
tearDown(() {
|
tearDown(() {
|
||||||
PrivilegedFs.use(was);
|
PrivilegedFs.use(was);
|
||||||
|
|
@ -145,8 +144,9 @@ void main() {
|
||||||
await t.runAsync(() => t.pumpWidget(MaterialApp(home: ExplorerPage(key: UniqueKey(), device: device, c: ctl(on: false)))));
|
await t.runAsync(() => t.pumpWidget(MaterialApp(home: ExplorerPage(key: UniqueKey(), device: device, c: ctl(on: false)))));
|
||||||
await wait(t, () => false);
|
await wait(t, () => false);
|
||||||
expect(find.text('시스템 (읽기 전용)'), findsNothing);
|
expect(find.text('시스템 (읽기 전용)'), findsNothing);
|
||||||
// 켬
|
// 켬 (같은 앱 - 아래 "다시 열기" 도 이 c: 경고 기억은 앱 세션 c.session, 설계 M5)
|
||||||
await t.runAsync(() => t.pumpWidget(MaterialApp(home: ExplorerPage(key: UniqueKey(), device: device, c: ctl()))));
|
final c = ctl();
|
||||||
|
await t.runAsync(() => t.pumpWidget(MaterialApp(home: ExplorerPage(key: UniqueKey(), device: device, c: c))));
|
||||||
await wait(t, () => find.text('시스템 (읽기 전용)').evaluate().isNotEmpty);
|
await wait(t, () => find.text('시스템 (읽기 전용)').evaluate().isNotEmpty);
|
||||||
await t.tap(find.text('시스템 (읽기 전용)').first);
|
await t.tap(find.text('시스템 (읽기 전용)').first);
|
||||||
await wait(t, () => find.text('알고 봅니다').evaluate().isNotEmpty);
|
await wait(t, () => find.text('알고 봅니다').evaluate().isNotEmpty);
|
||||||
|
|
@ -174,9 +174,9 @@ void main() {
|
||||||
expect(find.text('읽기 전용이라 옮길 수 없습니다 - 복사를 쓰세요'), findsOneWidget);
|
expect(find.text('읽기 전용이라 옮길 수 없습니다 - 복사를 쓰세요'), findsOneWidget);
|
||||||
expect(find.textContaining('담았습니다'), findsNothing, reason: '이동으로 담지도 않음');
|
expect(find.textContaining('담았습니다'), findsNothing, reason: '이동으로 담지도 않음');
|
||||||
expect(fake.calls.where((x) => x.startsWith('copy:')), isEmpty);
|
expect(fake.calls.where((x) => x.startsWith('copy:')), isEmpty);
|
||||||
// 다시 열기: 경고 없이 (이번 실행에서 이미 봄)
|
// 다시 열기: 경고 없이 (이번 실행 - 같은 앱 - 에서 이미 봄)
|
||||||
await t.pumpWidget(const SizedBox());
|
await t.pumpWidget(const SizedBox());
|
||||||
await t.runAsync(() => t.pumpWidget(MaterialApp(home: ExplorerPage(key: UniqueKey(), device: device, c: ctl()))));
|
await t.runAsync(() => t.pumpWidget(MaterialApp(home: ExplorerPage(key: UniqueKey(), device: device, c: c))));
|
||||||
await wait(t, () => find.text('시스템 (읽기 전용)').evaluate().isNotEmpty);
|
await wait(t, () => find.text('시스템 (읽기 전용)').evaluate().isNotEmpty);
|
||||||
await t.tap(find.text('시스템 (읽기 전용)').first);
|
await t.tap(find.text('시스템 (읽기 전용)').first);
|
||||||
await wait(t, () => find.text('obb').evaluate().isNotEmpty);
|
await wait(t, () => find.text('obb').evaluate().isNotEmpty);
|
||||||
|
|
@ -190,9 +190,10 @@ void main() {
|
||||||
t.view.devicePixelRatio = 1;
|
t.view.devicePixelRatio = 1;
|
||||||
addTearDown(t.view.reset);
|
addTearDown(t.view.reset);
|
||||||
device.volumes = () => [(tmp.path, 'T:')];
|
device.volumes = () => [(tmp.path, 'T:')];
|
||||||
ExplorerPage.shizukuWarnedThisRun = true;
|
|
||||||
fake.running = false;
|
fake.running = false;
|
||||||
await t.runAsync(() => t.pumpWidget(MaterialApp(home: ExplorerPage(key: UniqueKey(), device: device, c: ctl()))));
|
// 경고는 이번 실행에서 이미 보였음 (설계 M5 - 앱 세션 c.session)
|
||||||
|
final c = ctl()..session.shizukuWarned = true;
|
||||||
|
await t.runAsync(() => t.pumpWidget(MaterialApp(home: ExplorerPage(key: UniqueKey(), device: device, c: c))));
|
||||||
await wait(t, () => find.text('시스템 (읽기 전용)').evaluate().isNotEmpty);
|
await wait(t, () => find.text('시스템 (읽기 전용)').evaluate().isNotEmpty);
|
||||||
await t.tap(find.text('시스템 (읽기 전용)').first);
|
await t.tap(find.text('시스템 (읽기 전용)').first);
|
||||||
await wait(t, () => find.textContaining('Shizuku 가 켜져 있지 않습니다').evaluate().isNotEmpty);
|
await wait(t, () => find.textContaining('Shizuku 가 켜져 있지 않습니다').evaluate().isNotEmpty);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue