diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 5168130..9141f68 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - Notube + noTube CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -13,7 +13,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - notube + noTube CFBundlePackageType APPL CFBundleShortVersionString diff --git a/lib/constants.dart b/lib/constants.dart index 281db6f..542cad3 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -48,7 +48,7 @@ enum Format { '"18/22/bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best"', this.format = 'MP4', this.ffmpegCmd = '', - this.extension = '', + this.extension = 'mp4', }); } diff --git a/lib/dlForm/cubit/dl_form_cubit.dart b/lib/dlForm/cubit/dl_form_cubit.dart index 8ce0e71..3fd26a9 100644 --- a/lib/dlForm/cubit/dl_form_cubit.dart +++ b/lib/dlForm/cubit/dl_form_cubit.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; -import 'package:flutter/material.dart'; import 'package:notube/constants.dart'; import 'package:notube/models/video.dart'; import 'package:notube/services/download.dart'; @@ -29,6 +28,9 @@ class DlFormCubit extends Cubit { void parseUrl() async { dlService = await DLServices.init(); + if (state.url.isEmpty) { + return; + } var shellStream = await dlService.analyseUrl(state.url); shellStream.listen((line) { @@ -43,7 +45,8 @@ class DlFormCubit extends Cubit { var video = Video.fromJson(videoTmp); videos.add(video.copyWith( format: state.format, - filename: '${video.id}_${state.format.format}')); + filename: '${video.id}_${state.format.format}', + playlistTitle: playlistTitle)); } } else { var video = Video.fromJson(dataInfos); diff --git a/lib/dlForm/dl_form.dart b/lib/dlForm/dl_form.dart index 01707b2..02a4903 100644 --- a/lib/dlForm/dl_form.dart +++ b/lib/dlForm/dl_form.dart @@ -50,7 +50,7 @@ class DlForm extends StatelessWidget { DropdownFormat(), SubmitButton(), ]), - DebugDlFormState(), + //DebugDlFormState(), ], ); } diff --git a/lib/dlForm/formComponents/format_dropdown.dart b/lib/dlForm/formComponents/format_dropdown.dart index 1bf1a27..3b4adfa 100644 --- a/lib/dlForm/formComponents/format_dropdown.dart +++ b/lib/dlForm/formComponents/format_dropdown.dart @@ -25,7 +25,7 @@ class DropdownFormat extends StatelessWidget { onChanged: (String? value) { var format = Format.values.firstWhere((element) => element.format == value); - context.read().setFormat(format!); + context.read().setFormat(format); }, underline: Container(), items: Format.values.map>((Format format) { diff --git a/lib/dlForm/formComponents/submit_button.dart b/lib/dlForm/formComponents/submit_button.dart index 4ca5fd2..d2bd990 100644 --- a/lib/dlForm/formComponents/submit_button.dart +++ b/lib/dlForm/formComponents/submit_button.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter/rendering.dart'; import 'package:notube/constants.dart'; import 'package:notube/models/video.dart'; import 'package:notube/videoList/cubit/videos_cubit.dart'; @@ -35,23 +37,27 @@ class _SubmitButtonState extends State { constraints: BoxConstraints.tightFor(width: 200), child: MouseRegion( cursor: SystemMouseCursors.click, + onHover: (PointerHoverEvent event) { + setState(() => isHovering = true); + }, + onExit: (PointerExitEvent event) { + setState(() => isHovering = false); + }, child: GestureDetector( - onTap: () { - context.read().parseUrl(); - }, - child: InkWell( - onHover: (hovering) { - debugPrint('Hovering: $hovering'); - setState(() => isHovering = hovering); - }, - child: Container( - padding: const EdgeInsets.symmetric(vertical: 14), - decoration: BoxDecoration( - color: isHovering ? colorDarkRed : colorMainRed, - ), - child: const Text('Ok', textAlign: TextAlign.center).tr(), - ), - ))), + onTap: () { + context.read().parseUrl(); + }, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 14), + decoration: BoxDecoration( + color: isHovering ? colorDarkRed : colorMainRed, + borderRadius: BorderRadius.only( + topRight: Radius.circular(3), + bottomRight: Radius.circular(3), + )), + child: const Text('Ok', textAlign: TextAlign.center).tr(), + ), + )), )); } } diff --git a/lib/dlForm/formComponents/url_text_field.dart b/lib/dlForm/formComponents/url_text_field.dart index 888f64f..9913610 100644 --- a/lib/dlForm/formComponents/url_text_field.dart +++ b/lib/dlForm/formComponents/url_text_field.dart @@ -11,25 +11,34 @@ class UrlTextField extends StatelessWidget { @override Widget build(BuildContext context) { return Flexible( - child: TextFormField( - decoration: InputDecoration( - border: InputBorder.none, - hintText: 'search_url'.tr(), - filled: true, - fillColor: colorBackgroundBlack, - contentPadding: EdgeInsets.symmetric( - horizontal: 20, + child: Container( + decoration: BoxDecoration( + color: colorBackgroundBlack, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(3), + bottomLeft: Radius.circular(3), ), ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter some text'; - } - return null; - }, - onChanged: (newUrl) { - context.read().setUrl(newUrl); - }, + child: TextFormField( + decoration: InputDecoration( + border: InputBorder.none, + hintText: 'search_url'.tr(), + filled: true, + fillColor: Colors.transparent, + contentPadding: EdgeInsets.symmetric( + horizontal: 20, + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter some text'; + } + return null; + }, + onChanged: (newUrl) { + context.read().setUrl(newUrl); + }, + ), ), ); } diff --git a/lib/main.dart b/lib/main.dart index 372446d..8f35de8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:notube/screens/settings.dart'; import 'package:notube/wrapper.dart'; import 'package:notube/constants.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:window_manager/window_manager.dart'; import 'package:upgrader/upgrader.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -15,7 +17,7 @@ void main() async { WindowOptions windowOptions = WindowOptions( size: Size(800, 600), minimumSize: Size(320, 400), - title: 'NoTube', + title: 'noTube', ); windowManager.waitUntilReadyToShow(windowOptions, () async { await windowManager.setIcon('assets/images/icon.png'); @@ -63,9 +65,13 @@ class _NoTubeAppState extends State { localizationsDelegates: context.localizationDelegates, supportedLocales: context.supportedLocales, locale: context.locale, - home: BlocProvider( - create: (context) => VideosCubit(), - child: UpgradeAlert(upgrader: upgrader, child: const Wrapper())), + initialRoute: '/', + routes: { + '/': (context) => BlocProvider( + create: (context) => VideosCubit(), + child: UpgradeAlert(upgrader: upgrader, child: const Wrapper())), + '/settings': (context) => Settings(), + }, ); } } diff --git a/lib/models/Video.dart b/lib/models/Video.dart index df8ce40..6f7c05a 100644 --- a/lib/models/Video.dart +++ b/lib/models/Video.dart @@ -18,6 +18,7 @@ class Video extends Equatable { this.uploader = '', this.uploadDate = '', this.filename = '', + this.playlistTitle = '', }); final String id; @@ -33,6 +34,7 @@ class Video extends Equatable { final String uploader; final String uploadDate; final String filename; + final String playlistTitle; factory Video.fromJson(Map json) { return Video( @@ -63,6 +65,7 @@ class Video extends Equatable { String? uploader, String? uploadDate, String? filename, + String? playlistTitle, }) { return Video( id: id ?? this.id, @@ -78,6 +81,7 @@ class Video extends Equatable { uploader: uploader ?? this.uploader, uploadDate: uploadDate ?? this.uploadDate, filename: filename ?? this.filename, + playlistTitle: playlistTitle ?? this.playlistTitle, ); } @@ -95,5 +99,6 @@ class Video extends Equatable { url, isParsed, filename, + playlistTitle, ]; } diff --git a/lib/screens/home.dart b/lib/screens/home.dart index f66e2fd..cf84900 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -12,6 +12,16 @@ class Home extends StatelessWidget { return Scaffold( appBar: AppBar( title: Text('home').tr(), + actions: [ + SizedBox( + width: 50, + child: IconButton( + icon: Icon(Icons.settings), + onPressed: () { + Navigator.pushNamed(context, '/settings'); + }, + )) + ], ), body: LayoutBuilder(builder: (BuildContext context, BoxConstraints viewportConstraints) { @@ -64,10 +74,3 @@ class Home extends StatelessWidget { })); } } -/* -SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: viewportConstraints.maxHeight), - child: - */ \ No newline at end of file diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart new file mode 100644 index 0000000..f7d4d3b --- /dev/null +++ b/lib/screens/settings.dart @@ -0,0 +1,115 @@ +import 'dart:io'; + +import 'package:downloadsfolder/downloadsfolder.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:filesystem_picker/filesystem_picker.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class Settings extends StatefulWidget { + Settings({super.key}); + + @override + State createState() => _SettingsState(); +} + +class _SettingsState extends State { + late Directory downloadFolder; + late SharedPreferences prefs; + + Future _init() async { + prefs = await SharedPreferences.getInstance(); + Directory defaultDownloadDirectory = await getDownloadDirectory(); + downloadFolder = Directory( + prefs.getString('downloadFolder') ?? defaultDownloadDirectory.path); + return 'ok'; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('settings').tr(), + ), + body: FutureBuilder( + future: _init(), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return CircularProgressIndicator(); + } else if (snapshot.hasError) { + return Center(child: Text('Error: ${snapshot.error}')); + } else if (snapshot.hasData) { + return LayoutBuilder(builder: + (BuildContext context, BoxConstraints viewportConstraints) { + return viewportConstraints.maxHeight < double.infinity + ? Center( + child: Container( + constraints: viewportConstraints, + child: Column( + children: [ + SizedBox(height: 10), + Text('settings').tr(), + SizedBox(height: 10), + Flexible( + child: ListView( + children: [ + ListTile( + title: Text('download_folder').tr(), + subtitle: Text(downloadFolder.path), + trailing: IconButton( + icon: Icon(Icons.folder), + onPressed: () async { + String? selectedDirectory = + await FilePicker.platform + .getDirectoryPath(); + + if (selectedDirectory != null) { + prefs.setString('downloadFolder', + selectedDirectory); + setState(() { + downloadFolder = + Directory(selectedDirectory); + }); + } + }, + ), + ), + ListTile( + title: Text('language').tr(), + subtitle: + Text(context.locale.toString()).tr(), + trailing: DropdownButton( + items: context.supportedLocales + .map((locale) { + return DropdownMenuItem( + value: locale.toString(), + child: Text(locale.toString()).tr(), + ); + }).toList(), + value: context.locale.toString(), + onChanged: (String? value) => { + if (value != null) + { + context.setLocale(Locale( + value.split("_")[0], + value.split("_")[1])), + } + }, + ), + ) + ], + )), + ], + ), + ), + ) + : CircularProgressIndicator(); + }); + } else { + return Center(child: Text('No data')); + } + }, + )); + } +} diff --git a/lib/translations/en-US.json b/lib/translations/en-US.json index 811d59e..ace7174 100644 --- a/lib/translations/en-US.json +++ b/lib/translations/en-US.json @@ -2,5 +2,10 @@ "test": "EN_test", "home": "EN_home", "home_intro": "EN_home_intro", - "search_url": "EN_search_url" + "search_url": "EN_search_url", + "settings": "EN_Settings", + "download_folder": "Download folder", + "en_US": "English", + "fr_FR": "French", + "Ok": "OK" } \ No newline at end of file diff --git a/lib/translations/fr-FR.json b/lib/translations/fr-FR.json index ab26c04..b0d82df 100644 --- a/lib/translations/fr-FR.json +++ b/lib/translations/fr-FR.json @@ -2,5 +2,10 @@ "test": "Test", "home": "Accueil", "home_intro": "Saisissez l'url d'une video et laissez la magie opérer!", - "search_url": "Url" + "search_url": "Url", + "settings": "Paramètres", + "download_folder": "Dossier de téléchargement", + "en_US": "Anglais", + "fr_FR": "Français", + "Ok": "OK" } \ No newline at end of file diff --git a/lib/videoList/cubit/videos_cubit.dart b/lib/videoList/cubit/videos_cubit.dart index ea4c175..c22bb83 100644 --- a/lib/videoList/cubit/videos_cubit.dart +++ b/lib/videoList/cubit/videos_cubit.dart @@ -91,9 +91,28 @@ class VideosCubit extends Cubit { final tmpFile = File('temp/${video.filename}_done.${video.format.extension}'); + var playlistTitle = video.playlistTitle; + await Directory('${downloadDirectory.path}/$playlistTitle') + .create(recursive: true) + .catchError((e) => debugPrint('Error creating directory: $e')); + debugPrint('Playlist title: $playlistTitle'); + if (playlistTitle.isNotEmpty && playlistTitle != '') { + cleanTitle = playlistTitle + '/' + cleanTitle; + } + final newFile = File( - '${downloadDirectory.path}/${cleanTitle}.${video.format.extension}'); - await tmpFile.rename(newFile.path); + '${downloadDirectory.path}/$cleanTitle.${video.format.extension}'); + + var isMoved = false; + while (!isMoved) { + try { + await tmpFile.rename(newFile.path); + isMoved = true; + } on FileSystemException catch (e) { + debugPrint('Error moving file: $e'); + await Future.delayed(Duration(seconds: 1)); + } + } changeStatus(video, 'done'); debugPrint('File moved to ${newFile.path}'); } diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 65af100..46265d5 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -4,7 +4,7 @@ project(runner LANGUAGES CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. -set(BINARY_NAME "notube") +set(BINARY_NAME "noTube") # The unique GTK application identifier for this application. See: # https://wiki.gnome.org/HowDoI/ChooseApplicationID set(APPLICATION_ID "com.example.notube") diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig index c50f591..7dbec36 100644 --- a/macos/Runner/Configs/AppInfo.xcconfig +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -5,7 +5,7 @@ // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = notube +PRODUCT_NAME = noTube // The application's bundle identifier PRODUCT_BUNDLE_IDENTIFIER = com.example.notube diff --git a/pubspec.lock b/pubspec.lock index 17eab51..265c0f6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" + url: "https://pub.dev" + source: hosted + version: "0.3.4+1" crypto: dependency: transitive description: @@ -169,6 +177,22 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: "824f5b9f389bfc4dddac3dea76cd70c51092d9dff0b2ece7ef4f53db8547d258" + url: "https://pub.dev" + source: hosted + version: "8.0.6" + filesystem_picker: + dependency: "direct main" + description: + name: filesystem_picker + sha256: cc2bfe7e5a4ce21afd5b1b03824c0e6e5386a981ed6cce7bda062b1af805cf62 + url: "https://pub.dev" + source: hosted + version: "4.1.0" flutter: dependency: "direct main" description: flutter @@ -203,6 +227,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: c6b0b4c05c458e1c01ad9bcc14041dd7b1f6783d487be4386f793f47a8a4d03e + url: "https://pub.dev" + source: hosted + version: "2.0.20" flutter_redux: dependency: "direct main" description: @@ -301,6 +333,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + logger: + dependency: transitive + description: + name: logger + sha256: af05cc8714f356fd1f3888fb6741cbe9fbe25cdb6eedbab80e1a6db21047d4a4 + url: "https://pub.dev" + source: hosted + version: "2.3.0" matcher: dependency: transitive description: @@ -525,6 +565,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.0" + rename: + dependency: "direct main" + description: + name: rename + sha256: "6ef5daf4b11130e71d93630cfb70725e5a35b19039739cfcd2b272c834ba25fe" + url: "https://pub.dev" + source: hosted + version: "3.0.2" screen_retriever: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c688415..b5afae2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,9 +1,9 @@ name: notube -description: NoTube +description: noTube publish_to: "none" # Remove this line if you wish to publish to pub.dev -version: 0.0.1+1 +version: 0.0.18 environment: sdk: ">=3.0.0 <4.0.0" @@ -30,6 +30,9 @@ dependencies: bloc: ^8.1.4 shared_preferences: ^2.2.3 downloadsfolder: ^1.1.0 + filesystem_picker: ^4.1.0 + file_picker: ^8.0.6 + rename: ^3.0.2 dev_dependencies: flutter_test: diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp index fefeb83..b5bf23b 100644 --- a/windows/runner/main.cpp +++ b/windows/runner/main.cpp @@ -27,7 +27,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); - if (!window.Create(L"notube", origin, size)) { + if (!window.Create(L"noTube", origin, size)) { return EXIT_FAILURE; } window.SetQuitOnClose(true);