Skip to content

Commit 5fd4fa2

Browse files
authored
feat(cat-voices): io downloader (#3361)
* chore: adding dependencies * feat: adding permission for android * feat: permission handler factory * feat: exporting models * feat: adding dependencies * fix: android manifest format * feat: adding catalyst operating system * feat: adding catalyst operating system * feat: refactor to used rationale and explanation exception * feat: adding queue * chore: remove android manifest permission * fix: remove white space and sort intl * feat: io downloader implementation * chore: remove comments * fix: spelling * feat: extracting common logic * chore: update logic for downloader class * chore: remove unused mixin * fix: format * chore: review update * chore: set default strategy * chore: remove files * chore: added permission * chore: remove old files
1 parent 5da3d22 commit 5fd4fa2

File tree

14 files changed

+174
-99
lines changed

14 files changed

+174
-99
lines changed

.config/dictionaries/project.dic

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ dockerenv
9999
dockerhub
100100
domcontentloaded
101101
Dominik
102+
downloadsfolder
102103
donts
103104
dotenv
104105
dotenvy

catalyst_voices/apps/voices/ios/Runner/Info.plist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,7 @@
5151
</array>
5252
<key>UIViewControllerBasedStatusBarAppearance</key>
5353
<false/>
54+
<key>UISupportsDocumentBrowser</key>
55+
<true/>
5456
</dict>
5557
</plist>

catalyst_voices/apps/voices/macos/Flutter/GeneratedPluginRegistrant.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import FlutterMacOS
66
import Foundation
77

88
import device_info_plus
9+
import downloadsfolder
910
import file_picker
1011
import file_selector_macos
1112
import flutter_inappwebview_macos
@@ -22,6 +23,7 @@ import webview_flutter_wkwebview
2223

2324
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
2425
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
26+
DownloadsfolderPlugin.register(with: registry.registrar(forPlugin: "DownloadsfolderPlugin"))
2527
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
2628
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
2729
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))

catalyst_voices/apps/voices/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ dependencies:
3535
device_info_plus: ^11.5.0
3636
dotted_border: ^3.1.0
3737
equatable: ^2.0.7
38-
file_picker: ^10.2.1
38+
file_picker: ^10.3.2
3939
flutter:
4040
sdk: flutter
4141
flutter_bloc: ^9.1.1

catalyst_voices/melos.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,14 +99,15 @@ command:
9999
collection: ^1.19.1
100100
cryptography: ^2.7.0
101101
device_info_plus: ^11.5.0
102+
downloadsfolder: ^1.1.0
102103
dotted_border: ^3.1.0
103104
ed25519_hd_key: ^2.3.0
104105
email_validator: ^3.0.0
105106
equatable: ^2.0.7
106107
fake_async: ^1.3.3
107108
ffi: ^2.1.4
108109
ffigen: ^19.1.0
109-
file_picker: ^10.2.1
110+
file_picker: ^10.3.2
110111
flutter_bloc: ^9.1.1
111112
flutter_dropzone: ^4.2.1
112113
flutter_inappwebview: ^6.1.5

catalyst_voices/packages/internal/catalyst_voices_models/lib/src/config/app_environment.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,7 @@ final class AppEnvironment extends Equatable {
4646
factory AppEnvironment.fromEnv() {
4747
String? envName;
4848

49-
// TODO(LynxLynxx): Change to CatalystPlatform when its refactored
50-
if (kIsWeb) {
49+
if (CatalystPlatform.isWeb) {
5150
final envVars = getDartEnvVars();
5251
envName = envVars.envName;
5352
}
Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,82 @@
11
import 'dart:async';
22
import 'dart:typed_data';
33

4-
import 'package:catalyst_voices_services/src/downloader/utils/downloader.dart';
4+
import 'package:catalyst_voices_models/catalyst_voices_models.dart';
5+
import 'package:catalyst_voices_services/src/downloader/utils/file_save_strategy.dart';
6+
import 'package:catalyst_voices_shared/catalyst_voices_shared.dart';
7+
import 'package:downloadsfolder/downloadsfolder.dart';
8+
import 'package:path/path.dart' as p;
59

610
// ignore: one_member_abstracts
711
abstract interface class DownloaderService {
8-
static const bytesMimeType = 'application/octet-stream';
9-
1012
const factory DownloaderService() = DownloaderServiceImpl;
1113

12-
Future<void> download({
14+
Future<String?> download({
1315
required Uint8List data,
1416
required String filename,
15-
String mimeType = DownloaderService.bytesMimeType,
1617
});
1718
}
1819

1920
final class DownloaderServiceImpl implements DownloaderService {
2021
const DownloaderServiceImpl();
2122

22-
Downloader get _downloader => Downloader();
23-
2423
@override
25-
Future<void> download({
24+
Future<String?> download({
2625
required Uint8List data,
2726
required String filename,
28-
String mimeType = DownloaderService.bytesMimeType,
2927
}) async {
30-
await _downloader.download(
31-
data,
32-
path: Uri(path: filename),
33-
mimeType: mimeType,
28+
final envType = AppEnvironment.fromEnv().type;
29+
final strategyType = FileSaveStrategyFactory.getDefaultStrategyType();
30+
final strategy = FileSaveStrategyFactory.getStrategy(type: strategyType);
31+
32+
final fileUri = await _buildSaveUri(filename, strategyType, envType);
33+
34+
return strategy.saveFile(
35+
data: data,
36+
fileUri: fileUri,
3437
);
3538
}
39+
40+
Future<Uri> _buildSaveUri(
41+
String filename,
42+
FileSaveStrategyType strategyType,
43+
AppEnvironmentType envType,
44+
) async {
45+
final flavorName = _getFlavorName(strategyType, envType);
46+
final fileWithoutExtension = p.withoutExtension(filename);
47+
final extensionName = p.extension(filename);
48+
49+
final downloadPath = await _getDownloadPathIfNeeded(strategyType);
50+
final uniqueFilename = '$fileWithoutExtension$flavorName$extensionName';
51+
52+
return Uri.file(
53+
downloadPath != null ? '$downloadPath/$uniqueFilename' : uniqueFilename,
54+
windows: false,
55+
);
56+
}
57+
58+
Future<String?> _getDownloadPathIfNeeded(FileSaveStrategyType strategyType) async {
59+
if (strategyType == FileSaveStrategyType.downloadsDirectory) {
60+
final downloadDir = await getDownloadDirectory();
61+
return downloadDir.path;
62+
}
63+
return null;
64+
}
65+
66+
String _getFlavorName(FileSaveStrategyType strategyType, AppEnvironmentType envType) {
67+
if (strategyType == FileSaveStrategyType.filePicker) {
68+
return envType.fileFlavorName;
69+
} else {
70+
// For downloads directory, skip flavor on iOS
71+
return CatalystOperatingSystem.current.isIOS ? '' : envType.fileFlavorName;
72+
}
73+
}
74+
}
75+
76+
extension on AppEnvironmentType {
77+
String get fileFlavorName => switch (this) {
78+
AppEnvironmentType.dev => '_dev',
79+
AppEnvironmentType.preprod => '_preprod',
80+
_ => '',
81+
};
3682
}

catalyst_voices/packages/internal/catalyst_voices_services/lib/src/downloader/utils/downloader.dart

Lines changed: 0 additions & 20 deletions
This file was deleted.

catalyst_voices/packages/internal/catalyst_voices_services/lib/src/downloader/utils/downloader_stub.dart

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import 'dart:io';
2+
import 'dart:typed_data';
3+
4+
import 'package:catalyst_voices_shared/catalyst_voices_shared.dart';
5+
import 'package:file_picker/file_picker.dart';
6+
import 'package:flutter/services.dart';
7+
import 'package:path/path.dart' as p;
8+
9+
final _loggerDownloadsDirectory = Logger('DownloadsDirectorySaveStrategy');
10+
final _loggerFilePicker = Logger('FilePickerSaveStrategy');
11+
12+
final class DownloadsDirectorySaveStrategy implements FileSaveStrategy {
13+
const DownloadsDirectorySaveStrategy();
14+
15+
@override
16+
Future<String?> saveFile({
17+
required Uint8List data,
18+
required Uri fileUri,
19+
}) async {
20+
try {
21+
var file = File.fromUri(fileUri);
22+
23+
// If file exists, add numbers like web browsers do
24+
var counter = 1;
25+
while (file.existsSync()) {
26+
final pathWithoutExt = p.withoutExtension(fileUri.path);
27+
final extension = p.extension(fileUri.path);
28+
final newPath = '$pathWithoutExt ($counter)$extension';
29+
final newUri = Uri.file(newPath, windows: false);
30+
file = File.fromUri(newUri);
31+
counter++;
32+
}
33+
34+
await file.writeAsBytes(data);
35+
return file.path;
36+
} catch (e) {
37+
_loggerDownloadsDirectory.severe('Error saving file to downloads directory', e);
38+
rethrow;
39+
}
40+
}
41+
}
42+
43+
final class FilePickerSaveStrategy implements FileSaveStrategy {
44+
const FilePickerSaveStrategy();
45+
46+
@override
47+
Future<String?> saveFile({
48+
required Uint8List data,
49+
required Uri fileUri,
50+
}) async {
51+
try {
52+
final fileName = fileUri.path;
53+
54+
await FilePicker.platform.saveFile(
55+
fileName: fileName,
56+
bytes: data,
57+
);
58+
return null;
59+
} catch (e) {
60+
_loggerFilePicker.severe('Error saving file with FilePicker', e);
61+
rethrow;
62+
}
63+
}
64+
}
65+
66+
/// Abstract interface for file saving strategies
67+
// ignore: one_member_abstracts
68+
abstract interface class FileSaveStrategy {
69+
const FileSaveStrategy();
70+
71+
/// Saves the file represented by the [data] bytes with the given [fileUri].
72+
/// Returns the path where the file was saved.
73+
Future<String?> saveFile({
74+
required Uint8List data,
75+
required Uri fileUri,
76+
});
77+
}
78+
79+
/// Factory to get the appropriate file save strategy based on platform or preference
80+
class FileSaveStrategyFactory {
81+
const FileSaveStrategyFactory();
82+
83+
/// Returns the default strategy for the current platform
84+
static FileSaveStrategyType getDefaultStrategyType() {
85+
return switch (CatalystOperatingSystem.current) {
86+
_ when CatalystPlatform.isWeb || CatalystOperatingSystem.current.isIOS =>
87+
FileSaveStrategyType.filePicker,
88+
_ => FileSaveStrategyType.downloadsDirectory,
89+
};
90+
}
91+
92+
static FileSaveStrategy getStrategy({required FileSaveStrategyType type}) {
93+
return switch (type) {
94+
FileSaveStrategyType.filePicker => const FilePickerSaveStrategy(),
95+
FileSaveStrategyType.downloadsDirectory => const DownloadsDirectorySaveStrategy(),
96+
};
97+
}
98+
}
99+
100+
enum FileSaveStrategyType {
101+
filePicker,
102+
downloadsDirectory,
103+
}

0 commit comments

Comments
 (0)