Android 브라우저 점검 수정: 뒤로 키 · 알림 · 백그라운드 복귀 화면
- Android 뒤로 키: 앞 웹 페이지가 있으면 웹 페이지 뒤로 (위쪽 ← 화면 이동은 그대로 브라우저 닫기) - 실행 취소 · 목록 보기 · 허용 알림이 사라지지 않던 것 (Flutter 3.47 부터 action 이 있으면 기본 persist) → persist: false - 웹뷰를 텍스처로 그림 (useHybridComposition: false): 백그라운드로 실행 중 앱을 밀었다가 다시 열면 Impeller "EGL Bad Access" 로 앱 화면이 비던 것 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>묶음3
parent
fa0b7e56b2
commit
967b729e50
|
|
@ -417,7 +417,8 @@ Future<void> runAndroid(String dataDir) async {
|
||||||
downloads.log = controller.note;
|
downloads.log = controller.note;
|
||||||
// 진행 중인 일이 있으면 화면에서 내려가도 계속 (알림에 진행 상황)
|
// 진행 중인 일이 있으면 화면에서 내려가도 계속 (알림에 진행 상황)
|
||||||
AndroidKeepAlive(controller, downloads);
|
AndroidKeepAlive(controller, downloads);
|
||||||
// 백그라운드로 실행: 화면을 닫았다가 다시 열면 새 화면 (Activity) 이 살아 있던 엔진에 붙는다 → 화면 방향을 다시 적용
|
// 백그라운드로 실행: 화면 (Activity) 을 닫아도 엔진은 살아 있다가 다시 열면 새 화면에 붙는다 → 화면 방향을 다시 적용
|
||||||
|
// (웹뷰는 텍스처로 그려 새 화면에 붙어도 그대로 보인다: browser_page _webSettings)
|
||||||
AppLifecycleListener(onResume: () => applyScreenOrientation(controller.settings.screenOrientation));
|
AppLifecycleListener(onResume: () => applyScreenOrientation(controller.settings.screenOrientation));
|
||||||
// 동영상 목록 기억 (앱을 껐다 켜도 그대로, 없어진 파일은 뺀다) - Windows 와 같은 파일
|
// 동영상 목록 기억 (앱을 껐다 켜도 그대로, 없어진 파일은 뺀다) - Windows 와 같은 파일
|
||||||
unawaited(controller.shareVideoList(p.join(dataDir, 'videos.json')));
|
unawaited(controller.shareVideoList(p.join(dataDir, 'videos.json')));
|
||||||
|
|
@ -468,6 +469,7 @@ Future<void> runAndroid(String dataDir) async {
|
||||||
messengerKey.currentState?.showSnackBar(SnackBar(
|
messengerKey.currentState?.showSnackBar(SnackBar(
|
||||||
duration: const Duration(seconds: 20),
|
duration: const Duration(seconds: 20),
|
||||||
content: Text(tr('동영상을 고르고 MKV 를 만들려면 "모든 파일에 대한 접근" 권한이 필요합니다.')),
|
content: Text(tr('동영상을 고르고 MKV 를 만들려면 "모든 파일에 대한 접근" 권한이 필요합니다.')),
|
||||||
|
persist: false, // Flutter 3.47+: [action] 이 있으면 기본은 안 사라짐 → duration 대로 닫기
|
||||||
action: SnackBarAction(label: tr('허용'), onPressed: AndroidAccess.request),
|
action: SnackBarAction(label: tr('허용'), onPressed: AndroidAccess.request),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,9 @@ class AppNavButtons extends StatelessWidget {
|
||||||
scope == null || onExplorerPage ? null : () => ExplorerPage.open(Navigator.of(context), c: scope.controller),
|
scope == null || onExplorerPage ? null : () => ExplorerPage.open(Navigator.of(context), c: scope.controller),
|
||||||
here: onExplorerPage,
|
here: onExplorerPage,
|
||||||
),
|
),
|
||||||
btn(const Icon(Icons.arrow_back), tr('뒤로'), atRoot ? null : () => Navigator.maybePop(context)),
|
btn(const Icon(Icons.arrow_back), tr('뒤로'),
|
||||||
|
// 브라우저 화면: 뒤로 키는 웹 페이지 뒤로 (PopScope) 이지만 이 버튼은 화면 이동이라 바로 닫는다
|
||||||
|
atRoot ? null : () => onBrowserPage ? Navigator.pop(context) : Navigator.maybePop(context)),
|
||||||
btn(
|
btn(
|
||||||
Icon(Icons.download_for_offline_outlined, color: onDownloadsPage ? JjColors.accent : null),
|
Icon(Icons.download_for_offline_outlined, color: onDownloadsPage ? JjColors.accent : null),
|
||||||
onDownloadsPage ? tr('다운로드 목록 (지금 여기)') : tr('다운로드 목록'),
|
onDownloadsPage ? tr('다운로드 목록 (지금 여기)') : tr('다운로드 목록'),
|
||||||
|
|
|
||||||
|
|
@ -239,6 +239,7 @@ void removeBookmarkWithUndo(BuildContext context, BookmarksController bm, Bookma
|
||||||
..clearSnackBars()
|
..clearSnackBars()
|
||||||
..showSnackBar(SnackBar(
|
..showSnackBar(SnackBar(
|
||||||
content: Text(n.isFolder ? trf('폴더 삭제: {0} (안의 {1}개 포함)', [n.title, bm.tree.countLinks(n)]) : trf('삭제: {0}', [n.title])),
|
content: Text(n.isFolder ? trf('폴더 삭제: {0} (안의 {1}개 포함)', [n.title, bm.tree.countLinks(n)]) : trf('삭제: {0}', [n.title])),
|
||||||
|
persist: false, // Flutter 3.47+: [action] 이 있으면 기본은 안 사라짐 → duration 대로 닫기
|
||||||
action: SnackBarAction(label: tr('실행 취소'), onPressed: bm.undoRemove),
|
action: SnackBarAction(label: tr('실행 취소'), onPressed: bm.undoRemove),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||||
|
|
@ -355,6 +356,7 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
|
||||||
content: Text(trf('다운로드 추가: {0}', [_title.isEmpty ? _url : _title])),
|
content: Text(trf('다운로드 추가: {0}', [_title.isEmpty ? _url : _title])),
|
||||||
duration: const Duration(minutes: 10),
|
duration: const Duration(minutes: 10),
|
||||||
showCloseIcon: true, // [✕] 로 바로 닫기
|
showCloseIcon: true, // [✕] 로 바로 닫기
|
||||||
|
persist: false, // Flutter 3.47+: [action] 이 있으면 기본은 안 사라짐 → duration 대로 닫기
|
||||||
action: SnackBarAction(label: tr('목록 보기'), onPressed: () => DownloadsPage.open(nav, d)),
|
action: SnackBarAction(label: tr('목록 보기'), onPressed: () => DownloadsPage.open(nav, d)),
|
||||||
));
|
));
|
||||||
// 받기 준비가 끝나면 (진행률이 나오거나 · 끝 · 실패 · 취소 · 목록에서 지움) 알림을 닫는다
|
// 받기 준비가 끝나면 (진행률이 나오거나 · 끝 · 실패 · 취소 · 목록에서 지움) 알림을 닫는다
|
||||||
|
|
@ -420,7 +422,14 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
|
||||||
const SingleActivator(LogicalKeyboardKey.arrowLeft, alt: true): () => _nav?.back(),
|
const SingleActivator(LogicalKeyboardKey.arrowLeft, alt: true): () => _nav?.back(),
|
||||||
const SingleActivator(LogicalKeyboardKey.arrowRight, alt: true): () => _nav?.forward(),
|
const SingleActivator(LogicalKeyboardKey.arrowRight, alt: true): () => _nav?.forward(),
|
||||||
},
|
},
|
||||||
child: Focus(
|
// Android 뒤로 키: 앞 웹 페이지가 있으면 웹 페이지 뒤로 (없으면 브라우저 화면을 닫는다).
|
||||||
|
// 앱 위쪽의 ← (화면 이동) 은 이것과 상관없이 브라우저 화면을 닫는다 (AppNavButtons.onBrowserPage).
|
||||||
|
child: PopScope(
|
||||||
|
canPop: !(defaultTargetPlatform == TargetPlatform.android && _canBack),
|
||||||
|
onPopInvokedWithResult: (didPop, _) {
|
||||||
|
if (!didPop) _nav?.back();
|
||||||
|
},
|
||||||
|
child: Focus(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: ListenableBuilder(
|
body: ListenableBuilder(
|
||||||
|
|
@ -495,6 +504,7 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -609,6 +619,9 @@ InAppWebViewSettings _webSettings({required bool javaScript}) => InAppWebViewSet
|
||||||
javaScriptEnabled: javaScript,
|
javaScriptEnabled: javaScript,
|
||||||
mediaPlaybackRequiresUserGesture: false,
|
mediaPlaybackRequiresUserGesture: false,
|
||||||
supportMultipleWindows: false,
|
supportMultipleWindows: false,
|
||||||
|
// Android: 텍스처로 그린다. 기본 hybrid composition 은 백그라운드로 실행 중 화면 (Activity) 이 새로 붙을 때
|
||||||
|
// Impeller 가 "EGL Bad Access" 로 그리지 못해 앱 화면이 비어 버린다.
|
||||||
|
useHybridComposition: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
class _EdgeView extends StatefulWidget {
|
class _EdgeView extends StatefulWidget {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:jj_mkvmaker/app/app_controller.dart';
|
import 'package:jj_mkvmaker/app/app_controller.dart';
|
||||||
|
|
@ -50,8 +51,9 @@ class _Nav implements WebNav {
|
||||||
final loads = <String>[];
|
final loads = <String>[];
|
||||||
@override
|
@override
|
||||||
Future<void> load(String url) async => loads.add(url);
|
Future<void> load(String url) async => loads.add(url);
|
||||||
|
int backs = 0;
|
||||||
@override
|
@override
|
||||||
Future<void> back() async {}
|
Future<void> back() async => backs++;
|
||||||
@override
|
@override
|
||||||
Future<void> forward() async {}
|
Future<void> forward() async {}
|
||||||
@override
|
@override
|
||||||
|
|
@ -290,6 +292,60 @@ void main() {
|
||||||
d.dispose();
|
d.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('Android 뒤로 키: 앞 웹 페이지가 있으면 웹 뒤로, 없으면 화면 닫기 · 위쪽 ← 는 늘 닫기', (tester) async {
|
||||||
|
tester.view.physicalSize = const Size(1500, 900);
|
||||||
|
tester.view.devicePixelRatio = 1;
|
||||||
|
addTearDown(tester.view.reset);
|
||||||
|
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||||
|
final dir = Directory.systemTemp.createTempSync('jj_back_');
|
||||||
|
addTearDown(() => dir.deleteSync(recursive: true));
|
||||||
|
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||||
|
final bm = BookmarksController(p.join(dir.path, 'bookmarks.json'));
|
||||||
|
final navKey = GlobalKey<NavigatorState>();
|
||||||
|
late BrowserHost host;
|
||||||
|
final nav = _Nav();
|
||||||
|
Future<void> open() async {
|
||||||
|
navKey.currentState!.push(MaterialPageRoute<void>(
|
||||||
|
builder: (_) => BrowserPage(
|
||||||
|
c: c,
|
||||||
|
bookmarks: bm,
|
||||||
|
initialUrl: 'https://example.com/',
|
||||||
|
viewBuilder: (h, url) {
|
||||||
|
host = h;
|
||||||
|
h.attach(nav);
|
||||||
|
return const ColoredBox(color: Colors.white);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
}
|
||||||
|
|
||||||
|
await tester.pumpWidget(MaterialApp(navigatorKey: navKey, home: const Scaffold(body: Text('MKV 화면'))));
|
||||||
|
await open();
|
||||||
|
host.onHistory(true, false);
|
||||||
|
await tester.pump();
|
||||||
|
await tester.binding.handlePopRoute(); // 기기의 뒤로 키
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(nav.backs, 1);
|
||||||
|
expect(find.byType(BrowserPage), findsOneWidget);
|
||||||
|
host.onHistory(false, true);
|
||||||
|
await tester.pump();
|
||||||
|
await tester.binding.handlePopRoute();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(nav.backs, 1);
|
||||||
|
expect(find.byType(BrowserPage), findsNothing);
|
||||||
|
|
||||||
|
// 위쪽 ← (화면 이동) 은 웹 기록이 있어도 브라우저 화면을 닫는다
|
||||||
|
await open();
|
||||||
|
host.onHistory(true, false);
|
||||||
|
await tester.pump();
|
||||||
|
await tester.tap(find.byTooltip('뒤로'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(nav.backs, 1);
|
||||||
|
expect(find.byType(BrowserPage), findsNothing);
|
||||||
|
debugDefaultTargetPlatformOverride = null; // 끝나기 전에 되돌려야 한다 (테스트 검사)
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('브라우저: ☆ 추가 · 표시줄 · 다운로드 버튼 · 관리 패널 (삭제 · 실행 취소)', (tester) async {
|
testWidgets('브라우저: ☆ 추가 · 표시줄 · 다운로드 버튼 · 관리 패널 (삭제 · 실행 취소)', (tester) async {
|
||||||
tester.view.physicalSize = const Size(1500, 900);
|
tester.view.physicalSize = const Size(1500, 900);
|
||||||
tester.view.devicePixelRatio = 1;
|
tester.view.devicePixelRatio = 1;
|
||||||
|
|
@ -364,6 +420,18 @@ void main() {
|
||||||
await tester.tap(find.text('실행 취소'));
|
await tester.tap(find.text('실행 취소'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(bm.tree.findByUrl('https://www.youtube.com/watch?v=abc'), isNotNull);
|
expect(bm.tree.findByUrl('https://www.youtube.com/watch?v=abc'), isNotNull);
|
||||||
|
// 실행 취소 알림은 누르지 않으면 몇 초 뒤 저절로 사라진다 (Flutter 3.47+ 는 action 이 있으면 기본이 안 사라짐)
|
||||||
|
await tester.tap(find.byTooltip('더 보기').last);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('삭제').last);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('실행 취소'), findsOneWidget);
|
||||||
|
await tester.pump(const Duration(seconds: 5));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('실행 취소'), findsNothing);
|
||||||
|
bm.undoRemove();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(bm.tree.findByUrl('https://www.youtube.com/watch?v=abc'), isNotNull);
|
||||||
|
|
||||||
// 패널에서 누르면 그 주소로 이동
|
// 패널에서 누르면 그 주소로 이동
|
||||||
await tester.tap(find.text('OpenSubtitles').last);
|
await tester.tap(find.text('OpenSubtitles').last);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue