improved performance and battery usage on older devices
This commit is contained in:
+58
-37
@@ -4,6 +4,7 @@ import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
@@ -15,6 +16,7 @@ import '../routing/app_routes.dart';
|
||||
import '../share_intent/remote_file_ref.dart';
|
||||
import '../state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import '../utils/downloads/download_manager.dart';
|
||||
import '../utils/screen_bound_image.dart';
|
||||
import 'app_progress_indicator.dart';
|
||||
import 'async_action_button.dart';
|
||||
import 'centered_leading.dart';
|
||||
@@ -49,6 +51,7 @@ class FileViewer extends StatefulWidget {
|
||||
enum FileViewingActions { openExternal, share, save, sendToChat, saveToCloud }
|
||||
|
||||
class _FileViewerState extends State<FileViewer> {
|
||||
Future<_TextPayload>? _textPayload;
|
||||
final PhotoViewController photoViewController = PhotoViewController();
|
||||
|
||||
late SettingsCubit settings = context.read<SettingsCubit>();
|
||||
@@ -302,7 +305,13 @@ class _FileViewerState extends State<FileViewer> {
|
||||
controller: photoViewController,
|
||||
maxScale: 3.0,
|
||||
minScale: 0.1,
|
||||
imageProvider: Image.file(File(widget.path)).image,
|
||||
// 2× the screen stays sharp while zooming in; the 4096 px cap only
|
||||
// bites on camera-sized photos.
|
||||
imageProvider: screenBoundImage(
|
||||
context,
|
||||
FileImage(File(widget.path)),
|
||||
scale: 2,
|
||||
),
|
||||
backgroundDecoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
@@ -348,13 +357,17 @@ class _FileViewerState extends State<FileViewer> {
|
||||
Widget _buildTextView() => Scaffold(
|
||||
appBar: _appbar(),
|
||||
body: FutureBuilder<_TextPayload>(
|
||||
future: _readTextPayload(),
|
||||
// Cached: a future created in build re-read the file on every rebuild.
|
||||
future: _textPayload ??= compute(
|
||||
_loadTextPayload,
|
||||
(widget.path, _textViewMaxBytes),
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(child: AppProgressIndicator.large());
|
||||
}
|
||||
final payload = snapshot.data!;
|
||||
final lines = const LineSplitter().convert(payload.content);
|
||||
final lines = payload.lines;
|
||||
// Stable gutter width — sized by the highest line number's digit count.
|
||||
final gutterWidth = (lines.length.toString().length * 9.0) + 16;
|
||||
return SelectionArea(
|
||||
@@ -443,38 +456,6 @@ class _FileViewerState extends State<FileViewer> {
|
||||
}
|
||||
|
||||
static const int _textViewMaxBytes = 5 * 1024 * 1024;
|
||||
|
||||
Future<_TextPayload> _readTextPayload() async {
|
||||
final file = File(widget.path);
|
||||
final size = await file.length();
|
||||
final ext = widget.path.split('.').last.toLowerCase();
|
||||
if (size <= _textViewMaxBytes) {
|
||||
final raw = await file.readAsString();
|
||||
return _TextPayload(content: _maybePrettify(raw, ext), truncated: false);
|
||||
}
|
||||
final raf = await file.open();
|
||||
try {
|
||||
final bytes = await raf.read(_textViewMaxBytes);
|
||||
// Truncated payloads stay raw — a parser would choke on the dangling tail.
|
||||
return _TextPayload(
|
||||
content: utf8.decode(bytes, allowMalformed: true),
|
||||
truncated: true,
|
||||
);
|
||||
} finally {
|
||||
await raf.close();
|
||||
}
|
||||
}
|
||||
|
||||
/// Falls through to the original text on parse errors.
|
||||
String _maybePrettify(String content, String ext) {
|
||||
if (ext != 'json') return content;
|
||||
try {
|
||||
final parsed = jsonDecode(content);
|
||||
return const JsonEncoder.withIndent(' ').convert(parsed);
|
||||
} on Object {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _ActionDescriptor {
|
||||
@@ -489,7 +470,47 @@ class _ActionDescriptor {
|
||||
}
|
||||
|
||||
class _TextPayload {
|
||||
final String content;
|
||||
final List<String> lines;
|
||||
final bool truncated;
|
||||
const _TextPayload({required this.content, required this.truncated});
|
||||
const _TextPayload({required this.lines, required this.truncated});
|
||||
}
|
||||
|
||||
/// Reads, prettifies (JSON) and splits a text file on a background isolate:
|
||||
/// up to 5 MB of decoding and line splitting would otherwise freeze the UI.
|
||||
Future<_TextPayload> _loadTextPayload((String, int) args) async {
|
||||
final (path, maxBytes) = args;
|
||||
final file = File(path);
|
||||
final size = await file.length();
|
||||
final ext = path.split('.').last.toLowerCase();
|
||||
if (size <= maxBytes) {
|
||||
final raw = await file.readAsString();
|
||||
return _TextPayload(
|
||||
lines: const LineSplitter().convert(_maybePrettify(raw, ext)),
|
||||
truncated: false,
|
||||
);
|
||||
}
|
||||
final raf = await file.open();
|
||||
try {
|
||||
final bytes = await raf.read(maxBytes);
|
||||
// Truncated payloads stay raw — a parser would choke on the dangling tail.
|
||||
return _TextPayload(
|
||||
lines: const LineSplitter().convert(
|
||||
utf8.decode(bytes, allowMalformed: true),
|
||||
),
|
||||
truncated: true,
|
||||
);
|
||||
} finally {
|
||||
await raf.close();
|
||||
}
|
||||
}
|
||||
|
||||
/// Falls through to the original text on parse errors.
|
||||
String _maybePrettify(String content, String ext) {
|
||||
if (ext != 'json') return content;
|
||||
try {
|
||||
final parsed = jsonDecode(content);
|
||||
return const JsonEncoder.withIndent(' ').convert(parsed);
|
||||
} on Object {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user