154 lines
6.3 KiB
Dart
154 lines
6.3 KiB
Dart
/// 약속: 위쪽 막대의 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<UsageSample?>(
|
||
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),
|
||
),
|
||
),
|
||
);
|
||
});
|
||
},
|
||
);
|
||
}
|