Download Files

This commit is contained in:
Abhishek Shroff
2024-09-12 15:59:52 +05:30
parent 0d1fc7f0f2
commit 39091168c6
7 changed files with 201 additions and 13 deletions

View File

@@ -3,6 +3,7 @@ import 'package:flutter_state_notifier/flutter_state_notifier.dart';
import 'package:go_router/go_router.dart';
import 'package:offtheline/offtheline.dart';
import 'package:phylum/app_shortcuts.dart';
import 'package:phylum/integrations/download_manager.dart';
import 'package:phylum/libphylum/phylum_account.dart';
import 'package:phylum/ui/app/app_layout.dart';
import 'package:phylum/ui/app/nav_forward.dart';
@@ -28,6 +29,7 @@ class _PhylumAppState extends State<PhylumApp> {
Provider.value(value: widget.account),
StateNotifierProvider<ApiActionQueue, ApiActionQueueState>.value(key: ValueKey(widget.account.id), value: widget.account.actionQueue),
StateNotifierProvider<NavForwardManager, NavForwardState>.value(key: ValueKey(widget.account.id), value: historyManager),
StateNotifierProvider<DownloadManager, DownloadManagerState>(create: (context) => DownloadManager(widget.account))
],
child: MaterialApp.router(
key: ValueKey(widget.account),

View File

@@ -0,0 +1,38 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';
class PhylumDirectories {
late final Directory? tmp;
late final Directory? downloads;
static final PhylumDirectories instance = PhylumDirectories._();
PhylumDirectories._();
Future<void> initialize() async {
if (kIsWeb) return;
final tmpDirectory = await getTemporaryDirectory();
tmpDirectory.create(recursive: true);
if (!tmpDirectory.existsSync()) {
throw 'Unable to create temporary directory';
}
tmp = tmpDirectory;
// final downloadsDirectory = switch() {
// }
final downloadsDirectory = await (Platform.isLinux || Platform.isWindows || Platform.isMacOS || Platform.isFuchsia
? getDownloadsDirectory()
: Platform.isAndroid
? getExternalStorageDirectory()
: getApplicationDocumentsDirectory());
downloadsDirectory!.createSync(recursive: true);
if (!downloadsDirectory.existsSync()) {
throw 'Unable to create temporary directory';
}
downloads = downloadsDirectory;
}
}

View File

@@ -0,0 +1,130 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:path/path.dart' as p;
import 'package:phylum/integrations/directories.dart';
import 'package:phylum/libphylum/db/db.dart';
import 'package:phylum/libphylum/phylum_account.dart';
import 'package:phylum/libphylum/requests/resource_contents_request.dart';
import 'package:state_notifier/state_notifier.dart';
class DownloadManagerState {
final List<DownloadTask> tasks;
const DownloadManagerState([this.tasks = const []]);
}
class DownloadManager extends StateNotifier<DownloadManagerState> {
final PhylumAccount account;
DownloadManager(this.account) : super(const DownloadManagerState());
void downloadResource(Resource r) async {
final output = _createDownloadFile(PhylumDirectories.instance.downloads!, r.name);
if (output == null) {
return;
}
final task = DownloadTask(resourceId: r.id, resourceName: r.name, savePath: output.path, contentLength: r.size);
state = DownloadManagerState([...state.tasks, task]);
IOSink? sink;
sink = output.openWrite();
final response = await account.api.sendRequestRaw(ResourceContentsRequest(r.id));
if (response == null) return;
final length = response.contentLength ?? r.size;
int received = 0;
response.stream
.map((s) {
received += s.length;
task._status.value = DownloadStatusRunning(received, length);
return s;
})
.pipe(sink)
.whenComplete(() => task._status.value = const DownloadStatusFinished())
.onError((err, _) {
task._status.value = DownloadStatusError(err.toString());
return null;
});
l() {
final status = task._status.value;
debugPrint(status.toString());
if (status is DownloadStatusError || status is DownloadStatusFinished) {
task._status.removeListener(l);
}
}
task._status.addListener(l);
}
File? _createDownloadFile(Directory dir, String name) {
final file = File(p.join(dir.path, name));
if (!file.existsSync()) {
file.createSync();
return file;
}
final basename = p.basename(name);
final ext = p.extension(name);
for (int i = 1; i < 1000; i++) {
final file = File(p.join(dir.path, '$basename ($i).$ext'));
if (!file.existsSync()) {
file.createSync();
return file;
}
}
return null;
}
}
class DownloadTask {
final String resourceId;
final String resourceName;
final String savePath;
final int contentLength;
final ValueNotifier<DownloadStatus> _status = ValueNotifier(const DownloadStatusEnqueued());
ValueListenable<DownloadStatus> get status => _status;
DownloadTask({
required this.resourceId,
required this.resourceName,
required this.savePath,
required this.contentLength,
});
}
sealed class DownloadStatus {
const DownloadStatus();
}
class DownloadStatusEnqueued extends DownloadStatus {
const DownloadStatusEnqueued();
@override
String toString() => "Download Enqueued";
}
class DownloadStatusRunning extends DownloadStatus {
final int received;
final int total;
const DownloadStatusRunning(this.received, this.total);
@override
String toString() => "Download Running: $received / $total (${received / total * 100} %)";
}
class DownloadStatusFinished extends DownloadStatus {
const DownloadStatusFinished();
@override
String toString() => "Download Finished";
}
class DownloadStatusError extends DownloadStatus {
final String error;
const DownloadStatusError(this.error);
@override
String toString() => "Download Error: $error";
}

View File

@@ -10,6 +10,7 @@ import 'package:offtheline/offtheline.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'package:phylum/app.dart';
import 'package:phylum/integrations/directories.dart';
import 'package:phylum/libphylum/actions/deserializers.dart';
import 'package:phylum/libphylum/db/db.dart';
import 'package:phylum/libphylum/phylum_account.dart';
@@ -21,6 +22,7 @@ const storageDir = String.fromEnvironment("STORAGE_DIR");
void main() async {
WidgetsFlutterBinding.ensureInitialized();
PhylumDirectories.instance.initialize();
OTL.logger = Logger(level: Level.info);
GoRouter.optionURLReflectsImperativeAPIs = true;

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:phylum/integrations/download_manager.dart';
import 'package:phylum/libphylum/db/db.dart';
import 'package:phylum/libphylum/phylum_account.dart';
import 'package:provider/provider.dart';
@@ -33,7 +34,7 @@ class _ResourcePreviewState extends State<ResourcePreview> {
@override
Widget build(BuildContext context) {
final resource = this.resource;
final r = resource;
return Theme(
data: ThemeData.dark(),
child: Scaffold(
@@ -42,21 +43,27 @@ class _ResourcePreviewState extends State<ResourcePreview> {
leading: CloseButton(
onPressed: () => Navigator.of(context).pop(),
),
actions: [IconButton(onPressed: () {}, icon: const Icon(Icons.download))],
title: (resource != null) ? Text(resource.name) : null,
actions: [IconButton(onPressed: (r == null || r.dir) ? null : () => downloadResource(r), icon: const Icon(Icons.download))],
title: (r != null) ? Text(r.name) : null,
backgroundColor: Colors.transparent,
),
body: Center(
child: loading
? const CircularProgressIndicator()
: resource == null
: r == null
? const Text('Error loading details', style: TextStyle(fontSize: 18))
: buildPreview(resource)),
: buildPreview(r)),
),
);
}
Widget buildPreview(Resource r) {
if (r.dir) {
return const Text(
'Cannot preview directory',
style: TextStyle(fontSize: 18),
);
}
if (r.size > maxPreviewSize) {
return const Text(
'File too large to preview',
@@ -79,4 +86,8 @@ class _ResourcePreviewState extends State<ResourcePreview> {
),
);
}
void downloadResource(Resource r) async {
context.read<DownloadManager>().downloadResource(r);
}
}

View File

@@ -576,12 +576,18 @@ packages:
offtheline:
dependency: "direct main"
description:
path: "."
ref: "7e4457c8e7f86662cd6d8424383423ceec7c06bb"
resolved-ref: "7e4457c8e7f86662cd6d8424383423ceec7c06bb"
url: "https://codeberg.org/shroff/offtheline.git"
source: git
path: "../../offtheline"
relative: true
source: path
version: "0.13.0"
open_file:
dependency: "direct main"
description:
name: open_file
sha256: a5a32d44acb7c899987d0999e1e3cbb0a0f1adebbf41ac813ec6d2d8faa0af20
url: "https://pub.dev"
source: hosted
version: "3.3.2"
package_config:
dependency: transitive
description:

View File

@@ -19,9 +19,8 @@ dependencies:
logger:
mime:
offtheline:
git:
url: https://codeberg.org/shroff/offtheline.git
ref: 7e4457c8e7f86662cd6d8424383423ceec7c06bb
path: ../../offtheline
open_file:
path:
path_provider:
provider: