다운로드 알림: 준비가 끝나면 닫힘, 화면을 오간 뒤에도 '목록 보기' 동작 (다운로드 목록은 열린 화면으로 돌아가기)
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,
|
hotkey: controller.settings.showHotkey,
|
||||||
minimizeToTray: () => controller.settings.minimizeToTray,
|
minimizeToTray: () => controller.settings.minimizeToTray,
|
||||||
onCloseRequested: confirmQuit,
|
onCloseRequested: confirmQuit,
|
||||||
onDownloadsRequested: () => navigatorKey.currentState
|
onDownloadsRequested: () {
|
||||||
?.push(MaterialPageRoute<void>(builder: (_) => DownloadsPage(d: downloads))),
|
final nav = navigatorKey.currentState;
|
||||||
|
if (nav != null) unawaited(DownloadsPage.open(nav, downloads));
|
||||||
|
},
|
||||||
);
|
);
|
||||||
// 제목 표시줄에 버전 · 지난번 창 위치에서 열기 · 동영상 목록 불러오기 (창끼리 공유)
|
// 제목 표시줄에 버전 · 지난번 창 위치에서 열기 · 동영상 목록 불러오기 (창끼리 공유)
|
||||||
await windowManager.setTitle(appTitle);
|
await windowManager.setTitle(appTitle);
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ class AppNavButtons extends StatelessWidget {
|
||||||
onDownloadsPage ? '다운로드 목록 (지금 여기)' : '다운로드 목록',
|
onDownloadsPage ? '다운로드 목록 (지금 여기)' : '다운로드 목록',
|
||||||
downloads == null || onDownloadsPage
|
downloads == null || onDownloadsPage
|
||||||
? null
|
? null
|
||||||
: () => Navigator.push(context, MaterialPageRoute<void>(builder: (_) => DownloadsPage(d: downloads))),
|
: () => DownloadsPage.open(Navigator.of(context), downloads),
|
||||||
here: onDownloadsPage,
|
here: onDownloadsPage,
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
@ -14,6 +15,7 @@ import '../core/download_detect.dart' show CookieRecord, toNetscapeCookies;
|
||||||
import '../core/web_address.dart';
|
import '../core/web_address.dart';
|
||||||
import '../platform/windows/cef_runtime.dart';
|
import '../platform/windows/cef_runtime.dart';
|
||||||
import '../platform/windows/com_guard.dart';
|
import '../platform/windows/com_guard.dart';
|
||||||
|
import '../services/downloader.dart' show DownloadState;
|
||||||
import 'app_actions.dart';
|
import 'app_actions.dart';
|
||||||
import 'bookmark_ui.dart';
|
import 'bookmark_ui.dart';
|
||||||
import 'downloads_page.dart';
|
import 'downloads_page.dart';
|
||||||
|
|
@ -199,18 +201,34 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
|
||||||
await _nav?.exportCookies();
|
await _nav?.exportCookies();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final t = widget.downloads!.addPage(_url);
|
final d = widget.downloads!;
|
||||||
|
final t = d.addPage(_url);
|
||||||
final m = ScaffoldMessenger.of(context);
|
final m = ScaffoldMessenger.of(context);
|
||||||
m.showSnackBar(SnackBar(
|
if (t == null) {
|
||||||
content: Text(t == null ? '이미 받는 중이거나 받을 수 없는 주소입니다.' : '다운로드 추가: ${_title.isEmpty ? _url : _title}'),
|
m.showSnackBar(const SnackBar(content: Text('이미 받는 중이거나 받을 수 없는 주소입니다.')));
|
||||||
action: t == null
|
return;
|
||||||
? null
|
}
|
||||||
: SnackBarAction(
|
// 알림은 앱 전체에 떠 있으므로 이 브라우저 화면이 아니라 앱의 Navigator 로 연다
|
||||||
label: '목록 보기',
|
// (화면을 오가 이 브라우저 화면이 닫혀도 "목록 보기" 가 된다)
|
||||||
onPressed: () => Navigator.push(
|
final nav = Navigator.of(context);
|
||||||
context, MaterialPageRoute<void>(builder: (_) => DownloadsPage(d: widget.downloads!))),
|
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) =>
|
void _openExternal(String url) =>
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,43 @@ class DownloadsPage extends StatefulWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<DownloadsPage> createState() => _DownloadsPageState();
|
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> {
|
class _DownloadsPageState extends State<DownloadsPage> {
|
||||||
final _url = TextEditingController();
|
final _url = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
DownloadsPage._open++;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
DownloadsPage._open--;
|
||||||
_url.dispose();
|
_url.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -288,8 +288,7 @@ class _DownloadBox extends StatelessWidget {
|
||||||
final p = d.overallProgress;
|
final p = d.overallProgress;
|
||||||
return InkWell(
|
return InkWell(
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
onTap: () => Navigator.push(
|
onTap: () => DownloadsPage.open(Navigator.of(context), d),
|
||||||
context, MaterialPageRoute<void>(builder: (_) => DownloadsPage(d: d))),
|
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 150,
|
width: 150,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||||
|
|
|
||||||
|
|
@ -48,12 +48,7 @@ class WorkPanel extends StatelessWidget {
|
||||||
_title(
|
_title(
|
||||||
'다운로드 ${downloads!.tasks.length}개',
|
'다운로드 ${downloads!.tasks.length}개',
|
||||||
action: TextButton(
|
action: TextButton(
|
||||||
onPressed: () => Navigator.push(
|
onPressed: () => DownloadsPage.open(Navigator.of(context), downloads!),
|
||||||
context,
|
|
||||||
MaterialPageRoute<void>(
|
|
||||||
builder: (_) => DownloadsPage(d: downloads!),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'전체 목록',
|
'전체 목록',
|
||||||
style: TextStyle(fontSize: 12),
|
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/downloader.dart';
|
||||||
import 'package:jj_mkvmaker/services/platform_services.dart';
|
import 'package:jj_mkvmaker/services/platform_services.dart';
|
||||||
import 'package:jj_mkvmaker/ui/browser_page.dart';
|
import 'package:jj_mkvmaker/ui/browser_page.dart';
|
||||||
|
import 'package:jj_mkvmaker/ui/downloads_page.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
class _Backend implements DownloadBackend {
|
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 {
|
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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue