/// 약속: 위쪽 막대의 CPU · 메모리 · 디스크 표시 ([UsageView]) · 화면 크기 버튼 ([ScaleButtons]) · 화면 전체 배율 ([UiScaler]). library; import 'package:flutter/material.dart'; import '../../app/app_controller.dart'; import '../../app/settings.dart'; import '../../services/system_usage.dart'; import '../theme.dart'; import '../../l10n/tr.dart'; /// "CPU 23% MEM 61%" (90% 넘으면 빨갛게). 글자 폭이 바뀌어도 옆 버튼이 움직이지 않게 너비를 고정한다. class UsageView extends StatelessWidget { final UsageMonitor usage; const UsageView({super.key, required this.usage}); static String _gb(int bytes) => (bytes / (1024 * 1024 * 1024)).toStringAsFixed(1); /// 남은 용량: "128GB" · "1.8TB" · "950MB" static String _size(int bytes) { const gb = 1024 * 1024 * 1024; if (bytes >= 1000 * gb) return '${(bytes / (1024 * gb)).toStringAsFixed(1)}TB'; if (bytes >= gb) return '${(bytes / gb).toStringAsFixed(bytes < 10 * gb ? 1 : 0)}GB'; return '${(bytes / (1024 * 1024)).round()}MB'; } @override Widget build(BuildContext context) => ValueListenableBuilder( valueListenable: usage, builder: (context, s, _) { Widget cell(String label, String value, bool hot, double width) { return SizedBox( width: width, child: Text.rich( TextSpan(children: [ TextSpan(text: '$label ', style: TextStyle(color: JjColors.textDim)), TextSpan( text: value, style: TextStyle( color: hot ? JjColors.danger : JjColors.text, fontWeight: FontWeight.w600)), ]), maxLines: 1, softWrap: false, style: const TextStyle(fontSize: 11, fontFamily: 'Consolas'), ), ); } Widget item(String label, double? v) => cell(label, v == null ? '--%' : '${(v * 100).round()}%', v != null && v >= 0.9, 62); final free = s?.diskFree; return Tooltip( message: s == null ? tr('PC 전체 CPU · 메모리 사용량') : trf('PC 전체 사용량\nCPU {0}%\n' '메모리 {1} / {2}GB ({3}%)' '{4}', [(s.cpu * 100).round(), _gb(s.memUsed), _gb(s.memTotal), (s.mem * 100).round(), free == null ? '' : trf('\n다운로드 디스크 {0} 남은 용량 {1}' '{2}', [s.disk, _size(free), s.diskTotal == null ? '' : trf(' / 전체 {0}', [_size(s.diskTotal!)])])]), child: Padding( padding: const EdgeInsets.only(left: 8), child: Row(mainAxisSize: MainAxisSize.min, children: [ item('CPU', s?.cpu), item('MEM', s?.mem), // 다운로드 대상 디스크의 남은 용량 if (free != null) cell('DISK', _size(free), s!.diskLow, 84), ]), ), ); }, ); } /// 화면 크기 조절: [−] [100%] [+]. 10% 씩 바꾸고, 가운데 숫자를 누르면 환경 설정의 기본 크기로. class ScaleButtons extends StatelessWidget { final AppController c; const ScaleButtons({super.key, required this.c}); void _set(double v) => c.updateSettings((s) => s.display.scale = DisplaySettings.clampScale(v)); @override Widget build(BuildContext context) => ListenableBuilder( listenable: c, builder: (context, _) { final s = c.settings.display.scale; Widget btn(IconData icon, String tip, VoidCallback? f) => SizedBox( width: 26, height: 32, child: IconButton( tooltip: tip, padding: EdgeInsets.zero, iconSize: 16, icon: Icon(icon), onPressed: f, ), ); return Padding( padding: const EdgeInsets.only(left: 6), child: Row(mainAxisSize: MainAxisSize.min, children: [ btn(Icons.remove, tr('화면 작게'), s <= DisplaySettings.scaleMin + 0.001 ? null : () => _set(s - 0.1)), Tooltip( message: trf('기본 크기 ({0}%) 로 · 기본 크기는 환경 설정 > 화면 에서', [(c.settings.display.scaleDefault * 100).round()]), child: InkWell( borderRadius: BorderRadius.circular(4), onTap: () => _set(c.settings.display.scaleDefault), child: SizedBox( width: 40, height: 28, child: Center( child: Text('${(s * 100).round()}%', style: const TextStyle(fontSize: 11, fontFamily: 'Consolas', fontWeight: FontWeight.w600)), ), ), ), ), btn(Icons.add, tr('화면 크게'), s >= DisplaySettings.scaleMax - 0.001 ? null : () => _set(s + 0.1)), ]), ); }, ); } /// 화면 전체를 [AppSettings.display.scale] 배로 그린다 (안쪽은 그만큼 작은 · 큰 화면이라고 여기고 배치). /// 앱의 builder 에서 한 번 감싼다 - 대화 상자 · 메뉴 · 안내 글까지 모두 같은 배율. class UiScaler extends StatelessWidget { final AppController c; final Widget child; const UiScaler({super.key, required this.c, required this.child}); @override Widget build(BuildContext context) => ListenableBuilder( listenable: c, builder: (context, _) { final s = c.settings.display.scale; if ((s - 1).abs() < 0.001) return child; return LayoutBuilder(builder: (context, box) { final size = box.biggest / s; final mq = MediaQuery.of(context); return ClipRect( child: OverflowBox( alignment: Alignment.topLeft, minWidth: size.width, maxWidth: size.width, minHeight: size.height, maxHeight: size.height, child: Transform.scale( scale: s, alignment: Alignment.topLeft, child: MediaQuery(data: mq.copyWith(size: size), child: child), ), ), ); }); }, ); }