Skip to content

Commit

Permalink
Implement rclone download
Browse files Browse the repository at this point in the history
  • Loading branch information
dhzdhd committed Jul 1, 2024
1 parent 0c2f147 commit a5a5ee4
Show file tree
Hide file tree
Showing 7 changed files with 215 additions and 49 deletions.
2 changes: 1 addition & 1 deletion lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ void main() async {
final systemTray = SystemTray();

await systemTray.initSystemTray(
title: 'Sync Vault',
title: 'SyncVault',
iconPath: 'assets/icons/icon.ico',
);

Expand Down
4 changes: 1 addition & 3 deletions lib/src/app.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
Expand Down Expand Up @@ -48,7 +46,7 @@ class MyApp extends ConsumerWidget {
HomeView.routeName => const HomeView(),
AccountView.routeName => const AccountView(),
IntroductionView.routeName => const IntroductionView(),
_ => !introSettings.alreadyViewed && Platform.isAndroid
_ => !introSettings.alreadyViewed
? const IntroductionView()
: const HomeView()
};
Expand Down
37 changes: 37 additions & 0 deletions lib/src/introduction/controllers/intro_controller.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,42 @@
import 'package:fpdart/fpdart.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:syncvault/errors.dart';
import 'package:syncvault/src/introduction/models/intro_model.dart';
import 'package:syncvault/src/introduction/services/intro_service.dart';

part 'intro_controller.g.dart';

// @riverpod
// Stream<Either<AppError, int>> rCloneDownloadController(
// RCloneDownloadControllerRef ref,
// ) async* {
// final introSettingsNotifier = ref.read(introSettingsProvider.notifier);
// final stream = introSettingsNotifier.downloadRClone();

// await for (final progress in stream) {
// yield progress;
// }
// }

@riverpod
class RCloneDownloadController extends _$RCloneDownloadController {
@override
FutureOr<int> build() => Future.value(0);

Future<void> rCloneDownload() async {
final introSettingsNotifier = ref.read(introSettingsProvider.notifier);
final stream = introSettingsNotifier.downloadRClone();

state = const AsyncLoading();
stream.listen((data) {
state = data.match(
(l) => AsyncError(l, StackTrace.current),
(r) => AsyncData(r),
);
});
}
}

@riverpod
class IntroSettings extends _$IntroSettings {
final _service = IntroService();
Expand All @@ -16,4 +49,8 @@ class IntroSettings extends _$IntroSettings {
void setAlreadyViewed() {
// _service.put()
}

Stream<Either<AppError, int>> downloadRClone() async* {
yield* _service.downloadRClone();
}
}
19 changes: 18 additions & 1 deletion lib/src/introduction/controllers/intro_controller.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 49 additions & 0 deletions lib/src/introduction/services/intro_service.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:dio/dio.dart';
import 'package:fpdart/fpdart.dart';
import 'package:get_it/get_it.dart';
import 'package:hive/hive.dart';
import 'package:path_provider/path_provider.dart';
import 'package:syncvault/errors.dart';
import 'package:syncvault/log.dart';
import 'package:syncvault/src/introduction/models/intro_model.dart';

class IntroService {
final _dio = GetIt.I<Dio>();
static const androidRCloneUrl =
'https://beta.rclone.org/v1.67.0/testbuilds/rclone-android-21-armv8a.gz';
static const windowsRCloneUrl =
'https://downloads.rclone.org/v1.67.0/rclone-v1.67.0-windows-amd64.zip';

IntroSettingsModel fetch() {
const defaultValue = IntroSettingsModel(alreadyViewed: false);

Expand All @@ -24,4 +37,40 @@ class IntroService {
return defaultValue;
}
}

Stream<Either<AppError, int>> downloadRClone() async* {
final url = switch (Platform.operatingSystem) {
'windows' => windowsRCloneUrl,
'android' => androidRCloneUrl,
_ => '',
};

final progressStreamController = StreamController<int>();

final dir = await getApplicationDocumentsDirectory();
final path = '${dir.path}/SyncVault/RClone.zip';

try {
_dio.download(
url,
path,
onReceiveProgress: (received, total) {
progressStreamController.add(((received / total) * 100).round());
},
options: Options(
responseType: ResponseType.bytes,
followRedirects: false,
validateStatus: (status) {
return status! < 500;
},
),
);

yield* progressStreamController.stream.map((val) => Right(val));
} catch (err) {
yield Left(err.segregateError());
} finally {
progressStreamController.close();
}
}
}
151 changes: 108 additions & 43 deletions lib/src/introduction/views/intro_view.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:introduction_screen/introduction_screen.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:syncvault/src/introduction/controllers/intro_controller.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:syncvault/helpers.dart';
import 'package:syncvault/errors.dart';

class IntroductionView extends StatefulHookConsumerWidget {
const IntroductionView({
Expand All @@ -17,6 +23,19 @@ class IntroductionView extends StatefulHookConsumerWidget {
class _IntroductionViewState extends ConsumerState<IntroductionView> {
@override
Widget build(BuildContext context) {
final rCloneDownloadProgress = ref.watch(rCloneDownloadControllerProvider);
final rCloneDownloadControllerNotifier =
ref.read(rCloneDownloadControllerProvider.notifier);

ref.listen<AsyncValue>(
rCloneDownloadControllerProvider,
(prev, state) {
if (!state.isLoading && state.hasError) {
context.showErrorSnackBar(state.error!.segregateError().message);
}
},
);

return Scaffold(
body: IntroductionScreen(
next: const Text('Next'),
Expand All @@ -38,59 +57,105 @@ class _IntroductionViewState extends ConsumerState<IntroductionView> {
child: Icon(Icons.waving_hand, size: 50.0),
),
),
PageViewModel(
title: 'Folder permissions',
body: 'SyncVault requires access to all files to operate normally.',
image: const Center(
child: Icon(Icons.folder, size: 50.0),
),
footer: Center(
child: FilledButton(
onPressed: () async {
await Permission.manageExternalStorage
.onDeniedCallback(() {})
.onGrantedCallback(() {})
.request();
},
child: const Text('Give permissions'),
if (Platform.isAndroid)
PageViewModel(
title: 'Folder permissions',
body:
'SyncVault requires access to all files to operate normally.',
image: const Center(
child: Icon(Icons.folder, size: 50.0),
),
footer: Center(
child: FilledButton(
onPressed: () async {
await Permission.manageExternalStorage
.onDeniedCallback(() {})
.onGrantedCallback(() {})
.request();
},
child: const Text('Give permissions'),
),
),
),
),
PageViewModel(
title: 'Battery permissions',
body:
'SyncVault requires you to remove battery optimizations to operate normally.',
image: const Center(
child: Icon(Icons.battery_saver, size: 50.0),
if (Platform.isAndroid)
PageViewModel(
title: 'Battery permissions',
body:
'SyncVault requires you to remove battery optimizations to operate normally.',
image: const Center(
child: Icon(Icons.battery_saver, size: 50.0),
),
footer: Center(
child: FilledButton(
onPressed: () async {
await Permission.ignoreBatteryOptimizations
.onDeniedCallback(() {})
.onGrantedCallback(() {})
.request();
},
child: const Text('Give permissions'),
),
),
),
footer: Center(
child: FilledButton(
onPressed: () async {
await Permission.ignoreBatteryOptimizations
.onDeniedCallback(() {})
.onGrantedCallback(() {})
.request();
},
child: const Text('Give permissions'),
if (Platform.isAndroid)
PageViewModel(
title: 'Notification permissions',
body:
'SyncVault requires notification permissions to notify you during sync.',
image: const Center(
child: Icon(Icons.notifications, size: 50.0),
),
footer: Center(
child: FilledButton(
onPressed: () async {
await Permission.notification
.onDeniedCallback(() {})
.onGrantedCallback(() {})
.request();
},
child: const Text('Give permissions'),
),
),
),
),
PageViewModel(
title: 'Notification permissions',
body:
'SyncVault requires notification permissions to notify you during sync.',
title: 'Download RClone',
bodyWidget: Column(
children: [
const Text(
'SyncVault requires you to download RClone which serves as the backend of the app.\nRClone is an open source software that syncs your files to cloud storage.',
textAlign: TextAlign.center,
),
TextButton(
onPressed: () async {
final res = await launchUrlString(
'https://github.com/rclone/rclone');
if (!res && context.mounted) {
context.showErrorSnackBar('Failed to open URL');
}
},
child: const Text('RClone Github page'),
)
],
),
image: const Center(
child: Icon(Icons.notifications, size: 50.0),
child: Icon(Icons.download, size: 50.0),
),
footer: Center(
child: FilledButton(
onPressed: () async {
await Permission.notification
.onDeniedCallback(() {})
.onGrantedCallback(() {})
.request();
},
child: const Text('Give permissions'),
onPressed: (rCloneDownloadProgress.isLoading ||
(rCloneDownloadProgress.value != null &&
rCloneDownloadProgress.value != 0))
? null
: () async {
await rCloneDownloadControllerNotifier.rCloneDownload();
},
child: Text(rCloneDownloadProgress.isLoading ||
rCloneDownloadProgress.value != null &&
rCloneDownloadProgress.value != 0
? rCloneDownloadProgress.value == 100
? 'Complete'
: 'Progress ${rCloneDownloadProgress.value ?? 0}%'
: 'Download RClone'),
),
),
)
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ dependencies:
watcher: ^1.1.0
window_manager: ^0.3.7
sentry_flutter: ^8.2.0
url_launcher: ^6.2.3
url_launcher: ^6.3.0
workmanager: ^0.5.2
permission_handler: ^11.3.1
flutter_dotenv: ^5.1.0
Expand Down

0 comments on commit a5a5ee4

Please sign in to comment.