149 lines
6.0 KiB
Dart
149 lines
6.0 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../app/app_controller.dart';
|
|
import '../app/live_sync.dart';
|
|
import '../app/settings.dart';
|
|
import '../l10n/tr.dart';
|
|
import 'app_actions.dart';
|
|
import 'copy_sync_settings.dart' show addLiveSyncPairDialog, LiveSyncRunOptions;
|
|
import 'monitor/copy_task_card.dart';
|
|
import 'monitor/live_pair_card.dart';
|
|
import 'monitor/live_sync_flow.dart';
|
|
import 'theme.dart';
|
|
|
|
// 모니터링의 부품은 lib/ui/monitor/ 의 모듈에 있다. 예전부터 이 파일에서 가져다 쓰던 이름은 그대로 내보낸다.
|
|
export 'monitor/live_sync_flow.dart' show confirmLiveDeletes, confirmLiveDeleteNow, confirmEmptySourceSync, askLiveSyncStart;
|
|
export 'monitor/transfer_widgets.dart' show TransferProgressLines, PruneChoice, pruneLabel;
|
|
|
|
/// 모니터링 (파일 탐색기 > 모니터링): 복사 · rsync 와 lsync (실시간 동기화) 를 따로 보여 준다.
|
|
|
|
class MonitorPage extends StatelessWidget {
|
|
final AppController c;
|
|
final int initialTab;
|
|
const MonitorPage({super.key, required this.c, this.initialTab = 0});
|
|
|
|
static Future<void> open(BuildContext context, AppController c, {int tab = 0}) => Navigator.of(context)
|
|
.push(MaterialPageRoute<void>(builder: (_) => MonitorPage(c: c, initialTab: tab)));
|
|
|
|
@override
|
|
Widget build(BuildContext context) => DefaultTabController(
|
|
length: 2,
|
|
initialIndex: initialTab,
|
|
child: Scaffold(
|
|
body: Column(children: [
|
|
Builder(builder: (context) {
|
|
// 좁은 화면: 둘째 줄 (높이 48) 에 맞게 탭에 아이콘을 옆으로
|
|
final compact = isCompact(context);
|
|
Tab tab(IconData icon, String text) => compact
|
|
? Tab(height: 46, child: Row(mainAxisSize: MainAxisSize.min, children: [Icon(icon, size: 16), const SizedBox(width: 6), Text(text)]))
|
|
: Tab(icon: Icon(icon, size: 18), text: text);
|
|
return AppTopBar(
|
|
nav: const AppNavButtons(),
|
|
actions: AppActions(c: c),
|
|
middle: Row(children: [
|
|
Text(tr('모니터링'), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: TabBar(
|
|
isScrollable: true,
|
|
tabAlignment: TabAlignment.start,
|
|
tabs: [
|
|
tab(Icons.copy_all_outlined, tr('복사 · rsync')),
|
|
tab(Icons.sync, tr('lsync (실시간 동기화)')),
|
|
],
|
|
),
|
|
),
|
|
]),
|
|
);
|
|
}),
|
|
Expanded(
|
|
child: TabBarView(children: [
|
|
CopyTaskList(c: c),
|
|
_LiveTab(c: c),
|
|
]),
|
|
),
|
|
]),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ───────── lsync (실시간 동기화) ─────────
|
|
|
|
class _LiveTab extends StatefulWidget {
|
|
final AppController c;
|
|
const _LiveTab({required this.c});
|
|
|
|
@override
|
|
State<_LiveTab> createState() => _LiveTabState();
|
|
}
|
|
|
|
class _LiveTabState extends State<_LiveTab> {
|
|
AppController get c => widget.c;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
// 열 때 맞출 것을 새로 센다
|
|
final live = LiveSync.instance;
|
|
if (live != null) {
|
|
for (final x in c.settings.liveSyncPairs) {
|
|
live.refreshPending(x);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 최종 정리를 시작했으면 진행을 볼 수 있게 복사 · rsync 탭으로
|
|
Future<void> _finalize(LiveSyncPair x) async {
|
|
if (await finalizeLiveSync(context, c, x) && mounted) DefaultTabController.of(context).animateTo(0);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final live = LiveSync.instance;
|
|
return ListenableBuilder(
|
|
listenable: Listenable.merge([c, ?live]),
|
|
builder: (context, _) {
|
|
final pairs = c.settings.liveSyncPairs;
|
|
return ListView(padding: const EdgeInsets.all(12), children: [
|
|
Row(children: [
|
|
Expanded(
|
|
child: Text(
|
|
tr('원본이 바뀌면 대상에 맞춥니다. 일정을 정하면 그 시간에만 맞추고, 그 밖의 시간에는 바뀐 것을 모아 두었다가 다음 시작 시간에 맞춥니다.'),
|
|
style: TextStyle(fontSize: 12, color: JjColors.textDim),
|
|
),
|
|
),
|
|
TextButton.icon(onPressed: () => addLiveSyncPairDialog(context, c), icon: const Icon(Icons.add), label: Text(tr('추가'))),
|
|
]),
|
|
// 백그라운드로 실행 · 다시 켤 때 동기화 (환경 설정과 같은 값) · 모두 시작 / 멈춤
|
|
Wrap(spacing: 12, runSpacing: 4, crossAxisAlignment: WrapCrossAlignment.center, children: [
|
|
LiveSyncRunOptions(c: c, dense: true),
|
|
if (live != null && pairs.any((x) => x.enabled))
|
|
live.paused.isEmpty
|
|
? TextButton.icon(
|
|
onPressed: live.pauseAll, icon: const Icon(Icons.pause, size: 18), label: Text(tr('모두 멈춤')))
|
|
: FilledButton.tonalIcon(
|
|
onPressed: live.resumeAll,
|
|
icon: const Icon(Icons.play_arrow, size: 18),
|
|
label: Text(trf('모두 시작 (멈춘 것 {0}개)', [pairs.where(live.isPaused).length]))),
|
|
]),
|
|
const SizedBox(height: 8),
|
|
if (pairs.isEmpty)
|
|
Padding(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Center(child: Text(tr('실시간 동기화가 없습니다. [추가] 하거나 복사 · rsync 탭에서 [lsync 로 이동] 하세요.'),
|
|
style: TextStyle(color: JjColors.textDim))),
|
|
),
|
|
for (final x in pairs)
|
|
LivePairCard(
|
|
c: c,
|
|
live: live,
|
|
pair: x,
|
|
onFinalize: () => _finalize(x),
|
|
onShowCopyList: () => DefaultTabController.of(context).animateTo(0),
|
|
),
|
|
]);
|
|
},
|
|
);
|
|
}
|
|
}
|