157 · 162 끝난 작업 알림은 따로 "끝난 작업" 채널로 · [열기] 는 Android 에서 이 앱의 탐색기로
- 157: 끝 알림이 진행 알림 채널 (조용히, 낮은 중요도) 에 올라가 알림 창의 조용한 칸에 묻혔다 - 새 채널 "끝난 작업" (보통 중요도, 소리 없음, 설정에서 따로 끌 수 있음) - 162: ExplorerPage.openAt - 이 앱의 탐색기를 그 폴더로 (이미 열려 있으면 그 화면에서, 처음 열면 첫 창으로). AI 해상도 끝 알림 · 작업 현황의 [열기] 가 Android 에서 이것을 씀 (Windows 는 Windows 탐색기). 폴더 메뉴의 "파일 관리자에서 보기" 로 다른 파일 앱도 그대로 열 수 있다 - 시험: openAt 으로 열기 · 열린 채로 다른 폴더 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>묶음4
parent
cc05fcad3c
commit
daac19a514
|
|
@ -83,10 +83,20 @@ class KeepAliveService : Service() {
|
|||
const val STOPPED_ID = 2
|
||||
const val DONE_ID = 3
|
||||
|
||||
/// 157: 끝난 작업 알림 (예: "4장 저장: 폴더"). 누르면 작업 현황으로
|
||||
const val DONE_CHANNEL_ID = "done"
|
||||
|
||||
/// 157: 끝난 작업 알림 (예: "4장 저장: 폴더"). 누르면 작업 현황으로.
|
||||
/// 진행 알림 채널 (조용히, 낮은 중요도) 과 따로 두어 알림 창의 "조용한 알림" 칸에 묻히지 않게 (보통 중요도, 소리 없음)
|
||||
fun notifyDone(context: Context, title: String, text: String) {
|
||||
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
notification(context, "", -1, "") // 알림 채널을 만들어 둔다
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && nm.getNotificationChannel(DONE_CHANNEL_ID) == null) {
|
||||
nm.createNotificationChannel(
|
||||
NotificationChannel(DONE_CHANNEL_ID, "끝난 작업", NotificationManager.IMPORTANCE_DEFAULT).apply {
|
||||
description = "AI 해상도 올리기 등 오래 걸린 작업이 끝났을 때"
|
||||
setSound(null, null)
|
||||
}
|
||||
)
|
||||
}
|
||||
val open = PendingIntent.getActivity(
|
||||
context, 3,
|
||||
Intent(context, MainActivity::class.java)
|
||||
|
|
@ -95,7 +105,7 @@ class KeepAliveService : Service() {
|
|||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
val b = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Notification.Builder(context, CHANNEL_ID)
|
||||
Notification.Builder(context, DONE_CHANNEL_ID)
|
||||
} else {
|
||||
@Suppress("DEPRECATION") Notification.Builder(context)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import '../l10n/tr.dart';
|
|||
import '../services/image_ai.dart' show ImageAiException;
|
||||
import 'ai_image_page.dart' show aiSaveDir;
|
||||
import 'confirm.dart';
|
||||
import 'explorer_page.dart' show ExplorerPage;
|
||||
import 'theme.dart';
|
||||
|
||||
UpscaleOptions upscaleOptionsOf(AppController c, {String? outDir}) {
|
||||
|
|
@ -265,6 +266,15 @@ Future<String?> upscaleOne(BuildContext context, AppController c, {required Stri
|
|||
}
|
||||
}
|
||||
|
||||
/// 157 · 162: 저장한 폴더 열기 - Android 는 이 앱의 파일 탐색기로 (삼성 파일 앱 대신), Windows 는 탐색기로
|
||||
Future<void> openFolderInApp(NavigatorState nav, AppController c, String dir) async {
|
||||
if (Platform.isAndroid) {
|
||||
await ExplorerPage.openAt(nav, c: c, dir: dir);
|
||||
} else {
|
||||
await c.services.shell.revealFile(dir);
|
||||
}
|
||||
}
|
||||
|
||||
/// 121: 폴더 · ZIP 의 모든 장을 올려 저장 (백그라운드 - 작업 알림 · 작업 현황에 남은 장 · 예상 시간)
|
||||
Future<void> upscaleAll(BuildContext context, AppController c, ReaderSource src) async {
|
||||
final n = src.length;
|
||||
|
|
@ -294,6 +304,7 @@ Future<void> upscaleAll(BuildContext context, AppController c, ReaderSource src)
|
|||
);
|
||||
if (!ok || !context.mounted) return;
|
||||
final messenger = ScaffoldMessenger.maybeOf(context);
|
||||
final nav = Navigator.of(context);
|
||||
final failed = <String>[];
|
||||
String? lastDir;
|
||||
final done = await AiJobs.instance.runTask(tr('AI 해상도 올리기'), n, (i, progress) async {
|
||||
|
|
@ -326,7 +337,7 @@ Future<void> upscaleAll(BuildContext context, AppController c, ReaderSource src)
|
|||
messenger?.showSnackBar(SnackBar(
|
||||
duration: const Duration(seconds: 10),
|
||||
content: Text(text),
|
||||
action: dir == null ? null : SnackBarAction(label: tr('열기'), onPressed: () => c.services.shell.revealFile(dir)),
|
||||
action: dir == null ? null : SnackBarAction(label: tr('열기'), onPressed: () => openFolderInApp(nav, c, dir)),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -73,6 +73,15 @@ class ExplorerPage extends StatefulWidget {
|
|||
/// Rsync 화면으로 (이미 열려 있으면 그 화면으로)
|
||||
static Future<void> openRsync(NavigatorState nav, {required AppController c}) => _show(nav, c, rsync: true);
|
||||
|
||||
/// 162: 이 앱의 파일 탐색기로 그 폴더를 연다 (이미 열려 있으면 그 화면에서 그 폴더로). 끝난 작업의 [열기] 등
|
||||
static Future<void> openAt(NavigatorState nav, {required AppController c, required String dir}) {
|
||||
goToRequest.value = dir;
|
||||
return _show(nav, c, rsync: false);
|
||||
}
|
||||
|
||||
/// 열려 있는 (또는 곧 열릴) 탐색기가 갈 폴더 (가면 비운다)
|
||||
static final goToRequest = ValueNotifier<String?>(null);
|
||||
|
||||
static Future<void> _show(NavigatorState nav, AppController c, {required bool rsync}) async {
|
||||
final name = rsync ? rsyncRouteName : routeName;
|
||||
if ((rsync ? _openRsync : _open) > 0) {
|
||||
|
|
@ -264,6 +273,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
ExplorerPage._openRsync--;
|
||||
} else {
|
||||
ExplorerPage._open--;
|
||||
ExplorerPage.goToRequest.removeListener(_onGoToRequest);
|
||||
}
|
||||
for (final pane in _panes) {
|
||||
pane.dispose();
|
||||
|
|
@ -288,6 +298,12 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
final wants = [
|
||||
for (var i = 0; i < 2; i++) reach[i] ? saved[i] : _local[0].$1,
|
||||
];
|
||||
// 162: "이 폴더를 탐색기로" 로 열렸으면 첫 창은 그 폴더 (지난 폴더 대신)
|
||||
final asked = widget.rsync ? null : ExplorerPage.goToRequest.value;
|
||||
if (asked != null) {
|
||||
ExplorerPage.goToRequest.value = null;
|
||||
wants[0] = asked;
|
||||
}
|
||||
for (var i = 0; i < 2; i++) {
|
||||
_panes[i]
|
||||
..root = _volumeOf(wants[i])
|
||||
|
|
@ -297,6 +313,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
}
|
||||
if (!mounted) return;
|
||||
setState(() => _ready = true);
|
||||
// (이미 열린 화면에 들어온 요청은 그 화면에서)
|
||||
if (!widget.rsync) ExplorerPage.goToRequest.addListener(_onGoToRequest);
|
||||
// 52: 다른 화면에 다녀와도 아직 도는 복사 · 이동의 진행 막대를 다시 보이고, 끝나면 그 폴더들을 새로 읽는다
|
||||
final center = CopyCenter.peekOf(c);
|
||||
if (center != null) {
|
||||
|
|
@ -1594,6 +1612,13 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
/// (Rsync 화면의 ⇄ 는 둘이 함께)
|
||||
List<TransferJob> _jobs = const [];
|
||||
|
||||
void _onGoToRequest() {
|
||||
final dir = ExplorerPage.goToRequest.value;
|
||||
if (dir == null || !_ready || !mounted) return;
|
||||
ExplorerPage.goToRequest.value = null;
|
||||
unawaited(_goTo(_pane, dir, fresh: true).then((_) => _reveal(_pane, dir)));
|
||||
}
|
||||
|
||||
/// 52: 이 화면을 떠난 사이에 시작된 (또는 앞의 화면이 시작한) 작업을 다시 보이고, 끝나면 새로 읽고 막대를 내린다
|
||||
void _adoptJob(TransferJob job) {
|
||||
if (_jobs.contains(job)) return;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import '../app/ai_local.dart';
|
|||
import '../app/download_manager.dart';
|
||||
import '../core/models.dart';
|
||||
import '../services/downloader.dart';
|
||||
import 'ai_upscale_ui.dart' show openFolderInApp;
|
||||
import 'app_actions.dart';
|
||||
import 'downloads_page.dart';
|
||||
import 'theme.dart';
|
||||
|
|
@ -108,7 +109,7 @@ class WorkPanel extends StatelessWidget {
|
|||
child: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 12),
|
||||
children: [
|
||||
..._jobs(),
|
||||
..._jobs(context),
|
||||
_title(trf('편집 목록 {0}개', [c.videos.length])),
|
||||
if (c.videos.isEmpty)
|
||||
_Dim(tr('아직 없습니다. 영상을 다 받으면 자동으로 들어옵니다 (설정에서 변경).')),
|
||||
|
|
@ -184,7 +185,7 @@ class WorkPanel extends StatelessWidget {
|
|||
),
|
||||
);
|
||||
|
||||
List<Widget> _jobs() => [
|
||||
List<Widget> _jobs(BuildContext context) => [
|
||||
_title(tr('작업')),
|
||||
// 123: AI 그림 (남은 장 · 예상 시간)
|
||||
if (AiJobs.instance.statusLine case final ai?) ...[
|
||||
|
|
@ -196,7 +197,7 @@ class WorkPanel extends StatelessWidget {
|
|||
Row(children: [
|
||||
Expanded(child: Text('✓ ${AiJobs.instance.lastDone}', style: const TextStyle(fontSize: 12))),
|
||||
if (AiJobs.instance.lastDoneDir case final dir?)
|
||||
TextButton(onPressed: () => c.services.shell.revealFile(dir), child: Text(tr('열기'))),
|
||||
TextButton(onPressed: () => openFolderInApp(Navigator.of(context), c, dir), child: Text(tr('열기'))),
|
||||
]),
|
||||
if (!c.busy && !AiJobs.instance.busy)
|
||||
_Dim(tr('쉬는 중'))
|
||||
|
|
|
|||
|
|
@ -351,6 +351,27 @@ void main() {
|
|||
expect(find.text('더 보기'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('157 · 162: 끝난 작업의 [열기] - 이 앱의 탐색기로 그 폴더를 연다 (이미 열려 있으면 그 화면에서 그 폴더로)', (tester) async {
|
||||
final out = Directory(p.join(tmp.path, 'out_AI'))..createSync();
|
||||
File(p.join(out.path, 'p1_x2.png')).writeAsStringSync('x');
|
||||
tester.view.physicalSize = const Size(1600, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
final navKey = GlobalKey<NavigatorState>();
|
||||
await tester.runAsync(() => tester.pumpWidget(MaterialApp(navigatorKey: navKey, home: const Text('앞 화면'))));
|
||||
unawaited(ExplorerPage.openAt(navKey.currentState!, c: c, dir: out.path));
|
||||
// 제목 (지금 경로) 이 그 폴더 - 목록은 저장 장치 맨 위부터라 그 파일 줄이 화면 밖일 수 있다
|
||||
await settle(tester, () => find.text(out.path).evaluate().isNotEmpty);
|
||||
expect(find.text(out.path), findsOneWidget);
|
||||
expect(ExplorerPage.goToRequest.value, isNull, reason: '간 뒤에는 비움');
|
||||
// 이미 열린 채로 다른 폴더를 요청하면 새 화면을 쌓지 않고 그 화면에서
|
||||
// (실제 비동기 구역에서: 그때 시작한 폴더 읽기가 시험의 가짜 시간에 묶이지 않게)
|
||||
await tester.runAsync(() async => unawaited(ExplorerPage.openAt(navKey.currentState!, c: c, dir: right)));
|
||||
await settle(tester, () => find.text(right).evaluate().isNotEmpty);
|
||||
expect(find.byType(ExplorerPage), findsOneWidget);
|
||||
expect(find.text(right), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('146 · 37-2: 없는 경로를 넣으면 창을 닫지 않고 칸 아래에 "폴더가 없거나 열 수 없습니다" · 고친 경로로 간다', (tester) async {
|
||||
tester.view.physicalSize = const Size(1600, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
|
|
|
|||
Loading…
Reference in New Issue