diff --git a/README.md b/README.md index b0527cf..c860a5e 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,9 @@ - ruTorrent Mobile - - - # ruTorrent Mobile [Stacked Architecture](https://pub.dev/packages/stacked) Migration ## Completion + drawing ## **A ruTorrent-based client built with Flutter** @@ -15,14 +12,13 @@ The project is a flutter application for ruTorrent web interface. The app commun Additionally, you can also stream torrents from your server (or seedbox) and download them locally to your mobile device (a save offline feature), which makes torrenting a seamless experience for ruTorrent users. - ## ruTorrent and rtorrent ruTorrent Web [ruTorrent](https://github.com/Novik/ruTorrent) is the most popular web interface for [rtorrent](https://github.com/rakshasa/rtorrent), which is possibly the most used BitTorrent client in Linux. It is mostly a web application, but it has its own backend that connects to rtorrent. -In short: +In short: rtorrent ⇒ The BitTorrent client, a console-based tool that also has an API to interact with it. @@ -50,8 +46,8 @@ You can learn more about seedbox [here](https://en.wikipedia.org/wiki/Seedbox). ## Some Prerequisites -* Flutter SDK Version (dependency compatibility) : >=2.0.4 -* Minimum deployment target in iOS : 10 +- Flutter SDK Version (dependency compatibility) : >=2.0.4 +- Minimum deployment target in iOS : 10 ## Getting Started @@ -74,9 +70,10 @@ flutter doctor ``` 4. For IOS -- Uncomment ```platform :ios, '9.0'``` from ios/Podfile -- Cd into the new ios directory```cd ios``` -- From the ios directory ```pod install --verbose``` + +- Uncomment `platform :ios, '9.0'` from ios/Podfile +- Cd into the new ios directory`cd ios` +- From the ios directory `pod install --verbose` 5. Run the app: @@ -106,9 +103,10 @@ refactor: Regular code refactoring and maintenance ``` ## Community + You may join the gsoc-rutorrent channel of CCExtractor community through slack and propose improvements to the project. -* CCExtractor Development on [Slack](https://ccextractor.org/public:general:support?) +- CCExtractor Development on [Slack](https://ccextractor.org/public:general:support?) ## License diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml index db0026a..5408f7b 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -1,5 +1,4 @@ - + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index eca2757..9c436e1 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - + - + - + @@ -48,8 +41,6 @@ - + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml index 304732f..9953e20 100644 --- a/android/app/src/main/res/drawable/launch_background.xml +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -4,9 +4,9 @@ - + android:gravity="fill_horizontal|fill_vertical" + android:src="@drawable/logo" /> + diff --git a/android/app/src/main/res/drawable/logo.png b/android/app/src/main/res/drawable/logo.png new file mode 100644 index 0000000..cdbe3b7 Binary files /dev/null and b/android/app/src/main/res/drawable/logo.png differ diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml index bc95c32..078c0b8 100644 --- a/android/app/src/profile/AndroidManifest.xml +++ b/android/app/src/profile/AndroidManifest.xml @@ -1,5 +1,4 @@ - + diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 6b4c0f7..a161118 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -1,26 +1,26 @@ - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - MinimumOSVersion - 8.0 - + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 8.0 + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist index 18d9810..9b59d84 100644 --- a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -1,8 +1,8 @@ - - IDEDidComputeMac32BitWarning - - + + IDEDidComputeMac32BitWarning + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings index f9b0d7c..a8acefa 100644 --- a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -1,8 +1,8 @@ - - PreviewsEnabled - - + + PreviewsEnabled + + diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist index 18d9810..9b59d84 100644 --- a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -1,8 +1,8 @@ - - IDEDidComputeMac32BitWarning - - + + IDEDidComputeMac32BitWarning + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings index f9b0d7c..a8acefa 100644 --- a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -1,8 +1,8 @@ - - PreviewsEnabled - - + + PreviewsEnabled + + diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index 73d3b7f..e342e1a 100644 --- a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1 +1,100 @@ -{"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"}]} \ No newline at end of file +{ + "images": [ + { + "size": "60x60", + "expected-size": "180", + "filename": "180.png", + "folder": "Assets.xcassets/AppIcon.appiconset/", + "idiom": "iphone", + "scale": "3x" + }, + { + "size": "40x40", + "expected-size": "80", + "filename": "80.png", + "folder": "Assets.xcassets/AppIcon.appiconset/", + "idiom": "iphone", + "scale": "2x" + }, + { + "size": "40x40", + "expected-size": "120", + "filename": "120.png", + "folder": "Assets.xcassets/AppIcon.appiconset/", + "idiom": "iphone", + "scale": "3x" + }, + { + "size": "60x60", + "expected-size": "120", + "filename": "120.png", + "folder": "Assets.xcassets/AppIcon.appiconset/", + "idiom": "iphone", + "scale": "2x" + }, + { + "size": "57x57", + "expected-size": "57", + "filename": "57.png", + "folder": "Assets.xcassets/AppIcon.appiconset/", + "idiom": "iphone", + "scale": "1x" + }, + { + "size": "29x29", + "expected-size": "58", + "filename": "58.png", + "folder": "Assets.xcassets/AppIcon.appiconset/", + "idiom": "iphone", + "scale": "2x" + }, + { + "size": "29x29", + "expected-size": "29", + "filename": "29.png", + "folder": "Assets.xcassets/AppIcon.appiconset/", + "idiom": "iphone", + "scale": "1x" + }, + { + "size": "29x29", + "expected-size": "87", + "filename": "87.png", + "folder": "Assets.xcassets/AppIcon.appiconset/", + "idiom": "iphone", + "scale": "3x" + }, + { + "size": "57x57", + "expected-size": "114", + "filename": "114.png", + "folder": "Assets.xcassets/AppIcon.appiconset/", + "idiom": "iphone", + "scale": "2x" + }, + { + "size": "20x20", + "expected-size": "40", + "filename": "40.png", + "folder": "Assets.xcassets/AppIcon.appiconset/", + "idiom": "iphone", + "scale": "2x" + }, + { + "size": "20x20", + "expected-size": "60", + "filename": "60.png", + "folder": "Assets.xcassets/AppIcon.appiconset/", + "idiom": "iphone", + "scale": "3x" + }, + { + "size": "1024x1024", + "filename": "1024.png", + "expected-size": "1024", + "idiom": "ios-marketing", + "folder": "Assets.xcassets/AppIcon.appiconset/", + "scale": "1x" + } + ] +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json index 0bedcf2..781d7cd 100644 --- a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -1,23 +1,23 @@ { - "images" : [ + "images": [ { - "idiom" : "universal", - "filename" : "LaunchImage.png", - "scale" : "1x" + "idiom": "universal", + "filename": "LaunchImage.png", + "scale": "1x" }, { - "idiom" : "universal", - "filename" : "LaunchImage@2x.png", - "scale" : "2x" + "idiom": "universal", + "filename": "LaunchImage@2x.png", + "scale": "2x" }, { - "idiom" : "universal", - "filename" : "LaunchImage@3x.png", - "scale" : "3x" + "idiom": "universal", + "filename": "LaunchImage@3x.png", + "scale": "3x" } ], - "info" : { - "version" : 1, - "author" : "xcode" + "info": { + "version": 1, + "author": "xcode" } } diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md index 89c2725..b5b843a 100644 --- a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -2,4 +2,4 @@ You can customize the launch screen with your own desired assets by replacing the image files in this directory. -You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 62d9dfe..e482581 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -1,50 +1,50 @@ - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - rutorrent - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - LSApplicationQueriesSchemes - - https - http - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + rutorrent + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + LSApplicationQueriesSchemes + + https + http + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + diff --git a/lib/app/app.dart b/lib/app/app.dart index 655f113..36deedc 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -1,22 +1,24 @@ import 'package:rutorrentflutter/services/api/dev_api_service.dart'; import 'package:rutorrentflutter/services/api/i_api_service.dart'; -import 'package:rutorrentflutter/theme/app_state_notifier.dart'; import 'package:rutorrentflutter/services/api/prod_api_service.dart'; import 'package:rutorrentflutter/services/functional_services/authentication_service.dart'; import 'package:rutorrentflutter/services/functional_services/disk_space_service.dart'; import 'package:rutorrentflutter/services/functional_services/internet_service.dart'; import 'package:rutorrentflutter/services/functional_services/notification_service.dart'; import 'package:rutorrentflutter/services/functional_services/shared_preferences_service.dart'; +import 'package:rutorrentflutter/services/state_services/disk_file_service.dart'; import 'package:rutorrentflutter/services/state_services/file_service.dart'; import 'package:rutorrentflutter/services/state_services/history_service.dart'; import 'package:rutorrentflutter/services/state_services/torrent_service.dart'; import 'package:rutorrentflutter/services/state_services/user_preferences_service.dart'; -import 'package:rutorrentflutter/ui/views/disk_explorer/disk_explorer_view.dart'; +import 'package:rutorrentflutter/theme/app_state_notifier.dart'; import 'package:rutorrentflutter/ui/views/History/history_view.dart'; -import 'package:rutorrentflutter/ui/views/media_player/media_stream_view.dart'; import 'package:rutorrentflutter/ui/views/Settings/settings_view.dart'; +import 'package:rutorrentflutter/ui/views/disk_explorer/disk_explorer_view.dart'; +import 'package:rutorrentflutter/ui/views/media_player/media_stream_view.dart'; import 'package:rutorrentflutter/ui/views/torrent_detail/torrent_detail_view.dart'; import 'package:rutorrentflutter/utils/file_picker_service.dart'; +import 'package:rutorrentflutter/utils/package_info_service.dart'; import 'package:stacked/stacked_annotations.dart'; import 'package:stacked_services/stacked_services.dart'; @@ -61,6 +63,8 @@ import '../ui/views/splash/splash_view.dart'; LazySingleton(classType: BottomSheetService), LazySingleton(classType: FilePickerService), LazySingleton(classType: TorrentService), + LazySingleton(classType: PackageInfoService), + LazySingleton(classType: DiskFileService), ], logger: StackedLogger(), ) diff --git a/lib/app/app.locator.dart b/lib/app/app.locator.dart index 0fd234c..e90c5d5 100644 --- a/lib/app/app.locator.dart +++ b/lib/app/app.locator.dart @@ -18,12 +18,14 @@ import '../services/functional_services/disk_space_service.dart'; import '../services/functional_services/internet_service.dart'; import '../services/functional_services/notification_service.dart'; import '../services/functional_services/shared_preferences_service.dart'; +import '../services/state_services/disk_file_service.dart'; import '../services/state_services/file_service.dart'; import '../services/state_services/history_service.dart'; import '../services/state_services/torrent_service.dart'; import '../services/state_services/user_preferences_service.dart'; import '../theme/app_state_notifier.dart'; import '../utils/file_picker_service.dart'; +import '../utils/package_info_service.dart'; final locator = StackedLocator.instance; @@ -52,4 +54,6 @@ void setupLocator({String? environment, EnvironmentFilter? environmentFilter}) { locator.registerLazySingleton(() => BottomSheetService()); locator.registerLazySingleton(() => FilePickerService()); locator.registerLazySingleton(() => TorrentService()); + locator.registerLazySingleton(() => PackageInfoService()); + locator.registerLazySingleton(() => DiskFileService()); } diff --git a/lib/app/app.router.dart b/lib/app/app.router.dart index 509baca..70648ad 100644 --- a/lib/app/app.router.dart +++ b/lib/app/app.router.dart @@ -10,14 +10,14 @@ import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart'; import '../models/torrent.dart'; -import '../ui/views/disk_explorer/disk_explorer_view.dart'; import '../ui/views/History/history_view.dart'; -import '../ui/views/media_player/media_stream_view.dart'; import '../ui/views/Settings/settings_view.dart'; -import '../ui/views/torrent_detail/torrent_detail_view.dart'; +import '../ui/views/disk_explorer/disk_explorer_view.dart'; import '../ui/views/home/home_view.dart'; import '../ui/views/login/login_view.dart'; +import '../ui/views/media_player/media_stream_view.dart'; import '../ui/views/splash/splash_view.dart'; +import '../ui/views/torrent_detail/torrent_detail_view.dart'; class Routes { static const String splashView = '/'; diff --git a/lib/app/constants.dart b/lib/app/constants.dart index 00792a0..876921c 100644 --- a/lib/app/constants.dart +++ b/lib/app/constants.dart @@ -3,7 +3,7 @@ library app_constants; import 'package:rutorrentflutter/enums/enums.dart'; -Map sortMap = { +Map sortMapTorrentList = { Sort.name_ascending: 'Name - A to Z', Sort.name_descending: 'Name - Z to A', Sort.dateAdded: 'Date Added', @@ -12,6 +12,19 @@ Map sortMap = { Sort.size_descending: 'Size - Large to Small', }; +Map sortMapDiskFileList = { + Sort.name_ascending: 'Name - A to Z', + Sort.name_descending: 'Name - Z to A', +}; + +Map sortMapHistoryList = { + Sort.name_ascending: 'Name - A to Z', + Sort.name_descending: 'Name - Z to A', + Sort.dateAdded: 'Date Added', + Sort.size_ascending: 'Size - Small to Large', + Sort.size_descending: 'Size - Large to Small', +}; + /// APP RELEASE INFO const String BUILD_NUMBER = '2'; const String RELEASE_DATE = '28.02.20'; diff --git a/lib/enums/enums.dart b/lib/enums/enums.dart index 40f86c6..cda6598 100644 --- a/lib/enums/enums.dart +++ b/lib/enums/enums.dart @@ -27,3 +27,9 @@ enum HomeViewBottomSheetMode { Torrent, RSS, } + +enum Screens { + TorrentListViewScreen, + DiskExplorerViewScreen, + TorrentHistoryViewScreen, +} diff --git a/lib/main.dart b/lib/main.dart index a14873f..8a35fd0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,12 +2,12 @@ import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; import 'package:injectable/injectable.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:rutorrentflutter/theme/app_state_notifier.dart'; -import 'package:rutorrentflutter/theme/app_theme.dart'; import 'package:rutorrentflutter/app/app.locator.dart'; import 'package:rutorrentflutter/app/app.router.dart'; import 'package:rutorrentflutter/services/functional_services/notification_service.dart'; import 'package:rutorrentflutter/services/state_services/user_preferences_service.dart'; +import 'package:rutorrentflutter/theme/app_state_notifier.dart'; +import 'package:rutorrentflutter/theme/app_theme.dart'; import 'package:rutorrentflutter/ui/widgets/smart_widgets/bottom_sheets/bottom_sheet_setup.dart'; import 'package:stacked/stacked.dart'; import 'package:stacked_services/stacked_services.dart'; @@ -24,7 +24,7 @@ void main() async { setUpBottomSheetUi(); //Setting up Services locator().init(); - locator().init(); + await locator().init(); runApp(MyApp()); } diff --git a/lib/services/api/dev_api_service.dart b/lib/services/api/dev_api_service.dart index d6561d3..d3171c2 100644 --- a/lib/services/api/dev_api_service.dart +++ b/lib/services/api/dev_api_service.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:io'; + import 'package:fluttertoast/fluttertoast.dart'; import 'package:http/io_client.dart'; import 'package:logger/logger.dart'; @@ -12,9 +13,9 @@ import 'package:rutorrentflutter/models/rss.dart'; import 'package:rutorrentflutter/models/rss_filter.dart'; import 'package:rutorrentflutter/models/torrent.dart'; import 'package:rutorrentflutter/models/torrent_file.dart'; +import 'package:rutorrentflutter/services/api/i_api_service.dart'; import 'package:rutorrentflutter/services/functional_services/authentication_service.dart'; import 'package:rutorrentflutter/services/functional_services/disk_space_service.dart'; -import 'package:rutorrentflutter/services/api/i_api_service.dart'; import 'package:rutorrentflutter/services/mock_data/accounts.dart'; import 'package:rutorrentflutter/services/mock_data/disk_space.dart'; import 'package:rutorrentflutter/services/mock_data/history.dart'; @@ -82,7 +83,7 @@ class DevApiService implements IApiService { return true; } } - Fluttertoast.showToast(msg: 'invalid'); + Fluttertoast.showToast(msg: 'Invalid'); return false; } @@ -407,4 +408,29 @@ class DevApiService implements IApiService { _torrentService!.setTorrentList(torrentsList); return torrentsList; } + + @override + Future> getAllAccountsDiskFiles(String path) { + throw UnimplementedError(); + } + + @override + Future> getAllAccountsHistory({int? lastHours}) { + throw UnimplementedError(); + } + + @override + Future> getAllAccountsRSSFilters() { + throw UnimplementedError(); + } + + @override + Future> loadAllAccountsRSS() { + throw UnimplementedError(); + } + + @override + updateAllAccountsHistory() { + throw UnimplementedError(); + } } diff --git a/lib/services/api/i_api_service.dart b/lib/services/api/i_api_service.dart index bdfc0a0..34c39e2 100644 --- a/lib/services/api/i_api_service.dart +++ b/lib/services/api/i_api_service.dart @@ -49,19 +49,26 @@ abstract class IApiService { Future> getHistory({int? lastHours}); + Future> getAllAccountsHistory({int? lastHours}); + removeHistoryItem(String hashValue); updateHistory(); + updateAllAccountsHistory(); + setTorrentLabel({required String hashValue, required String label}); removeTorrentLabel({required String hashValue}); Future changePassword(int index, String newPassword); - /// Gets Disk Files + /// Fetches Disk Files Future> getDiskFiles(String path); + /// Fetches Disk Files for All Accounts + Future> getAllAccountsDiskFiles(String path); + /// Gets list of files for a particular torrent Future> getFiles(String hashValue); @@ -73,6 +80,9 @@ abstract class IApiService { /// Gets list of saved RSS Feeds Future> loadRSS(); + /// Gets list of saved RSS Feeds + Future> loadAllAccountsRSS(); + /// Removes RSS Feed removeRSS(String hashValue); @@ -85,6 +95,9 @@ abstract class IApiService { /// Gets details of RSS Filters Future> getRSSFilters(); + /// Gets details of RSS Filters for All Accounts + Future> getAllAccountsRSSFilters(); + // ignore: unused_element List? _parseTorrentData(String responseBody, Account? currAccount); } diff --git a/lib/services/api/prod_api_service.dart b/lib/services/api/prod_api_service.dart index b103c2e..7871541 100644 --- a/lib/services/api/prod_api_service.dart +++ b/lib/services/api/prod_api_service.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:io'; + import 'package:fluttertoast/fluttertoast.dart'; import 'package:http/http.dart'; import 'package:http/io_client.dart'; @@ -13,10 +14,11 @@ import 'package:rutorrentflutter/models/rss.dart'; import 'package:rutorrentflutter/models/rss_filter.dart'; import 'package:rutorrentflutter/models/torrent.dart'; import 'package:rutorrentflutter/models/torrent_file.dart'; +import 'package:rutorrentflutter/services/api/i_api_service.dart'; import 'package:rutorrentflutter/services/functional_services/authentication_service.dart'; import 'package:rutorrentflutter/services/functional_services/disk_space_service.dart'; -import 'package:rutorrentflutter/services/api/i_api_service.dart'; import 'package:rutorrentflutter/services/services_info.dart'; +import 'package:rutorrentflutter/services/state_services/disk_file_service.dart'; import 'package:rutorrentflutter/services/state_services/history_service.dart'; import 'package:rutorrentflutter/services/state_services/torrent_service.dart'; import 'package:xml/xml.dart'; @@ -30,6 +32,7 @@ class ProdApiService implements IApiService { DiskSpaceService? _diskSpaceService = locator(); TorrentService? _torrentService = locator(); HistoryService? _historyService = locator(); + DiskFileService _diskFileService = locator(); IOClient get ioClient { /// Url with some issue with their SSL certificates can be trusted explicitly with this @@ -64,16 +67,23 @@ class ProdApiService implements IApiService { get explorerPluginUrl => url + '/plugins/explorer/action.php'; /// Authentication header - Map getAuthHeader() { - return { - 'authorization': 'Basic ' + - base64Encode( - utf8.encode('${account!.username}:${account!.password}')), - }; + Map getAuthHeader({Account? currentAccount}) { + return currentAccount == null + ? { + 'authorization': 'Basic ' + + base64Encode( + utf8.encode('${account!.username}:${account!.password}')), + } + : { + 'authorization': 'Basic ' + + base64Encode(utf8.encode( + '${currentAccount.username}:${currentAccount.password}')), + }; } Future testConnectionAndLogin(Account? account) async { log.v("Testing connection with server"); + log.e(getAuthHeader()); Response? response; var total; try { @@ -81,15 +91,16 @@ class ProdApiService implements IApiService { headers: getAuthHeader()); log.i("Test Connection Response : " + response.body); total = jsonDecode(response.body)['total']; - Fluttertoast.showToast(msg: 'Connected'); } catch (e) { - Fluttertoast.showToast(msg: 'invalid'); + Fluttertoast.showToast(msg: 'Username or Password Incorrect'); return false; } if (total != null && response.statusCode != 200) { Fluttertoast.showToast(msg: 'Something\'s Wrong'); return false; } + + Fluttertoast.showToast(msg: 'Connected'); await _authenticationService!.saveLogin(account); return true; } @@ -104,7 +115,8 @@ class ProdApiService implements IApiService { /// Gets list of torrents for all saved accounts [Apis] Stream> getAllAccountsTorrentList() async* { - log.v("Fetching torrent lists from all accounts"); + log.v( + "Fetching torrent lists from all accounts\n [Will be run every other second]"); List? accounts = _authenticationService!.accounts.value; while (true) { List allTorrentList = []; @@ -131,14 +143,14 @@ class ProdApiService implements IApiService { /// Gets list of torrents for a particular account Stream?> getTorrentList() async* { - log.v("Fetching torrent lists from all accounts"); + log.v( + "Fetching torrent lists from all accounts\n [Will be run every other second]"); while (true) { try { var response = await ioClient .post(Uri.parse(httpRpcPluginUrl), headers: getAuthHeader(), body: { 'mode': 'list', }); - yield _parseTorrentData(response.body, account)!; } catch (e) { print('Exception caught in getTorrentList Api Request ' + e.toString()); @@ -216,6 +228,7 @@ class ProdApiService implements IApiService { } addTorrent(String url) async { + log.v("Adding torrent"); Fluttertoast.showToast(msg: 'Adding torrent'); await ioClient .post(Uri.parse(addTorrentPluginUrl), headers: getAuthHeader(), body: { @@ -224,16 +237,16 @@ class ProdApiService implements IApiService { } addTorrentFile(String torrentPath) async { + log.v("Adding torrent"); Fluttertoast.showToast(msg: 'Adding torrent'); var request = MultipartRequest('POST', Uri.parse(addTorrentPluginUrl)); + request.headers.addAll(getAuthHeader()); // request.fields['label'] = "hell"; request.files .add(await MultipartFile.fromPath('torrent_file', torrentPath)); try { - var response = await request.send(); - - print(response.headers); + await request.send(); } catch (e) { print(e.toString()); } @@ -290,6 +303,38 @@ class ProdApiService implements IApiService { return historyItems; } + /// Gets History for all accounts of last [lastHours] hours + Future> getAllAccountsHistory({int? lastHours}) async { + log.v( + "Fetching history items from server for ${lastHours ?? 'infinite'} hours ago"); + List historyItems = []; + String timestamp = '0'; + if (lastHours != null) { + timestamp = ((DateTime.now().millisecondsSinceEpoch - + Duration(hours: lastHours).inMilliseconds) ~/ + 1000) + .toString(); + } + for (Account? account in accounts) { + var response = await ioClient.post(Uri.parse(historyPluginUrl), + headers: getAuthHeader(currentAccount: account), + body: { + 'cmd': 'get', + 'mark': timestamp, + }); + + var items = jsonDecode(response.body)['items']; + + for (var item in items) { + HistoryItem historyItem = HistoryItem(item['name'], item['action'], + item['action_time'], item['size'], item['hash']); + historyItems.add(historyItem); + } + } + _historyService?.setTorrentHistoryList(historyItems); + return historyItems; + } + removeHistoryItem(String hashValue) async { log.v("Removing history item from server"); Fluttertoast.showToast(msg: 'Removing Torrent from History'); @@ -328,6 +373,32 @@ class ProdApiService implements IApiService { _historyService?.notify(); } + updateAllAccountsHistory() async { + log.v("Updating history items from server"); + List historyItems = []; + String timestamp = ((CustomizableDateTime.current.millisecondsSinceEpoch - + Duration(seconds: 10).inMilliseconds) ~/ + 1000) + .toString(); + for (Account? account in accounts) { + var response = await ioClient.post(Uri.parse(historyPluginUrl), + headers: getAuthHeader(currentAccount: account), + body: { + 'cmd': 'get', + 'mark': timestamp, + }); + + var items = jsonDecode(response.body)['items']; + for (var item in items) { + HistoryItem historyItem = HistoryItem(item['name'], item['action'], + item['action_time'], item['size'], item['hash']); + historyItems.add(historyItem); + } + } + _historyService?.setTorrentHistoryList(historyItems); + _historyService?.notify(); + } + setTorrentLabel({required String hashValue, required String label}) async { log.v( "Setting torrent label with $label for torrent with hashValue $hashValue"); @@ -399,6 +470,7 @@ class ProdApiService implements IApiService { diskFiles.add(diskFile); } + _diskFileService.setDiskFileList(diskFiles); return diskFiles; } on Exception catch (e) { print(e.toString()); @@ -406,6 +478,39 @@ class ProdApiService implements IApiService { } } + /// Gets Disk Files + Future> getAllAccountsDiskFiles(String path) async { + log.v("Fetching Disk Files"); + List diskFiles = []; + for (Account? account in accounts) { + try { + var response = await ioClient.post(Uri.parse(explorerPluginUrl), + headers: getAuthHeader(currentAccount: account), + body: { + 'cmd': 'get', + 'src': path, + }); + + var files = jsonDecode(response.body)['files']; + + for (var file in files) { + DiskFile diskFile = DiskFile(); + + diskFile.isDirectory = file['is_dir']; + diskFile.name = file['data']['name']; + diskFiles.add(diskFile); + } + + _diskFileService.setDiskFileList(diskFiles); + return diskFiles; + } on Exception catch (e) { + print(e.toString()); + return []; + } + } + return []; + } + /// Gets list of files for a particular torrent Future> getFiles(String hashValue) async { log.v("Fetching filles for torrent with hash $hashValue"); @@ -458,6 +563,27 @@ class ProdApiService implements IApiService { return rssLabels; } + /// Gets list of saved RSS Feeds + Future> loadAllAccountsRSS() async { + log.v("Loading RSS"); + List rssLabels = []; + for (Account? account in accounts) { + var rssResponse = await ioClient.post(Uri.parse(rssPluginUrl), + headers: getAuthHeader(currentAccount: account)); + + var feeds = jsonDecode(rssResponse.body)['list']; + for (var label in feeds) { + RSSLabel rssLabel = RSSLabel(label['hash'], label['label']); + for (var item in label['items']) { + RSSItem rssItem = RSSItem(item['title'], item['time'], item['href']); + rssLabel.items.add(rssItem); + } + rssLabels.add(rssLabel); + } + } + return rssLabels; + } + /// Removes RSS Feed removeRSS(String hashValue) async { log.v("Removing RSS"); @@ -537,8 +663,36 @@ class ProdApiService implements IApiService { return rssFilters; } + /// Gets details of RSS Filters + Future> getAllAccountsRSSFilters() async { + log.v("Fetching RSS Filters"); + List rssFilters = []; + + for (Account? account in accounts) { + var response = await ioClient.post(Uri.parse(rssPluginUrl), + headers: getAuthHeader(currentAccount: account), + body: { + 'mode': 'getfilters', + }); + + var filters = jsonDecode(response.body); + for (var filter in filters) { + RSSFilter rssFilter = RSSFilter( + filter['name'], + filter['enabled'], + filter['pattern'], + filter['label'], + filter['exclude'], + filter['dir'], + ); + rssFilters.add(rssFilter); + } + } + return rssFilters; + } + List? _parseTorrentData(String responseBody, Account? currAccount) { - log.v("List of Torrents being parsed"); + // log.v("List of Torrents being parsed"); // takes response and parse and return the torrents data List torrentsList = []; // A list of active torrents is required for changing the connection state from waiting to active diff --git a/lib/services/functional_services/notification_service.dart b/lib/services/functional_services/notification_service.dart index f734f82..945d98c 100644 --- a/lib/services/functional_services/notification_service.dart +++ b/lib/services/functional_services/notification_service.dart @@ -17,7 +17,7 @@ class NotificationService { init() async { await AwesomeNotifications().initialize( // set the icon to null if you want to use the default app icon - 'resource://drawable/ic_launcher', + 'resource://drawable/logo', [ NotificationChannel( enableVibration: true, @@ -98,6 +98,7 @@ class NotificationService { ///``` dispatchLocalNotification( {required String key, required Map customData}) async { + log.v("New Notification Dispatching"); // If two notifications happen to have same IDs // One will be replaced by the other, so to avoid // this scenario we generate a Random ID for any notification diff --git a/lib/services/services_info.dart b/lib/services/services_info.dart index 56f8833..92f61a0 100644 --- a/lib/services/services_info.dart +++ b/lib/services/services_info.dart @@ -11,12 +11,15 @@ class ServicesInfo { static const low_disk_space_notification_title = "Low Disk Space ⚠️ !"; static const low_disk_space_notification_body = "Your account has low disk space"; + static DateTime testDate = DateTime(0, 0, 0, 0, 0, 0, 0); } extension CustomizableDateTime on DateTime { static DateTime? _customTime; static DateTime get current { - return _customTime ?? DateTime.now(); + return _customTime?.isAtSameMomentAs(ServicesInfo.testDate) ?? false + ? ServicesInfo.testDate + : DateTime.now(); } static set customTime(DateTime customTime) { diff --git a/lib/services/state_services/disk_file_service.dart b/lib/services/state_services/disk_file_service.dart new file mode 100644 index 0000000..8d7c152 --- /dev/null +++ b/lib/services/state_services/disk_file_service.dart @@ -0,0 +1,74 @@ +import 'package:flutter/foundation.dart'; +import 'package:rutorrentflutter/app/app.locator.dart'; +import 'package:rutorrentflutter/enums/enums.dart'; +import 'package:rutorrentflutter/models/disk_file.dart'; +import 'package:rutorrentflutter/services/functional_services/shared_preferences_service.dart'; + +///Service to handle state of [DiskFile] objects in the application +class DiskFileService extends ChangeNotifier { + SharedPreferencesService _sharedPreferencesService = + locator(); + + ValueNotifier> _diskFileList = + new ValueNotifier(new List.empty()); + ValueNotifier> _diskFileDisplayList = + new ValueNotifier(new List.empty()); + + Sort _sortPreference = Sort.none; + + get sortPreference => _sortPreference; + + ValueNotifier> get diskFileList => _diskFileList; + ValueNotifier> get diskFileDisplayList => _diskFileDisplayList; + + setDiskFileList(List list) { + _diskFileList.value = list; + _diskFileDisplayList.value = list; + } + + setSortPreference(Sort newPreference) { + _sortPreference = newPreference; + _sharedPreferencesService.DB + .put("sortPreference_diskFiles", newPreference.index); + } + + /// Updates display list of [Torrent]s + updateDiskFileDisplayList({String? searchText}) { + log.v("Disk File Items being updated"); + List displayList = diskFileList.value; + //Sorting: sorting data on basis of sortPreference + displayList = _sortList(displayList, sortPreference)!; + + if (searchText != null) { + //Searching : showing list on basis of searched text + displayList = displayList + .where((element) => + element.name!.toLowerCase().contains(searchText.toLowerCase())) + .toList(); + } + diskFileDisplayList.value = displayList; + // ignore: invalid_use_of_visible_for_testing_member + // ignore: invalid_use_of_protected_member + diskFileDisplayList.notifyListeners(); + } + + List? _sortList(List? diskFileList, Sort? sort) { + switch (sort) { + case Sort.name_ascending: + diskFileList!.sort((a, b) => a.name!.compareTo(b.name!)); + return diskFileList; + + case Sort.name_descending: + diskFileList!.sort((a, b) => a.name!.compareTo(b.name!)); + return diskFileList.reversed.toList(); + + case Sort.dateAdded: + case Sort.ratio: + case Sort.size_ascending: + case Sort.size_descending: + case Sort.none: + default: + return diskFileList; + } + } +} diff --git a/lib/services/state_services/history_service.dart b/lib/services/state_services/history_service.dart index 2eb0099..39e2440 100644 --- a/lib/services/state_services/history_service.dart +++ b/lib/services/state_services/history_service.dart @@ -2,9 +2,11 @@ import 'package:flutter/foundation.dart'; import 'package:logger/logger.dart'; import 'package:rutorrentflutter/app/app.locator.dart'; import 'package:rutorrentflutter/app/app.logger.dart'; +import 'package:rutorrentflutter/enums/enums.dart'; import 'package:rutorrentflutter/models/history_item.dart'; import 'package:rutorrentflutter/services/api/i_api_service.dart'; import 'package:rutorrentflutter/services/functional_services/notification_service.dart'; +import 'package:rutorrentflutter/services/functional_services/shared_preferences_service.dart'; import 'package:rutorrentflutter/services/services_info.dart'; import 'package:rutorrentflutter/services/state_services/user_preferences_service.dart'; @@ -15,6 +17,8 @@ class HistoryService extends ChangeNotifier { UserPreferencesService _userPreferencesService = locator(); NotificationService _notificationService = locator(); + SharedPreferencesService _sharedPreferencesService = + locator(); ValueNotifier> _torrentsHistoryList = new ValueNotifier(new List.empty()); @@ -26,6 +30,10 @@ class HistoryService extends ChangeNotifier { ValueNotifier> get displayTorrentHistoryList => _torrentsHistoryDisplayList; + Sort _sortPreference = Sort.none; + + get sortPreference => _sortPreference; + setTorrentHistoryList(List list) { _torrentsHistoryList.value = list; _torrentsHistoryDisplayList.value = list; @@ -40,15 +48,58 @@ class HistoryService extends ChangeNotifier { : await _apiService.getHistory(lastHours: lastHours); } - updateTorrentHistoryDisplayList() { - _torrentsHistoryList.notifyListeners(); + updateTorrentHistoryDisplayList({String? searchText}) { + log.v("History Items being updated"); + List displayList = _torrentsHistoryList.value; + //Sorting: sorting data on basis of sortPreference + displayList = _sortList(displayList, sortPreference)!; + + if (searchText != null) { + //Searching : showing list on basis of searched text + displayList = displayList + .where((element) => + element.name.toLowerCase().contains(searchText.toLowerCase())) + .toList(); + } + _torrentsHistoryDisplayList.value = displayList; _torrentsHistoryDisplayList.notifyListeners(); } + List? _sortList(List? torrentsList, Sort? sort) { + switch (sort) { + case Sort.name_ascending: + torrentsList!.sort((a, b) => a.name.compareTo(b.name)); + return torrentsList; + + case Sort.name_descending: + torrentsList!.sort((a, b) => a.name.compareTo(b.name)); + return torrentsList.reversed.toList(); + + case Sort.dateAdded: + torrentsList!.sort((a, b) => a.actionTime.compareTo(b.actionTime)); + return torrentsList; + + case Sort.size_ascending: + torrentsList!.sort((a, b) => a.size!.compareTo(b.size!)); + return torrentsList; + + case Sort.size_descending: + torrentsList!.sort((a, b) => a.size!.compareTo(b.size!)); + return torrentsList.reversed.toList(); + + case Sort.none: + return _torrentsHistoryList.value; + + default: + return torrentsList; + } + } + notify() { + log.v("Checking for new torrents to send notification"); bool happenedNow(HistoryItem item) { - if (DateTime.now().millisecondsSinceEpoch ~/ 1000 - item.actionTime == 1) - return true; + if ((DateTime.now().millisecondsSinceEpoch ~/ 1000 - item.actionTime) < + 1000) return true; return false; } @@ -88,9 +139,16 @@ class HistoryService extends ChangeNotifier { case 3: // Torrent Deleted if (happenedNow(item)) { // Do Something + log.v("Torrent Deleted"); } break; } } } + + void setSortPreference(Sort newPreference) { + _sortPreference = newPreference; + _sharedPreferencesService.DB + .put("sortPreferenceHistory", newPreference.index); + } } diff --git a/lib/services/state_services/torrent_service.dart b/lib/services/state_services/torrent_service.dart index e181c13..ada5ee4 100644 --- a/lib/services/state_services/torrent_service.dart +++ b/lib/services/state_services/torrent_service.dart @@ -6,6 +6,7 @@ import 'package:rutorrentflutter/enums/enums.dart'; import 'package:rutorrentflutter/models/torrent.dart'; import 'package:rutorrentflutter/services/api/i_api_service.dart'; import 'package:rutorrentflutter/services/functional_services/shared_preferences_service.dart'; +import 'package:rutorrentflutter/services/state_services/history_service.dart'; import 'package:rutorrentflutter/services/state_services/user_preferences_service.dart'; Logger log = getLogger("TorrentService"); @@ -16,6 +17,7 @@ class TorrentService extends ChangeNotifier { locator(); SharedPreferencesService? _sharedPreferencesService = locator(); + HistoryService _historyService = locator(); ValueNotifier> _listOfLabels = new ValueNotifier(new List.empty()); @@ -78,7 +80,7 @@ class TorrentService extends ChangeNotifier { setActiveDownloads(List list) => _activeDownloads.value = list; setTorrentList(List list) { _torrentsList.value = list; - _torrentsDisplayList.value = list; + updateTorrentDisplayList(); } setSortPreference(Sort newPreference) { @@ -86,8 +88,8 @@ class TorrentService extends ChangeNotifier { _sharedPreferencesService!.DB.put("sortPreference", newPreference.index); } - /// Updates display list of [Torrent]s - updateTorrentDisplayList({String? searchText}) { + /// Updates display list of [Torrent]s Display List + updateTorrentDisplayList() { List displayList = torrentsList.value; //Sorting: sorting data on basis of sortPreference displayList = _sortList(displayList, sortPreference)!; @@ -100,11 +102,12 @@ class TorrentService extends ChangeNotifier { displayList = _filterListUsingLabel(displayList, selectedLabel); } - if (searchText != null) { + String? searchText = _userPreferencesService?.searchTextController.text; + if (searchText != null || (searchText?.isNotEmpty ?? false)) { //Searching : showing list on basis of searched text displayList = displayList .where((element) => - element.name.toLowerCase().contains(searchText.toLowerCase())) + element.name.toLowerCase().contains(searchText!.toLowerCase())) .toList(); } _torrentsDisplayList.value = displayList; @@ -204,8 +207,8 @@ class TorrentService extends ChangeNotifier { .cancel() : await _apiService.getTorrentList().listen((event) {}).cancel(); await _apiService.getHistory(); - await updateTorrentDisplayList( - searchText: _userPreferencesService?.searchTextController.text); + await updateTorrentDisplayList(); + _historyService.notify(); } ///Removes [Torrent] and updates torrents list diff --git a/lib/services/state_services/user_preferences_service.dart b/lib/services/state_services/user_preferences_service.dart index cf71b68..21bff30 100644 --- a/lib/services/state_services/user_preferences_service.dart +++ b/lib/services/state_services/user_preferences_service.dart @@ -1,15 +1,19 @@ import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; import 'package:logger/logger.dart'; +import 'package:package_info/package_info.dart'; import 'package:rutorrentflutter/app/app.locator.dart'; import 'package:rutorrentflutter/app/app.logger.dart'; import 'package:rutorrentflutter/enums/enums.dart'; import 'package:rutorrentflutter/services/functional_services/shared_preferences_service.dart'; +import 'package:rutorrentflutter/services/state_services/disk_file_service.dart'; +import 'package:rutorrentflutter/services/state_services/history_service.dart'; import 'package:rutorrentflutter/services/state_services/torrent_service.dart'; +import 'package:rutorrentflutter/theme/app_state_notifier.dart'; Logger log = getLogger("UserPreferencesService"); -///[Service] for keeping track of User Data +///Service for keeping track of [User] Data class UserPreferencesService { SharedPreferencesService? _sharedPreferencesService = locator(); @@ -20,6 +24,8 @@ class UserPreferencesService { bool _diskSpaceNotification = true; bool _addTorrentNotification = true; bool _downloadCompleteNotification = true; + bool _isDarkModeOn = false; + late PackageInfo _packageInfo; get allNotificationEnabled => _allNotificationEnabled; get diskSpaceNotification => @@ -28,19 +34,36 @@ class UserPreferencesService { _addTorrentNotification && _allNotificationEnabled; get downloadCompleteNotification => _downloadCompleteNotification && _allNotificationEnabled; + get isDarkModeOn => _isDarkModeOn; + get packageInfo => _packageInfo; init() { + //Fetch all User Preferences from local storage + //And restore state + + TorrentService _torrentService = locator(); + DiskFileService _diskFileService = locator(); + HistoryService _historyService = locator(); + AppStateNotifier _appStateNotifier = locator(); + // ignore: non_constant_identifier_names Box DB = _sharedPreferencesService!.DB; showAllAccounts = DB.get("showAllAccounts") ?? false; + _isDarkModeOn = DB.get("isDarkModeOn") ?? false; int sortPreferenceIdx = DB.get("sortPreference", defaultValue: 6); - TorrentService _torrentService = locator(); + int sortPreferenceIdxDiskFile = + DB.get("sortPreference_diskFiles", defaultValue: 6); + int sortPreferenceIdxHistory = + DB.get("sortPreferenceHistory", defaultValue: 6); _torrentService.setSortPreference(Sort.values[sortPreferenceIdx]); + _diskFileService.setSortPreference(Sort.values[sortPreferenceIdxDiskFile]); + _historyService.setSortPreference(Sort.values[sortPreferenceIdxHistory]); _allNotificationEnabled = DB.get("allNotificationEnabled") ?? true; _diskSpaceNotification = DB.get("diskSpaceNotification") ?? true; _addTorrentNotification = DB.get("addTorrentNotification") ?? true; _downloadCompleteNotification = DB.get("downloadCompleteNotification") ?? true; + _appStateNotifier.updateTheme(_isDarkModeOn); } setShowAllAccounts(bool showAccounts) { @@ -112,4 +135,17 @@ class UserPreferencesService { _sharedPreferencesService!.DB .put("downloadCompleteNotification", _downloadCompleteNotification); } + + setDarkMode(bool newVal) { + log.v("DarkMode set to " + newVal.toString()); + _isDarkModeOn = newVal; + _sharedPreferencesService!.DB.put("isDarkModeOn", _isDarkModeOn); + } + + setPackageInfo(PackageInfo newVal) { + //Application Version will not be saved to local storage + //Since we want it to be fetched everytime user opens application + log.v("PackageInfo Received ${newVal.version}"); + _packageInfo = newVal; + } } diff --git a/lib/theme/app_state_notifier.dart b/lib/theme/app_state_notifier.dart index cd6257c..bde89c7 100644 --- a/lib/theme/app_state_notifier.dart +++ b/lib/theme/app_state_notifier.dart @@ -1,11 +1,18 @@ import 'package:injectable/injectable.dart'; +import 'package:rutorrentflutter/app/app.locator.dart'; +import 'package:rutorrentflutter/services/state_services/user_preferences_service.dart'; import 'package:stacked/stacked.dart'; @lazySingleton class AppStateNotifier extends BaseViewModel { - static bool isDarkModeOn = false; + UserPreferencesService _userPreferencesService = + locator(); + + static bool isDarkModeOn = locator().isDarkModeOn; + void updateTheme(bool isdarkmodeon) { isDarkModeOn = isdarkmodeon; + _userPreferencesService.setDarkMode(isdarkmodeon); notifyListeners(); } } diff --git a/lib/ui/views/History/history_view.dart b/lib/ui/views/History/history_view.dart index 29380bd..7cc911b 100644 --- a/lib/ui/views/History/history_view.dart +++ b/lib/ui/views/History/history_view.dart @@ -2,11 +2,13 @@ import 'package:filesize/filesize.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:intl/intl.dart'; -import 'package:rutorrentflutter/theme/app_state_notifier.dart'; +import 'package:rutorrentflutter/enums/enums.dart'; import 'package:rutorrentflutter/models/history_item.dart'; +import 'package:rutorrentflutter/theme/app_state_notifier.dart'; import 'package:rutorrentflutter/ui/shared/shared_styles.dart'; import 'package:rutorrentflutter/ui/views/history/history_viewmodel.dart'; import 'package:rutorrentflutter/ui/widgets/dumb_widgets/loading_shimmer.dart'; +import 'package:rutorrentflutter/ui/widgets/smart_widgets/search_bar/search_bar_view.dart'; import 'package:stacked/stacked.dart'; class HistoryView extends StatelessWidget { @@ -50,66 +52,92 @@ class HistoryView extends StatelessWidget { ), body: RefreshIndicator( onRefresh: () async => model.refreshHistoryList(), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 16), - child: model.isBusy - ? LoadingShimmer().loadingEffect(context) - : (model.torrentHistoryDisplayList.value.length != 0) - ? ValueListenableBuilder( - valueListenable: model.torrentHistoryDisplayList, - builder: (context, List items, snapshot) { - return ListView.builder( - itemCount: items.length, - itemBuilder: (context, index) { - return ListTile( - onLongPress: () { - _showRemoveDialog( - items[index].hash, model, context); - }, - title: SizedBox( - width: 40, - child: Text(items[index].name, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600))), - trailing: Container( - padding: const EdgeInsets.all(4), - width: 70, - decoration: BoxDecoration( - border: Border.all( - color: getHistoryStatusColor( - context, items[index].action), - )), - child: Text( - HistoryItem - .historyStatus[items[index].action]!, - textAlign: TextAlign.center, - style: TextStyle( - color: getHistoryStatusColor( - context, items[index].action), - fontSize: 14, - fontWeight: FontWeight.w600, - )), - ), - subtitle: Text( - '${DateFormat('HH:mm dd MMM yy').format(DateTime.fromMillisecondsSinceEpoch(items[index].actionTime * 1000))} | ${filesize(items[index].size)}', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600), - ), - ); - }, - ); - }) - : Center( - child: SvgPicture.asset( - Theme.of(context).brightness == Brightness.light - ? 'assets/logo/empty.svg' - : 'assets/logo/empty_dark.svg', - width: 120, - height: 120, - ), - ), + child: Container( + // padding: const EdgeInsets.symmetric(vertical: 16), + child: Column( + children: [ + SearchBarWidget( + screen: Screens.TorrentHistoryViewScreen, + ), + ListTile( + title: Text( + 'Files (${model.torrentHistoryDisplayList.value.length})', + style: TextStyle(fontWeight: FontWeight.w600), + ), + ), + model.isBusy + ? Expanded( + child: Center( + child: LoadingShimmer().loadingEffect(context))) + : (model.torrentHistoryDisplayList.value.length != 0) + ? Expanded( + child: ValueListenableBuilder( + valueListenable: + model.torrentHistoryDisplayList, + builder: (context, List items, + snapshot) { + return ListView.builder( + // physics: NeverScrollableScrollPhysics(), + // scrollDirection: Axis.vertical, + // shrinkWrap: true, + itemCount: items.length, + itemBuilder: (context, index) { + return ListTile( + onLongPress: () { + _showRemoveDialog(items[index].hash, + model, context); + }, + title: SizedBox( + width: 40, + child: Text(items[index].name, + style: TextStyle( + fontSize: 14, + fontWeight: + FontWeight.w600))), + trailing: Container( + padding: const EdgeInsets.all(4), + width: 70, + decoration: BoxDecoration( + border: Border.all( + color: getHistoryStatusColor( + context, items[index].action), + )), + child: Text( + HistoryItem.historyStatus[ + items[index].action]!, + textAlign: TextAlign.center, + style: TextStyle( + color: getHistoryStatusColor( + context, + items[index].action), + fontSize: 14, + fontWeight: FontWeight.w600, + )), + ), + subtitle: Text( + '${DateFormat('HH:mm dd MMM yy').format(DateTime.fromMillisecondsSinceEpoch(items[index].actionTime * 1000))} | ${filesize(items[index].size)}', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600), + ), + ); + }, + ); + }), + ) + : Expanded( + child: Center( + child: SvgPicture.asset( + Theme.of(context).brightness == Brightness.light + ? 'assets/logo/empty.svg' + : 'assets/logo/empty_dark.svg', + width: 120, + height: 120, + ), + ), + ), + ], + ), ), ), ), diff --git a/lib/ui/views/History/history_viewmodel.dart b/lib/ui/views/History/history_viewmodel.dart index 4c600c0..8d0f316 100644 --- a/lib/ui/views/History/history_viewmodel.dart +++ b/lib/ui/views/History/history_viewmodel.dart @@ -22,6 +22,7 @@ class HistoryViewModel extends FutureViewModel { _apiService.updateHistory(); await Future.delayed(Duration(seconds: 1)); await _historyService.refreshTorrentHistoryList(); + _historyService.notify(); setBusy(false); } @@ -54,6 +55,7 @@ class HistoryViewModel extends FutureViewModel { selectedChoice == 'All' ? loadHistoryItems() : loadHistoryItems(lastHrs: int.parse(selectedChoice.split(' ')[2])); + _historyService.notify(); setBusy(false); } } diff --git a/lib/ui/views/Settings/settings_view.dart b/lib/ui/views/Settings/settings_view.dart index 2ddcb89..9a46269 100644 --- a/lib/ui/views/Settings/settings_view.dart +++ b/lib/ui/views/Settings/settings_view.dart @@ -25,6 +25,7 @@ class SettingsView extends StatelessWidget { valueListenable: model.accounts, builder: (context, List accounts, snapshot) { return ExpansionTile( + textColor: Theme.of(context).accentColor, title: Text('Manage Accounts', style: TextStyle(fontWeight: FontWeight.w600)), children: [ diff --git a/lib/ui/views/disk_explorer/disk_explorer_view.dart b/lib/ui/views/disk_explorer/disk_explorer_view.dart index 36b3d3f..73ba580 100644 --- a/lib/ui/views/disk_explorer/disk_explorer_view.dart +++ b/lib/ui/views/disk_explorer/disk_explorer_view.dart @@ -1,9 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:rutorrentflutter/enums/enums.dart'; +import 'package:rutorrentflutter/models/disk_file.dart'; import 'package:rutorrentflutter/theme/app_state_notifier.dart'; import 'package:rutorrentflutter/ui/views/disk_explorer/disk_explorer_viewmodel.dart'; import 'package:rutorrentflutter/ui/widgets/dumb_widgets/loading_shimmer.dart'; import 'package:rutorrentflutter/ui/widgets/smart_widgets/disk_file_tile/disk_file_tile_view.dart'; +import 'package:rutorrentflutter/ui/widgets/smart_widgets/search_bar/search_bar_view.dart'; import 'package:stacked/stacked.dart'; class DiskExplorerView extends StatelessWidget { @@ -29,27 +32,37 @@ class DiskExplorerView extends StatelessWidget { ? Container( child: Column( children: [ + SearchBarWidget( + screen: Screens.DiskExplorerViewScreen, + ), ListTile( title: Text( - 'Files (${model.diskFiles.length})', + 'Files (${model.diskFiles.value.length})', style: TextStyle(fontWeight: FontWeight.w600), ), ), model.isBusy ? Expanded( - child: LoadingShimmer().loadingEffect(context)) - : (model.diskFiles.length != 0) + child: Center( + child: + LoadingShimmer().loadingEffect(context))) + : (model.diskFiles.value.length != 0) ? Expanded( - child: ListView.builder( - itemCount: model.diskFiles.length, - itemBuilder: (context, index) { - return DiskFileTileView( - model.diskFiles[index], - model.path, - model.goBackwards, - model.goForwards); - }, - ), + child: ValueListenableBuilder( + valueListenable: model.diskFiles, + builder: (context, + List diskFiles, snapshot) { + return ListView.builder( + itemCount: diskFiles.length, + itemBuilder: (context, index) { + return DiskFileTileView( + diskFiles[index], + model.path, + model.goBackwards, + model.goForwards); + }, + ); + }), ) : Expanded( child: Center( diff --git a/lib/ui/views/disk_explorer/disk_explorer_viewmodel.dart b/lib/ui/views/disk_explorer/disk_explorer_viewmodel.dart index 1c941bc..bf19a97 100644 --- a/lib/ui/views/disk_explorer/disk_explorer_viewmodel.dart +++ b/lib/ui/views/disk_explorer/disk_explorer_viewmodel.dart @@ -1,8 +1,10 @@ +import 'package:flutter/foundation.dart'; import 'package:rutorrentflutter/app/app.locator.dart'; import 'package:rutorrentflutter/app/app.logger.dart'; import 'package:rutorrentflutter/models/disk_file.dart'; import 'package:rutorrentflutter/services/api/i_api_service.dart'; import 'package:rutorrentflutter/services/functional_services/authentication_service.dart'; +import 'package:rutorrentflutter/services/state_services/disk_file_service.dart'; import 'package:stacked/stacked.dart'; final log = getLogger("DiskExplorerViewModel"); @@ -11,12 +13,13 @@ class DiskExplorerViewModel extends FutureViewModel { IApiService _apiService = locator(); AuthenticationService _authenticationService = locator(); + DiskFileService _diskFileService = locator(); String path = "/"; bool isFeatureAvailable = false; - List _diskFiles = []; - get diskFiles => _diskFiles; + ValueNotifier> get diskFiles => + _diskFileService.diskFileDisplayList; Future onBackPress() async { log.v("Path is now : " + path); @@ -44,7 +47,7 @@ class DiskExplorerViewModel extends FutureViewModel { _getDiskFiles() async { setBusy(true); - _diskFiles = await _apiService.getDiskFiles(path); + await _apiService.getDiskFiles(path); setBusy(false); } diff --git a/lib/ui/views/home/home_view.dart b/lib/ui/views/home/home_view.dart index c470b85..831f8fe 100644 --- a/lib/ui/views/home/home_view.dart +++ b/lib/ui/views/home/home_view.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:rutorrentflutter/theme/app_state_notifier.dart'; import 'package:rutorrentflutter/ui/shared/shared_styles.dart'; import 'package:rutorrentflutter/ui/views/front_page/front_page_view.dart'; @@ -6,7 +7,6 @@ import 'package:rutorrentflutter/ui/widgets/dumb_widgets/floating_action_button_ import 'package:rutorrentflutter/ui/widgets/dumb_widgets/frontpageview_appbar_widget.dart'; import 'package:rutorrentflutter/ui/widgets/smart_widgets/drawer/drawer_view.dart'; import 'package:stacked/stacked.dart'; -import 'package:flutter/material.dart'; class HomeView extends StatefulWidget { const HomeView({Key? key}) : super(key: key); diff --git a/lib/ui/views/home/home_viewmodel.dart b/lib/ui/views/home/home_viewmodel.dart index 7e2c537..9fd1fbb 100644 --- a/lib/ui/views/home/home_viewmodel.dart +++ b/lib/ui/views/home/home_viewmodel.dart @@ -1,9 +1,9 @@ // import 'package:rutorrentflutter/app/locator.dart'; import 'package:flutter/cupertino.dart'; -import 'package:rutorrentflutter/theme/app_state_notifier.dart'; import 'package:rutorrentflutter/app/app.locator.dart'; import 'package:rutorrentflutter/models/account.dart'; import 'package:rutorrentflutter/services/functional_services/authentication_service.dart'; +import 'package:rutorrentflutter/theme/app_state_notifier.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:stacked/stacked.dart'; diff --git a/lib/ui/views/login/login_view.dart b/lib/ui/views/login/login_view.dart index 66a3153..9a3662d 100644 --- a/lib/ui/views/login/login_view.dart +++ b/lib/ui/views/login/login_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:modal_progress_hud_nsn/modal_progress_hud_nsn.dart'; +import 'package:rutorrentflutter/theme/app_state_notifier.dart'; import 'package:rutorrentflutter/ui/views/login/login_viewmodel.dart'; import 'package:rutorrentflutter/ui/widgets/dumb_widgets/data_input_widget.dart'; import 'package:rutorrentflutter/ui/widgets/dumb_widgets/password_input_widget.dart'; @@ -31,143 +32,159 @@ class LoginView extends StatelessWidget { passwordFocus.unfocus(); }, child: Scaffold( + backgroundColor: AppStateNotifier.isDarkModeOn + ? Theme.of(context).primaryColor + : Colors.white, appBar: AppBar( elevation: 0, + backgroundColor: AppStateNotifier.isDarkModeOn + ? Colors.black + : Theme.of(context).primaryColor, ), body: SingleChildScrollView( - child: Column( - children: [ - Container( - color: Theme.of(context).primaryColor, - width: double.infinity, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 16.0, - horizontal: 8.0, - ), - child: Column( - children: [ - SafeArea( - child: Padding( - padding: const EdgeInsets.only( - top: 8.0, - bottom: 24.0, - ), - child: Image( - image: AssetImage('assets/logo/white_logo.png'), - height: 50, + child: Container( + child: Column( + children: [ + Container( + color: AppStateNotifier.isDarkModeOn + ? Colors.black + : Theme.of(context).primaryColor, + width: double.infinity, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 16.0, + horizontal: 8.0, + ), + child: Column( + children: [ + SafeArea( + child: Padding( + padding: const EdgeInsets.only( + top: 8.0, + bottom: 24.0, + ), + child: Image( + image: !AppStateNotifier.isDarkModeOn + ? AssetImage('assets/logo/white_logo.png') + : AssetImage('assets/logo/dark_mode.png'), + height: 50, + ), ), ), - ), - SizedBox( - height: 10, - ), - Form( - key: _formKey, - child: DataInput( - borderColor: Colors.white, - textEditingController: urlController, - hintText: 'Location of ruTorrent', - hintTextColor: Colors.white54, - focus: urlFocus, - validator: model.urlValidator, - suffixIconButton: IconButton( - color: Colors.white, - onPressed: () async { - ClipboardData? data = - await Clipboard.getData('text/plain'); - if (data != null) - urlController.text = data.text.toString(); - if (urlFocus.hasFocus) urlFocus.unfocus(); - }, - icon: Icon(Icons.content_paste), - ), + SizedBox( + height: 10, ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Icon( - Icons.info_outline, + Form( + key: _formKey, + child: DataInput( + borderColor: Colors.white, + textEditingController: urlController, + hintText: 'Location of ruTorrent', + hintTextColor: Colors.white54, + focus: urlFocus, + validator: model.urlValidator, + suffixIconButton: IconButton( color: Colors.white, + onPressed: () async { + ClipboardData? data = + await Clipboard.getData('text/plain'); + if (data != null) + urlController.text = data.text.toString(); + if (urlFocus.hasFocus) urlFocus.unfocus(); + }, + icon: Icon(Icons.content_paste), ), ), - Flexible( - child: Text( - 'Check your browser address bar', - style: TextStyle( + ), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + Icons.info_outline, color: Colors.white, - fontSize: 12, - fontWeight: FontWeight.w600, ), ), - ), - ], - ) - ], + Flexible( + child: Text( + 'Check your browser address bar', + style: TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ) + ], + ), ), ), - ), - SizedBox( - height: 10, - ), - Padding( - padding: const EdgeInsets.all(16.0), - child: DataInput( - onFieldSubmittedCallback: (v) { - FocusScope.of(context).requestFocus(passwordFocus); - }, - focus: usernameFocus, - textEditingController: usernameController, - hintText: 'Username', - textInputAction: TextInputAction.next, + SizedBox( + height: 10, ), - ), - Padding( - padding: const EdgeInsets.all(16.0), - child: PasswordInput( - textEditingController: passwordController, - passwordFocus: passwordFocus, + Padding( + padding: const EdgeInsets.all(16.0), + child: DataInput( + onFieldSubmittedCallback: (v) { + FocusScope.of(context).requestFocus(passwordFocus); + }, + focus: usernameFocus, + textEditingController: usernameController, + hintText: 'Username', + textInputAction: TextInputAction.next, + ), ), - ), - Padding( - padding: const EdgeInsets.symmetric( - vertical: 16, horizontal: 32), - child: Container( - width: double.infinity, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5.0), - side: BorderSide( - color: Theme.of(context).primaryColor), + Padding( + padding: const EdgeInsets.all(16.0), + child: PasswordInput( + textEditingController: passwordController, + passwordFocus: passwordFocus, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 16, horizontal: 32), + child: Container( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5.0), + side: BorderSide( + color: Theme.of(context).primaryColor), + ), + primary: !AppStateNotifier.isDarkModeOn + ? Colors.white + : Colors.black), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 28, vertical: 16), + child: Text( + 'Let\'s get started', + style: TextStyle( + color: !AppStateNotifier.isDarkModeOn + ? Theme.of(context).primaryColor + : Colors.white, + fontSize: 18), ), - primary: Colors.white), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 28, vertical: 16), - child: Text( - 'Let\'s get started', - style: TextStyle( - color: Theme.of(context).primaryColor, - fontSize: 18), ), + onPressed: () async { + if (_formKey.currentState!.validate()) { + await model.login( + url: urlController.text.trim(), + username: usernameController.text.trim(), + password: passwordController.text.trim(), + ); + } + }, ), - onPressed: () async { - if (_formKey.currentState!.validate()) { - await model.login( - url: urlController.text.trim(), - username: usernameController.text.trim(), - password: passwordController.text.trim(), - ); - } - }, ), - ), - ) - ], + ) + ], + ), ), ), ), diff --git a/lib/ui/views/login/login_viewmodel.dart b/lib/ui/views/login/login_viewmodel.dart index b708fe4..428782d 100644 --- a/lib/ui/views/login/login_viewmodel.dart +++ b/lib/ui/views/login/login_viewmodel.dart @@ -39,20 +39,27 @@ class LoginViewModel extends BaseViewModel { return null; } - login({String? url, required String username, String? password}) async { + login({String? url, required String? username, String? password}) async { setBusy(true); - if (username.contains(" ") || - password!.contains(" ") || - username.isEmpty || - password.isEmpty) { - Fluttertoast.showToast(msg: 'Invalid username or password'); - } else { - _account = Account(url: url, username: username, password: password); - _authenticationService!.tempAccount = _account; - bool isValidConfiguration = await _validateConfigurationDetails(_account); - if (isValidConfiguration) - _navigationService?.replaceWith(Routes.splashView); + + if (username!.isEmpty) { + Fluttertoast.showToast(msg: 'Username cannot be empty!'); + setBusy(false); + return; + } + + if (password!.isEmpty) { + Fluttertoast.showToast(msg: 'Password cannot be empty!'); + setBusy(false); + return; } + + _account = Account(url: url, username: username, password: password); + _authenticationService!.tempAccount = _account; + bool isValidConfiguration = await _validateConfigurationDetails(_account); + if (isValidConfiguration) + _navigationService?.replaceWith(Routes.splashView); + setBusy(false); } diff --git a/lib/ui/views/splash/splash_viewmodel.dart b/lib/ui/views/splash/splash_viewmodel.dart index d07d112..39e210c 100644 --- a/lib/ui/views/splash/splash_viewmodel.dart +++ b/lib/ui/views/splash/splash_viewmodel.dart @@ -1,10 +1,12 @@ import 'package:logger/logger.dart'; import 'package:rutorrentflutter/app/app.locator.dart'; -import 'package:rutorrentflutter/app/app.router.dart'; import 'package:rutorrentflutter/app/app.logger.dart'; +import 'package:rutorrentflutter/app/app.router.dart'; import 'package:rutorrentflutter/models/account.dart'; import 'package:rutorrentflutter/services/functional_services/authentication_service.dart'; import 'package:rutorrentflutter/services/functional_services/shared_preferences_service.dart'; +import 'package:rutorrentflutter/services/state_services/user_preferences_service.dart'; +import 'package:rutorrentflutter/utils/package_info_service.dart'; import 'package:stacked/stacked.dart'; import 'package:stacked_services/stacked_services.dart'; @@ -16,12 +18,19 @@ class SplashViewModel extends FutureViewModel { NavigationService? _navigationService = locator(); AuthenticationService? _authenticationService = locator(); + PackageInfoService? _packageInfoService = locator(); + UserPreferencesService? _userPreferencesService = + locator(); handleStartUpLogic() async { _authenticationService!.accounts.value = await _sharedPreferencesService!.fetchSavedLogin(); List accounts = _authenticationService?.accounts.value ?? []; + //Get and Save PackageInfo + _userPreferencesService! + .setPackageInfo(await _packageInfoService!.getPackageInfo()); + if (accounts.isNotEmpty) { log.v("User is logged in"); _navigationService?.replaceWith(Routes.homeView); diff --git a/lib/ui/views/torrent_detail/torrent_detail_view.dart b/lib/ui/views/torrent_detail/torrent_detail_view.dart index fc5771b..9640ea4 100644 --- a/lib/ui/views/torrent_detail/torrent_detail_view.dart +++ b/lib/ui/views/torrent_detail/torrent_detail_view.dart @@ -217,6 +217,7 @@ class TorrentDetailView extends StatelessWidget { ], )), ExpansionTile( + textColor: Theme.of(context).accentColor, initiallyExpanded: model.torrent != null && model.torrent.status == Status.downloading, title: Text( @@ -508,6 +509,7 @@ class TorrentDetailView extends StatelessWidget { ], ), ExpansionTile( + textColor: Theme.of(context).accentColor, title: Text('Files', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600)), @@ -524,6 +526,7 @@ class TorrentDetailView extends StatelessWidget { ], ), ExpansionTile( + textColor: Theme.of(context).accentColor, title: Text( 'Trackers', style: TextStyle( diff --git a/lib/ui/views/torrent_list/torrent_list_view.dart b/lib/ui/views/torrent_list/torrent_list_view.dart index 67c78bc..9cb6d22 100644 --- a/lib/ui/views/torrent_list/torrent_list_view.dart +++ b/lib/ui/views/torrent_list/torrent_list_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:rutorrentflutter/app/app.logger.dart'; +import 'package:rutorrentflutter/enums/enums.dart'; import 'package:rutorrentflutter/models/torrent.dart'; import 'package:rutorrentflutter/ui/views/torrent_list/torrent_list_viewmodel.dart'; import 'package:rutorrentflutter/ui/widgets/dumb_widgets/loading_shimmer.dart'; @@ -26,7 +27,9 @@ class _TorrentListViewState extends State return ViewModelBuilder.reactive( builder: (context, model, child) => Column( children: [ - SearchBarWidget(), + SearchBarWidget( + screen: Screens.TorrentListViewScreen, + ), Expanded( child: StreamBuilder( stream: model.showAllAccounts diff --git a/lib/ui/widgets/dumb_widgets/filter_tile_list_widgets.dart b/lib/ui/widgets/dumb_widgets/filter_tile_list_widgets.dart index f157cd7..4cd6b6a 100644 --- a/lib/ui/widgets/dumb_widgets/filter_tile_list_widgets.dart +++ b/lib/ui/widgets/dumb_widgets/filter_tile_list_widgets.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:rutorrentflutter/theme/app_state_notifier.dart'; import 'package:rutorrentflutter/enums/enums.dart'; +import 'package:rutorrentflutter/theme/app_state_notifier.dart'; import 'package:rutorrentflutter/ui/widgets/smart_widgets/Drawer/drawer_viewmodel.dart'; class FilterTile extends StatelessWidget { @@ -13,18 +13,29 @@ class FilterTile extends StatelessWidget { @override Widget build(BuildContext context) { + bool isSelected = + (model.selectedFilter == filter && !model.isLabelSelected); return Container( - color: (model.selectedFilter == filter && !model.isLabelSelected) - ? Theme.of(context).disabledColor - : null, + color: isSelected ? Theme.of(context).accentColor : null, child: ListTile( dense: true, leading: Icon( icon, - color: !AppStateNotifier.isDarkModeOn ? Colors.black : Colors.white, + color: isSelected + ? Colors.white + : !AppStateNotifier.isDarkModeOn + ? Colors.black + : Colors.white, ), title: Text( - filter.toString().substring(filter.toString().indexOf('.') + 1)), + filter.toString().substring(filter.toString().indexOf('.') + 1), + style: TextStyle( + color: isSelected + ? Colors.white + : !AppStateNotifier.isDarkModeOn + ? Colors.black + : Colors.white), + ), onTap: () { model.changeFilter(filter); Navigator.pop(context); diff --git a/lib/ui/widgets/dumb_widgets/frontpageview_appbar_widget.dart b/lib/ui/widgets/dumb_widgets/frontpageview_appbar_widget.dart index 447a9aa..b9d5d77 100644 --- a/lib/ui/widgets/dumb_widgets/frontpageview_appbar_widget.dart +++ b/lib/ui/widgets/dumb_widgets/frontpageview_appbar_widget.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:rutorrentflutter/theme/app_state_notifier.dart'; import 'package:rutorrentflutter/models/account.dart'; +import 'package:rutorrentflutter/theme/app_state_notifier.dart'; import 'package:rutorrentflutter/ui/views/home/home_viewmodel.dart'; @override diff --git a/lib/ui/widgets/dumb_widgets/label_tile_widget.dart b/lib/ui/widgets/dumb_widgets/label_tile_widget.dart index be58845..ff6006a 100644 --- a/lib/ui/widgets/dumb_widgets/label_tile_widget.dart +++ b/lib/ui/widgets/dumb_widgets/label_tile_widget.dart @@ -9,17 +9,28 @@ class LabelTile extends StatelessWidget { @override Widget build(BuildContext context) { + bool isSelected = model.selectedLabel == label && model.isLabelSelected; return Container( - color: (model.selectedLabel == label && model.isLabelSelected) - ? Theme.of(context).disabledColor - : null, + color: isSelected ? Theme.of(context).accentColor : null, child: ListTile( dense: true, leading: Icon( Icons.label_important_outline, - color: !AppStateNotifier.isDarkModeOn ? Colors.black : Colors.white, + color: isSelected + ? Colors.white + : !AppStateNotifier.isDarkModeOn + ? Colors.black + : Colors.white, + ), + title: Text( + label, + style: TextStyle( + color: isSelected + ? Colors.white + : !AppStateNotifier.isDarkModeOn + ? Colors.black + : Colors.white), ), - title: Text(label), onTap: () { model.changeLabel(label); Navigator.pop(context); diff --git a/lib/ui/widgets/dumb_widgets/show_disk_space_widget.dart b/lib/ui/widgets/dumb_widgets/show_disk_space_widget.dart index 724f43f..20ae470 100644 --- a/lib/ui/widgets/dumb_widgets/show_disk_space_widget.dart +++ b/lib/ui/widgets/dumb_widgets/show_disk_space_widget.dart @@ -1,7 +1,7 @@ import 'package:filesize/filesize.dart'; import 'package:flutter/material.dart'; -import 'package:rutorrentflutter/theme/app_state_notifier.dart'; import 'package:rutorrentflutter/models/disk_space.dart'; +import 'package:rutorrentflutter/theme/app_state_notifier.dart'; import 'package:rutorrentflutter/ui/shared/shared_styles.dart'; class ShowDiskSpace extends StatelessWidget { diff --git a/lib/ui/widgets/smart_widgets/bottom_sheets/bottom_sheet_setup.dart b/lib/ui/widgets/smart_widgets/bottom_sheets/bottom_sheet_setup.dart index fdd03c4..febc776 100644 --- a/lib/ui/widgets/smart_widgets/bottom_sheets/bottom_sheet_setup.dart +++ b/lib/ui/widgets/smart_widgets/bottom_sheets/bottom_sheet_setup.dart @@ -1,12 +1,13 @@ +import 'package:flutter/material.dart'; import 'package:rutorrentflutter/app/app.locator.dart'; import 'package:rutorrentflutter/enums/bottom_sheet_type.dart'; +import 'package:rutorrentflutter/enums/enums.dart'; import 'package:rutorrentflutter/ui/widgets/dumb_widgets/text_field_view.dart'; import 'package:rutorrentflutter/ui/widgets/smart_widgets/bottom_sheets/confirm_bottom_sheet/confirm_bottom_sheet_view.dart'; import 'package:rutorrentflutter/ui/widgets/smart_widgets/bottom_sheets/option_bottom_sheet/option_bottom_sheet_view.dart'; import 'package:rutorrentflutter/ui/widgets/smart_widgets/bottom_sheets/sort_bottom_sheet/sort_bottom_sheet_view.dart'; import 'package:stacked/stacked.dart'; import 'package:stacked_services/stacked_services.dart'; -import 'package:flutter/material.dart'; void setUpBottomSheetUi() { final bottomSheetService = locator(); @@ -15,7 +16,11 @@ void setUpBottomSheetUi() { BottomSheetType.floating: (context, sheetRequest, completer) => _FloatingBoxBottomSheet(request: sheetRequest, completer: completer), BottomSheetType.sortBottomSheet: (context, sheetRequest, completer) => - SortBottomSheetView(request: sheetRequest, completer: completer), + SortBottomSheetView( + request: sheetRequest, + completer: completer, + screen: Screens.TorrentListViewScreen, + ), BottomSheetType.confirmBottomSheet: (context, sheetRequest, completer) => ConfirmBottomSheetView(request: sheetRequest, completer: completer), BottomSheetType.optionBottomSheet: (context, sheetRequest, completer) => diff --git a/lib/ui/widgets/smart_widgets/bottom_sheets/sort_bottom_sheet/sort_bottom_sheet_view.dart b/lib/ui/widgets/smart_widgets/bottom_sheets/sort_bottom_sheet/sort_bottom_sheet_view.dart index 8985bc9..84e0eee 100644 --- a/lib/ui/widgets/smart_widgets/bottom_sheets/sort_bottom_sheet/sort_bottom_sheet_view.dart +++ b/lib/ui/widgets/smart_widgets/bottom_sheets/sort_bottom_sheet/sort_bottom_sheet_view.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:rutorrentflutter/theme/app_state_notifier.dart'; -import 'package:rutorrentflutter/app/constants.dart'; import 'package:rutorrentflutter/enums/enums.dart'; +import 'package:rutorrentflutter/theme/app_state_notifier.dart'; import 'package:rutorrentflutter/ui/shared/shared_styles.dart'; import 'package:rutorrentflutter/ui/widgets/smart_widgets/bottom_sheets/sort_bottom_sheet/sort_bottom_sheet_viewmodel.dart'; import 'package:stacked/stacked.dart'; @@ -10,11 +9,14 @@ import 'package:stacked_services/stacked_services.dart'; class SortBottomSheetView extends StatelessWidget { final SheetRequest request; final Function(SheetResponse) completer; - SortBottomSheetView({required this.request, required this.completer}); + final Screens screen; + SortBottomSheetView( + {required this.request, required this.completer, required this.screen}); @override Widget build(BuildContext context) { return ViewModelBuilder.reactive( + onModelReady: (model) => model.init(screen), builder: (context, model, child) => Padding( padding: const EdgeInsets.only( left: 16.0, @@ -61,31 +63,35 @@ class SortBottomSheetView extends StatelessWidget { child: ListView.builder( itemCount: Sort.values.length - 1, itemBuilder: (context, index) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - model.setSortPreference(completer, Sort.values[index]); - Navigator.of(context).pop(); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - sortMap[Sort.values[index]]!, - style: TextStyle(fontWeight: FontWeight.w600), - ), - Radio( - groupValue: model.sortPreference, - value: Sort.values[index], - onChanged: (selected) { + Map sortMap = model.getSortMap(); + return sortMap[Sort.values[index]] == null + ? Container() + : GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { model.setSortPreference( completer, Sort.values[index]); Navigator.of(context).pop(); }, - ), - ], - ), - ); + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + sortMap[Sort.values[index]]!, + style: TextStyle(fontWeight: FontWeight.w600), + ), + Radio( + groupValue: model.sortPreference, + value: Sort.values[index], + onChanged: (selected) { + model.setSortPreference( + completer, Sort.values[index]); + Navigator.of(context).pop(); + }, + ), + ], + ), + ); }, ), ) diff --git a/lib/ui/widgets/smart_widgets/bottom_sheets/sort_bottom_sheet/sort_bottom_sheet_viewmodel.dart b/lib/ui/widgets/smart_widgets/bottom_sheets/sort_bottom_sheet/sort_bottom_sheet_viewmodel.dart index 0a41c58..424e906 100644 --- a/lib/ui/widgets/smart_widgets/bottom_sheets/sort_bottom_sheet/sort_bottom_sheet_viewmodel.dart +++ b/lib/ui/widgets/smart_widgets/bottom_sheets/sort_bottom_sheet/sort_bottom_sheet_viewmodel.dart @@ -1,6 +1,9 @@ import 'package:rutorrentflutter/app/app.locator.dart'; import 'package:rutorrentflutter/app/app.logger.dart'; +import 'package:rutorrentflutter/app/constants.dart'; import 'package:rutorrentflutter/enums/enums.dart'; +import 'package:rutorrentflutter/services/state_services/disk_file_service.dart'; +import 'package:rutorrentflutter/services/state_services/history_service.dart'; import 'package:rutorrentflutter/services/state_services/torrent_service.dart'; import 'package:stacked/stacked.dart'; @@ -8,13 +11,53 @@ final log = getLogger("SortBottomSheetViewModel"); class SortBottomSheetViewModel extends BaseViewModel { TorrentService _torrentService = locator(); + DiskFileService _diskFileService = locator(); + HistoryService _historyService = locator(); - Sort get sortPreference => _torrentService.sortPreference; + late Screens screen; + + init(Screens screen) { + this.screen = screen; + } + + Sort get sortPreference { + switch (screen) { + case Screens.TorrentListViewScreen: + return _torrentService.sortPreference; + case Screens.DiskExplorerViewScreen: + return _diskFileService.sortPreference; + case Screens.TorrentHistoryViewScreen: + return _historyService.sortPreference; + } + } setSortPreference(Function func, Sort newPreference) { log.e(newPreference); - _torrentService.setSortPreference(newPreference); - _torrentService.updateTorrentDisplayList(); + switch (screen) { + case Screens.TorrentListViewScreen: + _torrentService.setSortPreference(newPreference); + _torrentService.updateTorrentDisplayList(); + break; + case Screens.DiskExplorerViewScreen: + _diskFileService.setSortPreference(newPreference); + _diskFileService.updateDiskFileDisplayList(); + break; + case Screens.TorrentHistoryViewScreen: + _historyService.setSortPreference(newPreference); + _historyService.updateTorrentHistoryDisplayList(); + break; + } notifyListeners(); } + + Map getSortMap() { + switch (screen) { + case Screens.TorrentListViewScreen: + return sortMapTorrentList; + case Screens.DiskExplorerViewScreen: + return sortMapDiskFileList; + case Screens.TorrentHistoryViewScreen: + return sortMapHistoryList; + } + } } diff --git a/lib/ui/widgets/smart_widgets/drawer/drawer_view.dart b/lib/ui/widgets/smart_widgets/drawer/drawer_view.dart index d0e6e11..b448476 100644 --- a/lib/ui/widgets/smart_widgets/drawer/drawer_view.dart +++ b/lib/ui/widgets/smart_widgets/drawer/drawer_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:package_info/package_info.dart'; -import 'package:rutorrentflutter/theme/app_state_notifier.dart'; import 'package:rutorrentflutter/app/constants.dart'; +import 'package:rutorrentflutter/theme/app_state_notifier.dart'; import 'package:rutorrentflutter/ui/widgets/dumb_widgets/label_tile_widget.dart'; import 'package:rutorrentflutter/ui/widgets/dumb_widgets/show_disk_space_widget.dart'; import 'package:rutorrentflutter/ui/widgets/smart_widgets/Drawer/drawer_viewmodel.dart'; @@ -62,6 +62,7 @@ class DrawerView extends StatelessWidget { : Colors.white), title: Text( 'Filters', + style: TextStyle(color: Theme.of(context).accentColor), ), children: model.filterTileList(model), ), @@ -73,6 +74,7 @@ class DrawerView extends StatelessWidget { : Colors.white), title: Text( 'Labels', + style: TextStyle(color: Theme.of(context).accentColor), ), children: ((model.listOfLabels.value as List) .map((e) => LabelTile( diff --git a/lib/ui/widgets/smart_widgets/drawer/drawer_viewmodel.dart b/lib/ui/widgets/smart_widgets/drawer/drawer_viewmodel.dart index 7c3e223..309613a 100644 --- a/lib/ui/widgets/smart_widgets/drawer/drawer_viewmodel.dart +++ b/lib/ui/widgets/smart_widgets/drawer/drawer_viewmodel.dart @@ -1,5 +1,5 @@ +import 'package:flutter/material.dart'; import 'package:logger/logger.dart'; -import 'package:package_info/package_info.dart'; import 'package:rutorrentflutter/app/app.locator.dart'; import 'package:rutorrentflutter/app/app.logger.dart'; import 'package:rutorrentflutter/app/app.router.dart'; @@ -14,7 +14,6 @@ import 'package:rutorrentflutter/services/state_services/user_preferences_servic import 'package:rutorrentflutter/ui/widgets/dumb_widgets/add_another_account_widget.dart'; import 'package:rutorrentflutter/ui/widgets/dumb_widgets/filter_tile_list_widgets.dart'; import 'package:stacked/stacked.dart'; -import 'package:flutter/material.dart'; import 'package:stacked_services/stacked_services.dart'; Logger log = getLogger("DrawerViewModel"); @@ -29,15 +28,11 @@ class DrawerViewModel extends BaseViewModel { locator(); IApiService _apiService = locator(); - PackageInfo packageinfo = new PackageInfo( - packageName: '', appName: '', buildNumber: '', version: ''); - - get packageInfo => packageinfo; + get packageInfo => _userPreferencesService.packageInfo; void init() async { setBusy(true); await _apiService.updateDiskSpace(); - packageinfo = await PackageInfo.fromPlatform(); setBusy(false); } diff --git a/lib/ui/widgets/smart_widgets/rss_label_tile/rss_detail_sheet/rss_detail_sheet_view.dart b/lib/ui/widgets/smart_widgets/rss_label_tile/rss_detail_sheet/rss_detail_sheet_view.dart index 5a00d8d..ce20d8d 100644 --- a/lib/ui/widgets/smart_widgets/rss_label_tile/rss_detail_sheet/rss_detail_sheet_view.dart +++ b/lib/ui/widgets/smart_widgets/rss_label_tile/rss_detail_sheet/rss_detail_sheet_view.dart @@ -1,9 +1,9 @@ +import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:rutorrentflutter/theme/app_state_notifier.dart'; import 'package:rutorrentflutter/models/rss.dart'; +import 'package:rutorrentflutter/theme/app_state_notifier.dart'; import 'package:rutorrentflutter/ui/widgets/dumb_widgets/loading_shimmer.dart'; import 'package:rutorrentflutter/ui/widgets/smart_widgets/rss_label_tile/rss_detail_sheet/rss_detail_sheet_viewmodel.dart'; -import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart'; class RSSDetailSheetView extends StatelessWidget { diff --git a/lib/ui/widgets/smart_widgets/rss_label_tile/rss_label_tile_view.dart b/lib/ui/widgets/smart_widgets/rss_label_tile/rss_label_tile_view.dart index e99edab..91c6609 100644 --- a/lib/ui/widgets/smart_widgets/rss_label_tile/rss_label_tile_view.dart +++ b/lib/ui/widgets/smart_widgets/rss_label_tile/rss_label_tile_view.dart @@ -40,6 +40,7 @@ class RSSLabelTileView extends StatelessWidget { }), ) : ExpansionTile( + textColor: Theme.of(context).accentColor, leading: FaIcon( FontAwesomeIcons.rssSquare, color: Colors.orange[500], diff --git a/lib/ui/widgets/smart_widgets/search_bar/search_bar_view.dart b/lib/ui/widgets/smart_widgets/search_bar/search_bar_view.dart index f9bd059..25411e6 100644 --- a/lib/ui/widgets/smart_widgets/search_bar/search_bar_view.dart +++ b/lib/ui/widgets/smart_widgets/search_bar/search_bar_view.dart @@ -1,18 +1,21 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:rutorrentflutter/enums/enums.dart'; import 'package:rutorrentflutter/theme/app_state_notifier.dart'; import 'package:rutorrentflutter/ui/shared/shared_styles.dart'; -import 'package:rutorrentflutter/ui/widgets/smart_widgets/search_bar/search_bar_viewmodel.dart'; import 'package:rutorrentflutter/ui/widgets/smart_widgets/bottom_sheets/sort_bottom_sheet/sort_bottom_sheet_view.dart'; +import 'package:rutorrentflutter/ui/widgets/smart_widgets/search_bar/search_bar_viewmodel.dart'; import 'package:stacked/stacked.dart'; import 'package:stacked_services/stacked_services.dart'; class SearchBarWidget extends StatelessWidget { - const SearchBarWidget({Key? key}) : super(key: key); + final Screens screen; + const SearchBarWidget({Key? key, required this.screen}) : super(key: key); @override Widget build(BuildContext context) { return ViewModelBuilder.reactive( + onModelReady: (model) => model.init(screen), builder: (context, model, child) => Padding( padding: const EdgeInsets.all(16.0), child: Row( @@ -51,7 +54,7 @@ class SearchBarWidget extends StatelessWidget { border: InputBorder.none, contentPadding: EdgeInsets.symmetric(horizontal: 16), - hintText: 'Search torrent by name'), + hintText: model.getHintText()), onChanged: (value) => model.onTyping(value), ), ), @@ -82,6 +85,7 @@ class SearchBarWidget extends StatelessWidget { isScrollControlled: true, builder: (context) { return SortBottomSheetView( + screen: screen, completer: (s) => {}, request: SheetRequest(), ); diff --git a/lib/ui/widgets/smart_widgets/search_bar/search_bar_viewmodel.dart b/lib/ui/widgets/smart_widgets/search_bar/search_bar_viewmodel.dart index 97fe9b4..1832ddb 100644 --- a/lib/ui/widgets/smart_widgets/search_bar/search_bar_viewmodel.dart +++ b/lib/ui/widgets/smart_widgets/search_bar/search_bar_viewmodel.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; import 'package:rutorrentflutter/app/app.locator.dart'; +import 'package:rutorrentflutter/enums/enums.dart'; +import 'package:rutorrentflutter/services/state_services/disk_file_service.dart'; +import 'package:rutorrentflutter/services/state_services/history_service.dart'; import 'package:rutorrentflutter/services/state_services/torrent_service.dart'; import 'package:rutorrentflutter/services/state_services/user_preferences_service.dart'; import 'package:stacked/stacked.dart'; @@ -8,11 +11,14 @@ class SearchBarWidgetViewModel extends BaseViewModel { UserPreferencesService? _userPreferencesService = locator(); TorrentService _torrentService = locator(); + DiskFileService _diskFileService = locator(); + HistoryService _historyService = locator(); + Screens screen = Screens.TorrentListViewScreen; bool isSearching = false; FocusNode _searchBarFocus = FocusNode(); - get searchBarFocus => _searchBarFocus; + get searchBarFocus => _searchBarFocus; get searchTextController => _userPreferencesService!.searchTextController; setSearchingState(bool search) { @@ -21,6 +27,33 @@ class SearchBarWidgetViewModel extends BaseViewModel { } onTyping(String searchKey) { - _torrentService.updateTorrentDisplayList(searchText: searchKey); + switch (screen) { + case Screens.TorrentListViewScreen: + _torrentService.updateTorrentDisplayList(); + break; + case Screens.DiskExplorerViewScreen: + _diskFileService.updateDiskFileDisplayList(); + break; + case Screens.TorrentHistoryViewScreen: + _historyService.updateTorrentHistoryDisplayList(); + break; + default: + break; + } + } + + void init(Screens screen) { + this.screen = screen; + } + + String getHintText() { + switch (screen) { + case Screens.TorrentListViewScreen: + return 'Search torrent by name'; + case Screens.DiskExplorerViewScreen: + return 'Search file by name'; + case Screens.TorrentHistoryViewScreen: + return 'Search History items by name'; + } } } diff --git a/lib/ui/widgets/smart_widgets/torrent_tile/torrent_tile_view.dart b/lib/ui/widgets/smart_widgets/torrent_tile/torrent_tile_view.dart index 4fec201..686e7fb 100644 --- a/lib/ui/widgets/smart_widgets/torrent_tile/torrent_tile_view.dart +++ b/lib/ui/widgets/smart_widgets/torrent_tile/torrent_tile_view.dart @@ -1,7 +1,7 @@ import 'package:filesize/filesize.dart'; import 'package:flutter/material.dart'; -import 'package:rutorrentflutter/theme/app_state_notifier.dart'; import 'package:rutorrentflutter/models/torrent.dart'; +import 'package:rutorrentflutter/theme/app_state_notifier.dart'; import 'package:rutorrentflutter/ui/shared/shared_styles.dart'; import 'package:rutorrentflutter/ui/widgets/dumb_widgets/remove_torrent_dialog_widget.dart'; import 'package:rutorrentflutter/ui/widgets/smart_widgets/torrent_tile/torrent_tile_viewmodel.dart'; diff --git a/lib/ui/widgets/smart_widgets/torrent_tile/torrent_tile_viewmodel.dart b/lib/ui/widgets/smart_widgets/torrent_tile/torrent_tile_viewmodel.dart index 7673ed6..2508f32 100644 --- a/lib/ui/widgets/smart_widgets/torrent_tile/torrent_tile_viewmodel.dart +++ b/lib/ui/widgets/smart_widgets/torrent_tile/torrent_tile_viewmodel.dart @@ -1,8 +1,8 @@ -import 'package:rutorrentflutter/services/api/i_api_service.dart'; import 'package:rutorrentflutter/app/app.locator.dart'; import 'package:rutorrentflutter/app/app.logger.dart'; import 'package:rutorrentflutter/app/app.router.dart'; import 'package:rutorrentflutter/models/torrent.dart'; +import 'package:rutorrentflutter/services/api/i_api_service.dart'; import 'package:rutorrentflutter/services/state_services/torrent_service.dart'; import 'package:rutorrentflutter/services/state_services/user_preferences_service.dart'; import 'package:stacked/stacked.dart'; diff --git a/lib/ui/widgets/smart_widgets/url_bottom_sheet/url_bottomsheet_view.dart b/lib/ui/widgets/smart_widgets/url_bottom_sheet/url_bottomsheet_view.dart index b075fc7..5062e14 100644 --- a/lib/ui/widgets/smart_widgets/url_bottom_sheet/url_bottomsheet_view.dart +++ b/lib/ui/widgets/smart_widgets/url_bottom_sheet/url_bottomsheet_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:rutorrentflutter/theme/app_state_notifier.dart'; import 'package:rutorrentflutter/enums/enums.dart'; +import 'package:rutorrentflutter/theme/app_state_notifier.dart'; import 'package:rutorrentflutter/ui/widgets/dumb_widgets/data_input_widget.dart'; import 'package:rutorrentflutter/ui/widgets/smart_widgets/URL_bottom_sheet/url_bottomsheet_viewmodel.dart'; import 'package:stacked/stacked.dart'; diff --git a/lib/ui/widgets/smart_widgets/url_bottom_sheet/url_bottomsheet_viewmodel.dart b/lib/ui/widgets/smart_widgets/url_bottom_sheet/url_bottomsheet_viewmodel.dart index e23ec63..e890cb2 100644 --- a/lib/ui/widgets/smart_widgets/url_bottom_sheet/url_bottomsheet_viewmodel.dart +++ b/lib/ui/widgets/smart_widgets/url_bottom_sheet/url_bottomsheet_viewmodel.dart @@ -2,11 +2,13 @@ import 'package:flutter/material.dart'; import 'package:rutorrentflutter/app/app.locator.dart'; import 'package:rutorrentflutter/enums/enums.dart'; import 'package:rutorrentflutter/services/api/i_api_service.dart'; +import 'package:rutorrentflutter/services/state_services/torrent_service.dart'; import 'package:rutorrentflutter/utils/file_picker_service.dart'; import 'package:stacked/stacked.dart'; class URLBottomSheetViewModel extends BaseViewModel { FilePickerService? _filePickerService = locator(); + TorrentService _torrentService = locator(); IApiService _apiService = locator(); final TextEditingController urlTextController = TextEditingController(); @@ -33,12 +35,13 @@ class URLBottomSheetViewModel extends BaseViewModel { } void pickTorrentFile() async { - String? torrentPath = - await (_filePickerService!.selectFile() as Future); + String? torrentPath = await _filePickerService!.selectFile(); if (torrentPath != null) { - _apiService.addTorrentFile(torrentPath); + await _apiService.addTorrentFile(torrentPath); } + await Future.delayed(Duration(seconds: 2)); + await _torrentService.refreshTorrentList(); } void submit(HomeViewBottomSheetMode? mode) { diff --git a/lib/utils/package_info_service.dart b/lib/utils/package_info_service.dart new file mode 100644 index 0000000..579b28a --- /dev/null +++ b/lib/utils/package_info_service.dart @@ -0,0 +1,9 @@ +import 'package:package_info/package_info.dart'; + +///A Service to fetch [PackageInfo] +class PackageInfoService { + Future getPackageInfo() async { + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + return packageInfo; + } +} diff --git a/pubspec.lock b/pubspec.lock index 47ccc5d..2364354 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -450,6 +450,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.0" + mockito: + dependency: "direct dev" + description: + name: mockito + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.14" modal_bottom_sheet: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 37f0109..3b393ff 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,6 +48,7 @@ dev_dependencies: sdk: flutter stacked_generator: ^0.4.6 build_runner: ^2.0.3 + mockito: flutter: uses-material-design: true diff --git a/test/helpers/test_data.dart b/test/helpers/test_data.dart new file mode 100644 index 0000000..8ae3d47 --- /dev/null +++ b/test/helpers/test_data.dart @@ -0,0 +1,40 @@ +import 'package:flutter/foundation.dart'; +import 'package:rutorrentflutter/models/account.dart'; +import 'package:rutorrentflutter/models/history_item.dart'; +import 'package:rutorrentflutter/services/services_info.dart'; + +/// This class contains the test data used in tests to remove non-deterministic behaviour +class TestData { + static const url = "http://localhost:8080"; + + static ValueNotifier> accounts = ValueNotifier([ + Account(username: "test", password: "test", url: "http://localhost:8080") + ]); + + static const updateHistoryJSONReponse = + '{"items":[{"action":3,"name":"Test Torrent","size":276445467,"downloaded":0,"uploaded":0,"ratio":0,"creation":1490916601,"added":1617006438,"finished":0,"tracker":"udp:\/\/tracker.leechers-paradise.org:6969","label":"","action_time":1617006494,"hash":"965a1ccb9c297983eeb4e6e1f7a6f693"}],"mode":false}'; + + static List historyItems = [ + HistoryItem("Test Torrent", 3, 1617006494, 276445467, + "965a1ccb9c297983eeb4e6e1f7a6f693"), + ]; + + static const httpRpcPluginUrl = url + '/plugins/httprpc/action.php'; + + static const addTorrentPluginUrl = url + '/php/addtorrent.php'; + + static const diskSpacePluginUrl = url + '/plugins/diskspace/action.php'; + + static const rssPluginUrl = url + '/plugins/rss/action.php'; + + static const historyPluginUrl = url + '/plugins/history/action.php'; + + static const explorerPluginUrl = url + '/plugins/explorer/action.php'; + + static String getConstantTimeStamp() { + return ((CustomizableDateTime.current.millisecondsSinceEpoch - + Duration(seconds: 10).inMilliseconds) ~/ + 1000) + .toString(); + } +} diff --git a/test/helpers/test_helpers.dart b/test/helpers/test_helpers.dart new file mode 100644 index 0000000..949b736 --- /dev/null +++ b/test/helpers/test_helpers.dart @@ -0,0 +1,131 @@ +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:rutorrentflutter/app/app.locator.dart'; +import 'package:rutorrentflutter/services/api/prod_api_service.dart'; +import 'package:rutorrentflutter/services/functional_services/authentication_service.dart'; +import 'package:rutorrentflutter/services/functional_services/disk_space_service.dart'; +import 'package:rutorrentflutter/services/functional_services/notification_service.dart'; +import 'package:rutorrentflutter/services/functional_services/shared_preferences_service.dart'; +import 'package:rutorrentflutter/services/state_services/disk_file_service.dart'; +import 'package:rutorrentflutter/services/state_services/history_service.dart'; +import 'package:rutorrentflutter/services/state_services/torrent_service.dart'; +import 'package:rutorrentflutter/services/state_services/user_preferences_service.dart'; + +import 'test_data.dart'; +import 'test_helpers.mocks.dart'; + +@GenerateMocks([], customMocks: [ + MockSpec(returnNullOnMissingStub: true), + MockSpec(returnNullOnMissingStub: true), + MockSpec(returnNullOnMissingStub: true), + MockSpec(returnNullOnMissingStub: true), + MockSpec(returnNullOnMissingStub: true), + MockSpec(returnNullOnMissingStub: true), + MockSpec(returnNullOnMissingStub: true), + MockSpec(returnNullOnMissingStub: true), + MockSpec(returnNullOnMissingStub: true), +]) +ProdApiService getAndRegisterProdApiServiceMock() { + print("getAndRegisterProdApiServiceMock"); + _removeRegistrationIfExists(); + final service = ProdApiService(); + when(service.historyPluginUrl) + .thenAnswer((realInvocation) => TestData.historyPluginUrl); + locator.registerSingleton(service); + return service; +} + +HistoryService getAndRegisterHistoryServiceMock() { + print("getAndRegisterHistoryServiceMock"); + _removeRegistrationIfExists(); + final service = HistoryService(); + locator.registerSingleton(service); + return service; +} + +AuthenticationService getAndRegisterAuthenticationService() { + print("getAndRegisterAuthenticationService"); + _removeRegistrationIfExists(); + final service = MockAuthenticationService(); + when(service.accounts).thenAnswer((realInvocation) => TestData.accounts); + locator.registerSingleton(service); + return service; +} + +DiskSpaceService getAndRegisterDiskSpaceService() { + print("getAndRegisterDiskSpaceService"); + _removeRegistrationIfExists(); + final service = MockDiskSpaceService(); + locator.registerSingleton(service); + return service; +} + +TorrentService getAndRegisterTorrentService() { + print("getAndRegisterTorrentService"); + _removeRegistrationIfExists(); + final service = MockTorrentService(); + locator.registerSingleton(service); + return service; +} + +DiskFileService getAndRegisterDiskFileService() { + print("getAndRegisterDiskFileService"); + _removeRegistrationIfExists(); + final service = MockDiskFileService(); + locator.registerSingleton(service); + return service; +} + +UserPreferencesService getAndRegisterUserPreferencesService() { + print("getAndRegisterUserPreferencesService"); + _removeRegistrationIfExists(); + final service = MockUserPreferencesService(); + locator.registerSingleton(service); + return service; +} + +SharedPreferencesService getAndRegisterSharedPreferencesService() { + print("getAndRegisterSharedPreferencesService"); + _removeRegistrationIfExists(); + final service = MockSharedPreferencesService(); + locator.registerSingleton(service); + return service; +} + +NotificationService getAndRegisterNotificationService() { + print("getAndRegisterNotificationService"); + _removeRegistrationIfExists(); + final service = MockNotificationService(); + locator.registerSingleton(service); + return service; +} + +void registerServices() { + getAndRegisterSharedPreferencesService(); + getAndRegisterNotificationService(); + getAndRegisterUserPreferencesService(); + getAndRegisterAuthenticationService(); + getAndRegisterHistoryServiceMock(); + getAndRegisterDiskSpaceService(); + getAndRegisterTorrentService(); + getAndRegisterDiskFileService(); + getAndRegisterProdApiServiceMock(); +} + +void unregisterServices() { + locator.unregister(); + locator.unregister(); + locator.unregister(); + locator.unregister(); + locator.unregister(); + locator.unregister(); + locator.unregister(); + locator.unregister(); + locator.unregister(); +} + +void _removeRegistrationIfExists() { + if (locator.isRegistered()) { + locator.unregister(); + } +} diff --git a/test/helpers/test_helpers.mocks.dart b/test/helpers/test_helpers.mocks.dart new file mode 100644 index 0000000..6774c79 --- /dev/null +++ b/test/helpers/test_helpers.mocks.dart @@ -0,0 +1,547 @@ +// Mocks generated by Mockito 5.0.14 from annotations +// in rutorrentflutter/test/helpers/test_helpers.dart. +// Do not manually edit this file. + +import 'dart:async' as _i9; +import 'dart:ui' as _i18; + +import 'package:awesome_notifications/awesome_notifications.dart' as _i28; +import 'package:flutter/foundation.dart' as _i3; +import 'package:flutter/material.dart' as _i5; +import 'package:hive/hive.dart' as _i6; +import 'package:http/io_client.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; +import 'package:package_info/package_info.dart' as _i24; +import 'package:rutorrentflutter/enums/enums.dart' as _i17; +import 'package:rutorrentflutter/models/account.dart' as _i8; +import 'package:rutorrentflutter/models/disk_file.dart' as _i12; +import 'package:rutorrentflutter/models/disk_space.dart' as _i4; +import 'package:rutorrentflutter/models/history_item.dart' as _i11; +import 'package:rutorrentflutter/models/rss.dart' as _i14; +import 'package:rutorrentflutter/models/rss_filter.dart' as _i15; +import 'package:rutorrentflutter/models/torrent.dart' as _i10; +import 'package:rutorrentflutter/models/torrent_file.dart' as _i13; +import 'package:rutorrentflutter/services/api/prod_api_service.dart' as _i7; +import 'package:rutorrentflutter/services/functional_services/authentication_service.dart' + as _i19; +import 'package:rutorrentflutter/services/functional_services/disk_space_service.dart' + as _i20; +import 'package:rutorrentflutter/services/functional_services/notification_service.dart' + as _i27; +import 'package:rutorrentflutter/services/functional_services/shared_preferences_service.dart' + as _i25; +import 'package:rutorrentflutter/services/state_services/disk_file_service.dart' + as _i22; +import 'package:rutorrentflutter/services/state_services/history_service.dart' + as _i16; +import 'package:rutorrentflutter/services/state_services/torrent_service.dart' + as _i21; +import 'package:rutorrentflutter/services/state_services/user_preferences_service.dart' + as _i23; +import 'package:shared_preferences/shared_preferences.dart' as _i26; + +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis + +// ignore: camel_case_types +class _FakeIOClient_0 extends _i1.Fake implements _i2.IOClient {} + +// ignore: camel_case_types +class _FakeValueNotifier_1 extends _i1.Fake implements _i3.ValueNotifier { +} + +// ignore: camel_case_types +class _FakeDiskSpace_2 extends _i1.Fake implements _i4.DiskSpace {} + +// ignore: camel_case_types +class _FakeTextEditingController_3 extends _i1.Fake + implements _i5.TextEditingController {} + +// ignore: camel_case_types +class _FakeBox_4 extends _i1.Fake implements _i6.Box {} + +/// A class which mocks [ProdApiService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockProdApiService extends _i1.Mock implements _i7.ProdApiService { + @override + _i2.IOClient get ioClient => (super.noSuchMethod(Invocation.getter(#ioClient), + returnValue: _FakeIOClient_0()) as _i2.IOClient); + @override + Map getAuthHeader({_i8.Account? currentAccount}) => + (super.noSuchMethod( + Invocation.method( + #getAuthHeader, [], {#currentAccount: currentAccount}), + returnValue: {}) as Map); + @override + _i9.Future testConnectionAndLogin(_i8.Account? account) => + (super.noSuchMethod(Invocation.method(#testConnectionAndLogin, [account]), + returnValue: Future.value(false)) as _i9.Future); + @override + _i9.Future updateDiskSpace() => + (super.noSuchMethod(Invocation.method(#updateDiskSpace, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Stream> getAllAccountsTorrentList() => + (super.noSuchMethod(Invocation.method(#getAllAccountsTorrentList, []), + returnValue: Stream>.empty()) + as _i9.Stream>); + @override + _i9.Stream?> getTorrentList() => + (super.noSuchMethod(Invocation.method(#getTorrentList, []), + returnValue: Stream?>.empty()) + as _i9.Stream?>); + @override + dynamic stopTorrent(String? hashValue) => + super.noSuchMethod(Invocation.method(#stopTorrent, [hashValue])); + @override + dynamic removeTorrent(String? hashValue) => + super.noSuchMethod(Invocation.method(#removeTorrent, [hashValue])); + @override + dynamic removeTorrentWithData(String? hashValue) => super + .noSuchMethod(Invocation.method(#removeTorrentWithData, [hashValue])); + @override + dynamic addTorrent(String? url) => + super.noSuchMethod(Invocation.method(#addTorrent, [url])); + @override + dynamic addTorrentFile(String? torrentPath) => + super.noSuchMethod(Invocation.method(#addTorrentFile, [torrentPath])); + @override + dynamic toggleTorrentStatus(_i10.Torrent? torrent) => + super.noSuchMethod(Invocation.method(#toggleTorrentStatus, [torrent])); + @override + _i9.Future> getHistory({int? lastHours}) => (super + .noSuchMethod(Invocation.method(#getHistory, [], {#lastHours: lastHours}), + returnValue: + Future>.value(<_i11.HistoryItem>[])) as _i9 + .Future>); + @override + _i9.Future> getAllAccountsHistory({int? lastHours}) => + (super.noSuchMethod( + Invocation.method( + #getAllAccountsHistory, [], {#lastHours: lastHours}), + returnValue: + Future>.value(<_i11.HistoryItem>[])) + as _i9.Future>); + @override + dynamic removeHistoryItem(String? hashValue) => + super.noSuchMethod(Invocation.method(#removeHistoryItem, [hashValue])); + @override + dynamic setTorrentLabel({String? hashValue, String? label}) => + super.noSuchMethod(Invocation.method( + #setTorrentLabel, [], {#hashValue: hashValue, #label: label})); + @override + dynamic removeTorrentLabel({String? hashValue}) => super.noSuchMethod( + Invocation.method(#removeTorrentLabel, [], {#hashValue: hashValue})); + @override + _i9.Future changePassword(int? index, String? newPassword) => (super + .noSuchMethod(Invocation.method(#changePassword, [index, newPassword]), + returnValue: Future.value(false)) as _i9.Future); + @override + _i9.Future> getDiskFiles(String? path) => + (super.noSuchMethod(Invocation.method(#getDiskFiles, [path]), + returnValue: Future>.value(<_i12.DiskFile>[])) + as _i9.Future>); + @override + _i9.Future> getAllAccountsDiskFiles(String? path) => + (super.noSuchMethod(Invocation.method(#getAllAccountsDiskFiles, [path]), + returnValue: Future>.value(<_i12.DiskFile>[])) + as _i9.Future>); + @override + _i9.Future> getFiles(String? hashValue) => + (super.noSuchMethod(Invocation.method(#getFiles, [hashValue]), + returnValue: + Future>.value(<_i13.TorrentFile>[])) + as _i9.Future>); + @override + _i9.Future> getTrackers(String? hashValue) => + (super.noSuchMethod(Invocation.method(#getTrackers, [hashValue]), + returnValue: Future>.value([])) + as _i9.Future>); + @override + _i9.Future> loadRSS() => + (super.noSuchMethod(Invocation.method(#loadRSS, []), + returnValue: Future>.value(<_i14.RSSLabel>[])) + as _i9.Future>); + @override + _i9.Future> loadAllAccountsRSS() => + (super.noSuchMethod(Invocation.method(#loadAllAccountsRSS, []), + returnValue: Future>.value(<_i14.RSSLabel>[])) + as _i9.Future>); + @override + dynamic removeRSS(String? hashValue) => + super.noSuchMethod(Invocation.method(#removeRSS, [hashValue])); + @override + dynamic addRSS(String? rssUrl) => + super.noSuchMethod(Invocation.method(#addRSS, [rssUrl])); + @override + _i9.Future getRSSDetails(_i14.RSSItem? rssItem, String? labelHash) => + (super.noSuchMethod( + Invocation.method(#getRSSDetails, [rssItem, labelHash]), + returnValue: Future.value(false)) as _i9.Future); + @override + _i9.Future> getRSSFilters() => (super.noSuchMethod( + Invocation.method(#getRSSFilters, []), + returnValue: Future>.value(<_i15.RSSFilter>[])) + as _i9.Future>); + @override + _i9.Future> getAllAccountsRSSFilters() => + (super.noSuchMethod(Invocation.method(#getAllAccountsRSSFilters, []), + returnValue: + Future>.value(<_i15.RSSFilter>[])) + as _i9.Future>); + @override + String toString() => super.toString(); +} + +/// A class which mocks [HistoryService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockHistoryService extends _i1.Mock implements _i16.HistoryService { + @override + _i3.ValueNotifier> get torrentsHistoryList => + (super.noSuchMethod(Invocation.getter(#torrentsHistoryList), + returnValue: _FakeValueNotifier_1>()) + as _i3.ValueNotifier>); + @override + _i3.ValueNotifier> get displayTorrentHistoryList => + (super.noSuchMethod(Invocation.getter(#displayTorrentHistoryList), + returnValue: _FakeValueNotifier_1>()) + as _i3.ValueNotifier>); + @override + bool get hasListeners => + (super.noSuchMethod(Invocation.getter(#hasListeners), returnValue: false) + as bool); + @override + dynamic setTorrentHistoryList(List<_i11.HistoryItem>? list) => + super.noSuchMethod(Invocation.method(#setTorrentHistoryList, [list])); + @override + void setSortPreference(_i17.Sort? newPreference) => + super.noSuchMethod(Invocation.method(#setSortPreference, [newPreference]), + returnValueForMissingStub: null); + @override + void addListener(_i18.VoidCallback? listener) => + super.noSuchMethod(Invocation.method(#addListener, [listener]), + returnValueForMissingStub: null); + @override + void removeListener(_i18.VoidCallback? listener) => + super.noSuchMethod(Invocation.method(#removeListener, [listener]), + returnValueForMissingStub: null); + @override + void dispose() => super.noSuchMethod(Invocation.method(#dispose, []), + returnValueForMissingStub: null); + @override + void notifyListeners() => + super.noSuchMethod(Invocation.method(#notifyListeners, []), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); +} + +/// A class which mocks [AuthenticationService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAuthenticationService extends _i1.Mock + implements _i19.AuthenticationService { + @override + _i3.ValueNotifier> get accounts => + (super.noSuchMethod(Invocation.getter(#accounts), + returnValue: _FakeValueNotifier_1>()) + as _i3.ValueNotifier>); + @override + set accounts(dynamic accounts) => + super.noSuchMethod(Invocation.setter(#accounts, accounts), + returnValueForMissingStub: null); + @override + set tempAccount(dynamic account) => + super.noSuchMethod(Invocation.setter(#tempAccount, account), + returnValueForMissingStub: null); + @override + bool get hasListeners => + (super.noSuchMethod(Invocation.getter(#hasListeners), returnValue: false) + as bool); + @override + _i9.Future<_i3.ValueNotifier>> getAccount() => + (super.noSuchMethod(Invocation.method(#getAccount, []), + returnValue: Future<_i3.ValueNotifier>>.value( + _FakeValueNotifier_1>())) + as _i9.Future<_i3.ValueNotifier>>); + @override + _i9.Future> saveLogin(_i8.Account? account) => + (super.noSuchMethod(Invocation.method(#saveLogin, [account]), + returnValue: Future>.value(<_i8.Account?>[])) + as _i9.Future>); + @override + bool matchAccount(_i8.Account? api1, _i8.Account? api2) => + (super.noSuchMethod(Invocation.method(#matchAccount, [api1, api2]), + returnValue: false) as bool); + @override + _i9.Future changePassword(int? index, String? password) => + (super.noSuchMethod(Invocation.method(#changePassword, [index, password]), + returnValue: Future.value(false)) as _i9.Future); + @override + void deleteAccount(int? index) => + super.noSuchMethod(Invocation.method(#deleteAccount, [index]), + returnValueForMissingStub: null); + @override + void logoutAllAccounts() => + super.noSuchMethod(Invocation.method(#logoutAllAccounts, []), + returnValueForMissingStub: null); + @override + void removeAccount(int? index) => + super.noSuchMethod(Invocation.method(#removeAccount, [index]), + returnValueForMissingStub: null); + @override + void addListener(_i18.VoidCallback? listener) => + super.noSuchMethod(Invocation.method(#addListener, [listener]), + returnValueForMissingStub: null); + @override + void removeListener(_i18.VoidCallback? listener) => + super.noSuchMethod(Invocation.method(#removeListener, [listener]), + returnValueForMissingStub: null); + @override + void dispose() => super.noSuchMethod(Invocation.method(#dispose, []), + returnValueForMissingStub: null); + @override + void notifyListeners() => + super.noSuchMethod(Invocation.method(#notifyListeners, []), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); +} + +/// A class which mocks [DiskSpaceService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockDiskSpaceService extends _i1.Mock implements _i20.DiskSpaceService { + @override + _i4.DiskSpace get diskSpace => + (super.noSuchMethod(Invocation.getter(#diskSpace), + returnValue: _FakeDiskSpace_2()) as _i4.DiskSpace); + @override + String toString() => super.toString(); +} + +/// A class which mocks [TorrentService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTorrentService extends _i1.Mock implements _i21.TorrentService { + @override + _i3.ValueNotifier> get listOfLabels => + (super.noSuchMethod(Invocation.getter(#listOfLabels), + returnValue: _FakeValueNotifier_1>()) + as _i3.ValueNotifier>); + @override + _i3.ValueNotifier> get activeDownloads => + (super.noSuchMethod(Invocation.getter(#activeDownloads), + returnValue: _FakeValueNotifier_1>()) + as _i3.ValueNotifier>); + @override + _i3.ValueNotifier> get torrentsList => + (super.noSuchMethod(Invocation.getter(#torrentsList), + returnValue: _FakeValueNotifier_1>()) + as _i3.ValueNotifier>); + @override + _i3.ValueNotifier> get displayTorrentList => + (super.noSuchMethod(Invocation.getter(#displayTorrentList), + returnValue: _FakeValueNotifier_1>()) + as _i3.ValueNotifier>); + @override + bool get hasListeners => + (super.noSuchMethod(Invocation.getter(#hasListeners), returnValue: false) + as bool); + @override + dynamic setListOfLabels(List? labels) => + super.noSuchMethod(Invocation.method(#setListOfLabels, [labels])); + @override + dynamic changeLabel(String? label) => + super.noSuchMethod(Invocation.method(#changeLabel, [label])); + @override + dynamic changeFilter(_i17.Filter? filter) => + super.noSuchMethod(Invocation.method(#changeFilter, [filter])); + @override + dynamic setActiveDownloads(List<_i10.Torrent>? list) => + super.noSuchMethod(Invocation.method(#setActiveDownloads, [list])); + @override + dynamic setTorrentList(List<_i10.Torrent>? list) => + super.noSuchMethod(Invocation.method(#setTorrentList, [list])); + @override + dynamic setSortPreference(_i17.Sort? newPreference) => super + .noSuchMethod(Invocation.method(#setSortPreference, [newPreference])); + @override + dynamic removeTorrent(String? hashValue) => + super.noSuchMethod(Invocation.method(#removeTorrent, [hashValue])); + @override + dynamic removeTorrentWithData(String? hashValue) => super + .noSuchMethod(Invocation.method(#removeTorrentWithData, [hashValue])); + @override + void addListener(_i18.VoidCallback? listener) => + super.noSuchMethod(Invocation.method(#addListener, [listener]), + returnValueForMissingStub: null); + @override + void removeListener(_i18.VoidCallback? listener) => + super.noSuchMethod(Invocation.method(#removeListener, [listener]), + returnValueForMissingStub: null); + @override + void dispose() => super.noSuchMethod(Invocation.method(#dispose, []), + returnValueForMissingStub: null); + @override + void notifyListeners() => + super.noSuchMethod(Invocation.method(#notifyListeners, []), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); +} + +/// A class which mocks [DiskFileService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockDiskFileService extends _i1.Mock implements _i22.DiskFileService { + @override + _i3.ValueNotifier> get diskFileList => + (super.noSuchMethod(Invocation.getter(#diskFileList), + returnValue: _FakeValueNotifier_1>()) + as _i3.ValueNotifier>); + @override + _i3.ValueNotifier> get diskFileDisplayList => + (super.noSuchMethod(Invocation.getter(#diskFileDisplayList), + returnValue: _FakeValueNotifier_1>()) + as _i3.ValueNotifier>); + @override + bool get hasListeners => + (super.noSuchMethod(Invocation.getter(#hasListeners), returnValue: false) + as bool); + @override + dynamic setDiskFileList(List<_i12.DiskFile>? list) => + super.noSuchMethod(Invocation.method(#setDiskFileList, [list])); + @override + dynamic setSortPreference(_i17.Sort? newPreference) => super + .noSuchMethod(Invocation.method(#setSortPreference, [newPreference])); + @override + void addListener(_i18.VoidCallback? listener) => + super.noSuchMethod(Invocation.method(#addListener, [listener]), + returnValueForMissingStub: null); + @override + void removeListener(_i18.VoidCallback? listener) => + super.noSuchMethod(Invocation.method(#removeListener, [listener]), + returnValueForMissingStub: null); + @override + void dispose() => super.noSuchMethod(Invocation.method(#dispose, []), + returnValueForMissingStub: null); + @override + void notifyListeners() => + super.noSuchMethod(Invocation.method(#notifyListeners, []), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); +} + +/// A class which mocks [UserPreferencesService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockUserPreferencesService extends _i1.Mock + implements _i23.UserPreferencesService { + @override + _i5.TextEditingController get searchTextController => + (super.noSuchMethod(Invocation.getter(#searchTextController), + returnValue: _FakeTextEditingController_3()) + as _i5.TextEditingController); + @override + set searchTextController(_i5.TextEditingController? _searchTextController) => + super.noSuchMethod( + Invocation.setter(#searchTextController, _searchTextController), + returnValueForMissingStub: null); + @override + bool get showAllAccounts => (super + .noSuchMethod(Invocation.getter(#showAllAccounts), returnValue: false) + as bool); + @override + set showAllAccounts(bool? _showAllAccounts) => + super.noSuchMethod(Invocation.setter(#showAllAccounts, _showAllAccounts), + returnValueForMissingStub: null); + @override + dynamic setShowAllAccounts(bool? showAccounts) => super + .noSuchMethod(Invocation.method(#setShowAllAccounts, [showAccounts])); + @override + dynamic setAllNotification(bool? newVal) => + super.noSuchMethod(Invocation.method(#setAllNotification, [newVal])); + @override + dynamic setDiskSpaceNotification(bool? newVal) => super + .noSuchMethod(Invocation.method(#setDiskSpaceNotification, [newVal])); + @override + dynamic setAddTorrentNotification(bool? newVal) => super + .noSuchMethod(Invocation.method(#setAddTorrentNotification, [newVal])); + @override + dynamic setDownloadCompleteNotification(bool? newVal) => super.noSuchMethod( + Invocation.method(#setDownloadCompleteNotification, [newVal])); + @override + dynamic setDarkMode(bool? newVal) => + super.noSuchMethod(Invocation.method(#setDarkMode, [newVal])); + @override + dynamic setPackageInfo(_i24.PackageInfo? newVal) => + super.noSuchMethod(Invocation.method(#setPackageInfo, [newVal])); + @override + String toString() => super.toString(); +} + +/// A class which mocks [SharedPreferencesService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockSharedPreferencesService extends _i1.Mock + implements _i25.SharedPreferencesService { + @override + String get accountsData => + (super.noSuchMethod(Invocation.getter(#accountsData), returnValue: '') + as String); + @override + set accountsData(String? _accountsData) => + super.noSuchMethod(Invocation.setter(#accountsData, _accountsData), + returnValueForMissingStub: null); + @override + // ignore: non_constant_identifier_names + _i6.Box get DB => (super.noSuchMethod(Invocation.getter(#DB), + returnValue: _FakeBox_4()) as _i6.Box); + @override + // ignore: non_constant_identifier_names + set DB(_i6.Box? DB) => super.noSuchMethod(Invocation.setter(#DB, DB), + returnValueForMissingStub: null); + @override + _i9.Future<_i26.SharedPreferences?> store() => + (super.noSuchMethod(Invocation.method(#store, []), + returnValue: Future<_i26.SharedPreferences?>.value()) + as _i9.Future<_i26.SharedPreferences?>); + @override + dynamic saveLogin(List<_i8.Account?>? accounts) => + super.noSuchMethod(Invocation.method(#saveLogin, [accounts])); + @override + _i9.Future> fetchSavedLogin() => + (super.noSuchMethod(Invocation.method(#fetchSavedLogin, []), + returnValue: Future>.value(<_i8.Account>[])) + as _i9.Future>); + @override + String toString() => super.toString(); +} + +/// A class which mocks [NotificationService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockNotificationService extends _i1.Mock + implements _i27.NotificationService { + @override + dynamic handleLocalNotification(_i28.ReceivedAction? receivedNotification) => + super.noSuchMethod( + Invocation.method(#handleLocalNotification, [receivedNotification])); + @override + dynamic dispatchLocalNotification( + {String? key, Map? customData}) => + super.noSuchMethod(Invocation.method(#dispatchLocalNotification, [], + {#key: key, #customData: customData})); + @override + String toString() => super.toString(); +} diff --git a/test/service_tests/api_service_test.dart b/test/service_tests/api_service_test.dart new file mode 100644 index 0000000..a3621ed --- /dev/null +++ b/test/service_tests/api_service_test.dart @@ -0,0 +1,20 @@ +import 'package:flutter_test/flutter_test.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('ApiServiceTests -', () { + group('StartUp -', () { + setUp(() => registerServices()); + tearDown(() => unregisterServices()); + }); + + group('Update History -', () { + test( + 'When update history network call made, should populate history items', + () async { + //todo Implement Test + }); + }); + }); +}