166 lines
6.7 KiB
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),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
]),
|
|
);
|
|
}
|
|
}
|