다운로드 알림: 준비가 끝나면 닫힘, 화면을 오간 뒤에도 '목록 보기' 동작 (다운로드 목록은 열린 화면으로 돌아가기)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>main
parent
d89bd8a245
commit
14b2489938
|
|
@ -177,8 +177,10 @@ Future<void> main(List<String> args) async {
|
|||
hotkey: controller.settings.showHotkey,
|
||||
minimizeToTray: () => controller.settings.minimizeToTray,
|
||||
onCloseRequested: confirmQuit,
|
||||
onDownloadsRequested: () => navigatorKey.currentState
|
||||
?.push(MaterialPageRoute<void>(builder: (_) => DownloadsPage(d: downloads))),
|
||||
onDownloadsRequested: () {
|
||||
final nav = navigatorKey.currentState;
|
||||
if (nav != null) unawaited(DownloadsPage.open(nav, downloads));
|
||||
},
|
||||
);
|
||||
// 제목 표시줄에 버전 · 지난번 창 위치에서 열기 · 동영상 목록 불러오기 (창끼리 공유)
|
||||
await windowManager.setTitle(appTitle);
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ class AppNavButtons extends StatelessWidget {
|
|||
onDownloadsPage ? '다운로드 목록 (지금 여기)' : '다운로드 목록',
|
||||
downloads == null || onDownloadsPage
|
||||
? null
|
||||
: () => Navigator.push(context, MaterialPageRoute<void>(builder: (_) => DownloadsPage(d: downloads))),
|
||||
: () => DownloadsPage.open(Navigator.of(context), downloads),
|
||||
here: onDownloadsPage,
|
||||
),
|
||||
]),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -14,6 +15,7 @@ import '../core/download_detect.dart' show CookieRecord, toNetscapeCookies;
|
|||
import '../core/web_address.dart';
|
||||
import '../platform/windows/cef_runtime.dart';
|
||||
import '../platform/windows/com_guard.dart';
|
||||
import '../services/downloader.dart' show DownloadState;
|
||||
import 'app_actions.dart';
|
||||
import 'bookmark_ui.dart';
|
||||
import 'downloads_page.dart';
|
||||
|
|
@ -199,18 +201,34 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
|
|||
await _nav?.exportCookies();
|
||||
} catch (_) {}
|
||||
if (!mounted) return;
|
||||
final t = widget.downloads!.addPage(_url);
|
||||
final d = widget.downloads!;
|
||||
final t = d.addPage(_url);
|
||||
final m = ScaffoldMessenger.of(context);
|
||||
m.showSnackBar(SnackBar(
|
||||
content: Text(t == null ? '이미 받는 중이거나 받을 수 없는 주소입니다.' : '다운로드 추가: ${_title.isEmpty ? _url : _title}'),
|
||||
action: t == null
|
||||
? null
|
||||
: SnackBarAction(
|
||||
label: '목록 보기',
|
||||
onPressed: () => Navigator.push(
|
||||
context, MaterialPageRoute<void>(builder: (_) => DownloadsPage(d: widget.downloads!))),
|
||||
),
|
||||
if (t == null) {
|
||||
m.showSnackBar(const SnackBar(content: Text('이미 받는 중이거나 받을 수 없는 주소입니다.')));
|
||||
return;
|
||||
}
|
||||
// 알림은 앱 전체에 떠 있으므로 이 브라우저 화면이 아니라 앱의 Navigator 로 연다
|
||||
// (화면을 오가 이 브라우저 화면이 닫혀도 "목록 보기" 가 된다)
|
||||
final nav = Navigator.of(context);
|
||||
final bar = m.showSnackBar(SnackBar(
|
||||
content: Text('다운로드 추가: ${_title.isEmpty ? _url : _title}'),
|
||||
duration: const Duration(minutes: 10),
|
||||
action: SnackBarAction(label: '목록 보기', onPressed: () => DownloadsPage.open(nav, d)),
|
||||
));
|
||||
// 받기 준비가 끝나면 (진행률이 나오거나 · 끝 · 실패 · 취소 · 목록에서 지움) 알림을 닫는다
|
||||
void check() {
|
||||
final ready = !d.tasks.contains(t) ||
|
||||
t.progress != null ||
|
||||
(t.state != DownloadState.queued && t.state != DownloadState.downloading);
|
||||
if (ready) {
|
||||
d.removeListener(check);
|
||||
bar.close();
|
||||
}
|
||||
}
|
||||
|
||||
d.addListener(check);
|
||||
unawaited(bar.closed.then((_) => d.removeListener(check)));
|
||||
}
|
||||
|
||||
void _openExternal(String url) =>
|
||||
|
|
|
|||
|
|
@ -15,13 +15,43 @@ class DownloadsPage extends StatefulWidget {
|
|||
|
||||
@override
|
||||
State<DownloadsPage> createState() => _DownloadsPageState();
|
||||
|
||||
/// 다운로드 목록 화면의 경로 이름 (이미 열려 있으면 그 화면으로 돌아가기 위해)
|
||||
static const routeName = 'downloads';
|
||||
|
||||
/// 열려 있는 다운로드 목록 화면 수
|
||||
static int _open = 0;
|
||||
|
||||
/// 다운로드 목록으로: 이미 열려 있으면 그 화면으로 돌아가고, 없으면 새로 연다.
|
||||
/// 위쪽 [다운로드 목록] 버튼 · 브라우저의 "목록 보기" · 작업 현황 등 어디서 열어도 같다.
|
||||
static Future<void> open(NavigatorState nav, DownloadManager d) async {
|
||||
if (_open > 0) {
|
||||
var found = false;
|
||||
nav.popUntil((r) {
|
||||
if (r.settings.name == routeName) found = true;
|
||||
return found || r.isFirst;
|
||||
});
|
||||
if (found) return;
|
||||
}
|
||||
await nav.push(MaterialPageRoute<void>(
|
||||
settings: const RouteSettings(name: routeName),
|
||||
builder: (_) => DownloadsPage(d: d),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class _DownloadsPageState extends State<DownloadsPage> {
|
||||
final _url = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
DownloadsPage._open++;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
DownloadsPage._open--;
|
||||
_url.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -288,8 +288,7 @@ class _DownloadBox extends StatelessWidget {
|
|||
final p = d.overallProgress;
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
onTap: () => Navigator.push(
|
||||
context, MaterialPageRoute<void>(builder: (_) => DownloadsPage(d: d))),
|
||||
onTap: () => DownloadsPage.open(Navigator.of(context), d),
|
||||
child: Container(
|
||||
width: 150,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
|
|
|
|||
|
|
@ -48,12 +48,7 @@ class WorkPanel extends StatelessWidget {
|
|||
_title(
|
||||
'다운로드 ${downloads!.tasks.length}개',
|
||||
action: TextButton(
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => DownloadsPage(d: downloads!),
|
||||
),
|
||||
),
|
||||
onPressed: () => DownloadsPage.open(Navigator.of(context), downloads!),
|
||||
child: const Text(
|
||||
'전체 목록',
|
||||
style: TextStyle(fontSize: 12),
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
|
|||
import 'package:jj_mkvmaker/services/downloader.dart';
|
||||
import 'package:jj_mkvmaker/services/platform_services.dart';
|
||||
import 'package:jj_mkvmaker/ui/browser_page.dart';
|
||||
import 'package:jj_mkvmaker/ui/downloads_page.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
class _Backend implements DownloadBackend {
|
||||
|
|
@ -182,6 +183,90 @@ void main() {
|
|||
});
|
||||
});
|
||||
|
||||
testWidgets('다운로드 알림: 브라우저 화면이 닫혀도 "목록 보기" 가 되고, 준비가 끝나면 사라진다', (tester) async {
|
||||
tester.view.physicalSize = const Size(1500, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
final dir = Directory.systemTemp.createTempSync('jj_dlbar_');
|
||||
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 d = DownloadManager(
|
||||
backends: [_Backend(DownloadKind.video)], settings: () => c.settings, readClipboard: () async => null);
|
||||
final navKey = GlobalKey<NavigatorState>();
|
||||
late BrowserHost host;
|
||||
Future<void> settle() async {
|
||||
for (var i = 0; i < 10; i++) {
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
}
|
||||
}
|
||||
|
||||
await tester.pumpWidget(MaterialApp(navigatorKey: navKey, home: const Scaffold(body: Text('MKV 화면'))));
|
||||
navKey.currentState!.push(MaterialPageRoute<void>(
|
||||
builder: (_) => BrowserPage(
|
||||
c: c,
|
||||
bookmarks: bm,
|
||||
downloads: d,
|
||||
initialUrl: 'https://www.youtube.com/watch?v=abc',
|
||||
viewBuilder: (h, url) {
|
||||
host = h;
|
||||
h.attach(_Nav());
|
||||
return const ColoredBox(color: Colors.white);
|
||||
},
|
||||
),
|
||||
));
|
||||
await settle();
|
||||
host.onUrl('https://www.youtube.com/watch?v=abc');
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('다운로드'));
|
||||
await settle();
|
||||
expect(find.text('목록 보기'), findsOneWidget);
|
||||
final t = d.tasks.single;
|
||||
|
||||
// MKV 화면으로 (브라우저 화면이 닫힘) → 알림의 "목록 보기" 를 눌러도 다운로드 목록이 열린다
|
||||
navKey.currentState!.popUntil((r) => r.isFirst);
|
||||
await settle();
|
||||
expect(find.byType(BrowserPage), findsNothing);
|
||||
await tester.tap(find.text('목록 보기'));
|
||||
await settle();
|
||||
expect(find.byType(DownloadsPage), findsOneWidget);
|
||||
expect(find.text('목록 보기'), findsNothing); // 누르면 알림은 닫힘
|
||||
// 다시 열어도 (위쪽 버튼 등) 목록 화면이 겹쳐 쌓이지 않는다
|
||||
await DownloadsPage.open(navKey.currentState!, d);
|
||||
await settle();
|
||||
expect(find.byType(DownloadsPage, skipOffstage: false), findsOneWidget);
|
||||
expect(t.state, DownloadState.downloading);
|
||||
|
||||
// 다른 동영상: 받기 준비가 끝나면 (진행률이 나오면) 알림이 저절로 사라진다
|
||||
navKey.currentState!.popUntil((r) => r.isFirst);
|
||||
navKey.currentState!.push(MaterialPageRoute<void>(
|
||||
builder: (_) => BrowserPage(
|
||||
c: c,
|
||||
bookmarks: bm,
|
||||
downloads: d,
|
||||
initialUrl: 'https://www.youtube.com/watch?v=def',
|
||||
viewBuilder: (h, url) {
|
||||
host = h;
|
||||
h.attach(_Nav());
|
||||
return const ColoredBox(color: Colors.white);
|
||||
},
|
||||
),
|
||||
));
|
||||
await settle();
|
||||
host.onUrl('https://www.youtube.com/watch?v=def');
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('다운로드'));
|
||||
await settle();
|
||||
expect(find.text('목록 보기'), findsOneWidget);
|
||||
await tester.pump(const Duration(seconds: 30));
|
||||
expect(find.text('목록 보기'), findsOneWidget, reason: '준비 중에는 그대로');
|
||||
d.tasks.first.progress = 0.1;
|
||||
d.refresh();
|
||||
await settle();
|
||||
expect(find.text('목록 보기'), findsNothing);
|
||||
d.dispose();
|
||||
});
|
||||
|
||||
testWidgets('브라우저: ☆ 추가 · 표시줄 · 다운로드 버튼 · 관리 패널 (삭제 · 실행 취소)', (tester) async {
|
||||
tester.view.physicalSize = const Size(1500, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
|
|
|
|||
Loading…
Reference in New Issue