JJ_MKVMaker/lib/ui/app_manager_page.dart

166 lines
6.7 KiB
Dart

import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import '../core/app_manager.dart';
import '../core/file_ops.dart' show formatSize;
import '../core/vfs.dart' show vDisplay;
import '../l10n/tr.dart';
import 'theme.dart';
/// X-plore 비교 5-2 앱 관리자 (사용자 결정 10/21, Android): 실행할 수 있는 앱 목록 - 이름 · 판 · 크기 · 설치일,
/// [APK 백업] (Download/JJ_MKVMaker/apk) · [앱 정보] (시스템 화면) · [지우기] (시스템 지우기 창)
Future<void> openAppManager(BuildContext context, {AppManagerBackend? backend}) => Navigator.of(context)
.push(MaterialPageRoute<void>(builder: (_) => AppManagerPage(backend: backend ?? AndroidAppManager())));
class AppManagerPage extends StatefulWidget {
final AppManagerBackend backend;
const AppManagerPage({super.key, required this.backend});
@override
State<AppManagerPage> createState() => _AppManagerPageState();
}
class _AppManagerPageState extends State<AppManagerPage> {
List<InstalledApp>? _apps;
String? _error;
String _sort = 'name';
String _filter = '';
final _icons = <String, Future<Uint8List?>>{};
@override
void initState() {
super.initState();
unawaited(_load());
}
Future<void> _load() async {
try {
final l = await widget.backend.list();
if (mounted) setState(() => _apps = l);
} catch (e) {
if (mounted) setState(() => _error = trf('앱 목록을 읽지 못했습니다: {0}', [e]));
}
}
List<InstalledApp> get _shown {
final f = _filter.toLowerCase();
final l = [for (final a in _apps ?? const <InstalledApp>[]) if (f.isEmpty || a.label.toLowerCase().contains(f) || a.package.contains(f)) a];
switch (_sort) {
case 'size':
l.sort((a, b) => b.size.compareTo(a.size));
case 'installed':
l.sort((a, b) => b.installed.compareTo(a.installed));
default:
l.sort((a, b) => a.label.toLowerCase().compareTo(b.label.toLowerCase()));
}
return l;
}
void _snack(String t) => ScaffoldMessenger.maybeOf(context)?.showSnackBar(SnackBar(content: Text(t)));
String _day(DateTime d) => '${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}';
Future<void> _backup(InstalledApp a) async {
try {
final out = await backupApk(a, await widget.backend.backupDir());
_snack(a.splits > 0
? trf('APK 를 저장했습니다: {0} - 나눠진 앱 (조각 {1}개) 이라 이 파일 하나로는 설치되지 않을 수 있습니다', [vDisplay(out), a.splits])
: trf('APK 를 저장했습니다: {0}', [vDisplay(out)]));
} catch (e) {
_snack(trf('APK 를 저장하지 못했습니다: {0}', [e]));
}
}
Future<void> _menu(InstalledApp a) async {
final w = await showModalBottomSheet<String>(
context: context,
builder: (ctx) => SafeArea(
child: Column(mainAxisSize: MainAxisSize.min, children: [
ListTile(title: Text(a.label), subtitle: Text(a.package)),
ListTile(leading: const Icon(Icons.save_alt), title: Text(tr('APK 백업')), onTap: () => Navigator.pop(ctx, 'backup')),
ListTile(leading: const Icon(Icons.info_outline), title: Text(tr('앱 정보 (시스템 화면)')), onTap: () => Navigator.pop(ctx, 'details')),
if (!a.system)
ListTile(
leading: const Icon(Icons.delete_outline, color: JjColors.danger),
title: Text(tr('지우기 (시스템 창에서 확인)')),
onTap: () => Navigator.pop(ctx, 'uninstall')),
]),
),
);
switch (w) {
case 'backup':
await _backup(a);
case 'details':
await widget.backend.details(a.package);
case 'uninstall':
await widget.backend.uninstall(a.package);
}
}
@override
Widget build(BuildContext context) {
final apps = _apps;
return Scaffold(
appBar: AppBar(
title: Text(tr('앱 관리자')),
actions: [IconButton(tooltip: tr('새로 고침'), icon: const Icon(Icons.refresh), onPressed: _load)],
),
body: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
child: Text(tr('보이는 앱만 나옵니다 (모든 앱 목록 권한은 쓰지 않음)'), style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(children: [
Expanded(
child: TextField(
decoration: InputDecoration(isDense: true, prefixIcon: const Icon(Icons.search, size: 18), hintText: tr('이름으로 찾기')),
onChanged: (v) => setState(() => _filter = v.trim()),
),
),
const SizedBox(width: 8),
DropdownButton<String>(
value: _sort,
items: [
DropdownMenuItem(value: 'name', child: Text(tr('이름'))),
DropdownMenuItem(value: 'size', child: Text(tr('크기'))),
DropdownMenuItem(value: 'installed', child: Text(tr('설치일'))),
],
onChanged: (v) => setState(() => _sort = v!),
),
]),
),
if (_error != null) Padding(padding: const EdgeInsets.all(16), child: Text(_error!, style: const TextStyle(color: JjColors.danger))),
if (apps == null && _error == null) const Expanded(child: Center(child: CircularProgressIndicator())),
if (apps != null)
Expanded(
child: ListView.builder(
itemCount: _shown.length,
itemBuilder: (_, i) {
final a = _shown[i];
return ListTile(
leading: SizedBox(
width: 40,
height: 40,
child: FutureBuilder<Uint8List?>(
future: _icons[a.package] ??= widget.backend.icon(a.package).catchError((_) => null),
builder: (_, s) => s.data == null ? const Icon(Icons.android) : Image.memory(s.data!, width: 40, height: 40),
),
),
title: Text(a.label, maxLines: 1, overflow: TextOverflow.ellipsis),
subtitle: Text([if (a.version.isNotEmpty) a.version, formatSize(a.size), trf('설치 {0}', [_day(a.installed)])].join(' · '),
maxLines: 1, overflow: TextOverflow.ellipsis),
trailing: IconButton(tooltip: tr('더 보기'), icon: const Icon(Icons.more_vert), onPressed: () => _menu(a)),
onTap: () => _menu(a),
);
},
),
),
]),
);
}
}