Merge branch 'feat/parameters' into 'main'

Feat/parameters

See merge request jscampucci/libu-notube!2
This commit is contained in:
jscampucci 2024-07-02 13:40:35 +00:00
commit b3e7a4b643
20 changed files with 288 additions and 61 deletions

View File

@ -1,6 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application <application
android:label="notube" android:label="noTube"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity

View File

@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Notube</string> <string>noTube</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
@ -13,7 +13,7 @@
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>notube</string> <string>noTube</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>

View File

@ -48,7 +48,7 @@ enum Format {
'"18/22/bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best"', '"18/22/bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best"',
this.format = 'MP4', this.format = 'MP4',
this.ffmpegCmd = '', this.ffmpegCmd = '',
this.extension = '', this.extension = 'mp4',
}); });
} }

View File

@ -2,7 +2,6 @@ import 'dart:convert';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:notube/constants.dart'; import 'package:notube/constants.dart';
import 'package:notube/models/video.dart'; import 'package:notube/models/video.dart';
import 'package:notube/services/download.dart'; import 'package:notube/services/download.dart';
@ -29,6 +28,9 @@ class DlFormCubit extends Cubit<DlFormState> {
void parseUrl() async { void parseUrl() async {
dlService = await DLServices.init(); dlService = await DLServices.init();
if (state.url.isEmpty) {
return;
}
var shellStream = await dlService.analyseUrl(state.url); var shellStream = await dlService.analyseUrl(state.url);
shellStream.listen((line) { shellStream.listen((line) {
@ -43,7 +45,8 @@ class DlFormCubit extends Cubit<DlFormState> {
var video = Video.fromJson(videoTmp); var video = Video.fromJson(videoTmp);
videos.add(video.copyWith( videos.add(video.copyWith(
format: state.format, format: state.format,
filename: '${video.id}_${state.format.format}')); filename: '${video.id}_${state.format.format}',
playlistTitle: playlistTitle));
} }
} else { } else {
var video = Video.fromJson(dataInfos); var video = Video.fromJson(dataInfos);

View File

@ -50,7 +50,7 @@ class DlForm extends StatelessWidget {
DropdownFormat(), DropdownFormat(),
SubmitButton(), SubmitButton(),
]), ]),
DebugDlFormState(), //DebugDlFormState(),
], ],
); );
} }

View File

@ -25,7 +25,7 @@ class DropdownFormat extends StatelessWidget {
onChanged: (String? value) { onChanged: (String? value) {
var format = var format =
Format.values.firstWhere((element) => element.format == value); Format.values.firstWhere((element) => element.format == value);
context.read<DlFormCubit>().setFormat(format!); context.read<DlFormCubit>().setFormat(format);
}, },
underline: Container(), underline: Container(),
items: Format.values.map<DropdownMenuItem<String>>((Format format) { items: Format.values.map<DropdownMenuItem<String>>((Format format) {

View File

@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter/rendering.dart';
import 'package:notube/constants.dart'; import 'package:notube/constants.dart';
import 'package:notube/models/video.dart'; import 'package:notube/models/video.dart';
import 'package:notube/videoList/cubit/videos_cubit.dart'; import 'package:notube/videoList/cubit/videos_cubit.dart';
@ -35,23 +37,27 @@ class _SubmitButtonState extends State<SubmitButton> {
constraints: BoxConstraints.tightFor(width: 200), constraints: BoxConstraints.tightFor(width: 200),
child: MouseRegion( child: MouseRegion(
cursor: SystemMouseCursors.click, cursor: SystemMouseCursors.click,
onHover: (PointerHoverEvent event) {
setState(() => isHovering = true);
},
onExit: (PointerExitEvent event) {
setState(() => isHovering = false);
},
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
context.read<DlFormCubit>().parseUrl(); context.read<DlFormCubit>().parseUrl();
}, },
child: InkWell( child: Container(
onHover: (hovering) { padding: const EdgeInsets.symmetric(vertical: 14),
debugPrint('Hovering: $hovering'); decoration: BoxDecoration(
setState(() => isHovering = hovering); color: isHovering ? colorDarkRed : colorMainRed,
}, borderRadius: BorderRadius.only(
child: Container( topRight: Radius.circular(3),
padding: const EdgeInsets.symmetric(vertical: 14), bottomRight: Radius.circular(3),
decoration: BoxDecoration( )),
color: isHovering ? colorDarkRed : colorMainRed, child: const Text('Ok', textAlign: TextAlign.center).tr(),
), ),
child: const Text('Ok', textAlign: TextAlign.center).tr(), )),
),
))),
)); ));
} }
} }

View File

@ -11,25 +11,34 @@ class UrlTextField extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Flexible( return Flexible(
child: TextFormField( child: Container(
decoration: InputDecoration( decoration: BoxDecoration(
border: InputBorder.none, color: colorBackgroundBlack,
hintText: 'search_url'.tr(), borderRadius: BorderRadius.only(
filled: true, topLeft: Radius.circular(3),
fillColor: colorBackgroundBlack, bottomLeft: Radius.circular(3),
contentPadding: EdgeInsets.symmetric(
horizontal: 20,
), ),
), ),
validator: (value) { child: TextFormField(
if (value == null || value.isEmpty) { decoration: InputDecoration(
return 'Please enter some text'; border: InputBorder.none,
} hintText: 'search_url'.tr(),
return null; filled: true,
}, fillColor: Colors.transparent,
onChanged: (newUrl) { contentPadding: EdgeInsets.symmetric(
context.read<DlFormCubit>().setUrl(newUrl); horizontal: 20,
}, ),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter some text';
}
return null;
},
onChanged: (newUrl) {
context.read<DlFormCubit>().setUrl(newUrl);
},
),
), ),
); );
} }

View File

@ -1,7 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:notube/screens/settings.dart';
import 'package:notube/wrapper.dart'; import 'package:notube/wrapper.dart';
import 'package:notube/constants.dart'; import 'package:notube/constants.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'package:upgrader/upgrader.dart'; import 'package:upgrader/upgrader.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@ -15,7 +17,7 @@ void main() async {
WindowOptions windowOptions = WindowOptions( WindowOptions windowOptions = WindowOptions(
size: Size(800, 600), size: Size(800, 600),
minimumSize: Size(320, 400), minimumSize: Size(320, 400),
title: 'NoTube', title: 'noTube',
); );
windowManager.waitUntilReadyToShow(windowOptions, () async { windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.setIcon('assets/images/icon.png'); await windowManager.setIcon('assets/images/icon.png');
@ -63,9 +65,13 @@ class _NoTubeAppState extends State<NoTubeApp> {
localizationsDelegates: context.localizationDelegates, localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales, supportedLocales: context.supportedLocales,
locale: context.locale, locale: context.locale,
home: BlocProvider( initialRoute: '/',
create: (context) => VideosCubit(), routes: {
child: UpgradeAlert(upgrader: upgrader, child: const Wrapper())), '/': (context) => BlocProvider(
create: (context) => VideosCubit(),
child: UpgradeAlert(upgrader: upgrader, child: const Wrapper())),
'/settings': (context) => Settings(),
},
); );
} }
} }

View File

@ -18,6 +18,7 @@ class Video extends Equatable {
this.uploader = '', this.uploader = '',
this.uploadDate = '', this.uploadDate = '',
this.filename = '', this.filename = '',
this.playlistTitle = '',
}); });
final String id; final String id;
@ -33,6 +34,7 @@ class Video extends Equatable {
final String uploader; final String uploader;
final String uploadDate; final String uploadDate;
final String filename; final String filename;
final String playlistTitle;
factory Video.fromJson(Map<String, dynamic> json) { factory Video.fromJson(Map<String, dynamic> json) {
return Video( return Video(
@ -63,6 +65,7 @@ class Video extends Equatable {
String? uploader, String? uploader,
String? uploadDate, String? uploadDate,
String? filename, String? filename,
String? playlistTitle,
}) { }) {
return Video( return Video(
id: id ?? this.id, id: id ?? this.id,
@ -78,6 +81,7 @@ class Video extends Equatable {
uploader: uploader ?? this.uploader, uploader: uploader ?? this.uploader,
uploadDate: uploadDate ?? this.uploadDate, uploadDate: uploadDate ?? this.uploadDate,
filename: filename ?? this.filename, filename: filename ?? this.filename,
playlistTitle: playlistTitle ?? this.playlistTitle,
); );
} }
@ -95,5 +99,6 @@ class Video extends Equatable {
url, url,
isParsed, isParsed,
filename, filename,
playlistTitle,
]; ];
} }

View File

@ -12,6 +12,16 @@ class Home extends StatelessWidget {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('home').tr(), title: Text('home').tr(),
actions: [
SizedBox(
width: 50,
child: IconButton(
icon: Icon(Icons.settings),
onPressed: () {
Navigator.pushNamed(context, '/settings');
},
))
],
), ),
body: LayoutBuilder(builder: body: LayoutBuilder(builder:
(BuildContext context, BoxConstraints viewportConstraints) { (BuildContext context, BoxConstraints viewportConstraints) {
@ -64,10 +74,3 @@ class Home extends StatelessWidget {
})); }));
} }
} }
/*
SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight),
child:
*/

115
lib/screens/settings.dart Normal file
View File

@ -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<Settings> createState() => _SettingsState();
}
class _SettingsState extends State<Settings> {
late Directory downloadFolder;
late SharedPreferences prefs;
Future<String> _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<String>(
future: _init(),
builder: (BuildContext context, AsyncSnapshot<String> 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'));
}
},
));
}
}

View File

@ -2,5 +2,10 @@
"test": "EN_test", "test": "EN_test",
"home": "EN_home", "home": "EN_home",
"home_intro": "EN_home_intro", "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"
} }

View File

@ -2,5 +2,10 @@
"test": "Test", "test": "Test",
"home": "Accueil", "home": "Accueil",
"home_intro": "Saisissez l'url d'une video et laissez la magie opérer!", "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"
} }

View File

@ -91,9 +91,28 @@ class VideosCubit extends Cubit<VideosState> {
final tmpFile = final tmpFile =
File('temp/${video.filename}_done.${video.format.extension}'); 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( final newFile = File(
'${downloadDirectory.path}/${cleanTitle}.${video.format.extension}'); '${downloadDirectory.path}/$cleanTitle.${video.format.extension}');
await tmpFile.rename(newFile.path);
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'); changeStatus(video, 'done');
debugPrint('File moved to ${newFile.path}'); debugPrint('File moved to ${newFile.path}');
} }

View File

@ -4,7 +4,7 @@ project(runner LANGUAGES CXX)
# The name of the executable created for the application. Change this to change # The name of the executable created for the application. Change this to change
# the on-disk name of your application. # the on-disk name of your application.
set(BINARY_NAME "notube") set(BINARY_NAME "noTube")
# The unique GTK application identifier for this application. See: # The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID # https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.example.notube") set(APPLICATION_ID "com.example.notube")

View File

@ -5,7 +5,7 @@
// 'flutter create' template. // 'flutter create' template.
// The application's name. By default this is also the title of the Flutter window. // 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 // The application's bundle identifier
PRODUCT_BUNDLE_IDENTIFIER = com.example.notube PRODUCT_BUNDLE_IDENTIFIER = com.example.notube

View File

@ -57,6 +57,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.18.0" 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: crypto:
dependency: transitive dependency: transitive
description: description:
@ -169,6 +177,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.0" 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: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -203,6 +227,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" 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: flutter_redux:
dependency: "direct main" dependency: "direct main"
description: description:
@ -301,6 +333,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.1" version: "2.1.1"
logger:
dependency: transitive
description:
name: logger
sha256: af05cc8714f356fd1f3888fb6741cbe9fbe25cdb6eedbab80e1a6db21047d4a4
url: "https://pub.dev"
source: hosted
version: "2.3.0"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@ -525,6 +565,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.4.0" 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: screen_retriever:
dependency: transitive dependency: transitive
description: description:

View File

@ -1,9 +1,9 @@
name: notube name: notube
description: NoTube description: noTube
publish_to: "none" # Remove this line if you wish to publish to pub.dev publish_to: "none" # Remove this line if you wish to publish to pub.dev
version: 0.0.1+1 version: 0.0.18
environment: environment:
sdk: ">=3.0.0 <4.0.0" sdk: ">=3.0.0 <4.0.0"
@ -30,6 +30,9 @@ dependencies:
bloc: ^8.1.4 bloc: ^8.1.4
shared_preferences: ^2.2.3 shared_preferences: ^2.2.3
downloadsfolder: ^1.1.0 downloadsfolder: ^1.1.0
filesystem_picker: ^4.1.0
file_picker: ^8.0.6
rename: ^3.0.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@ -27,7 +27,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
FlutterWindow window(project); FlutterWindow window(project);
Win32Window::Point origin(10, 10); Win32Window::Point origin(10, 10);
Win32Window::Size size(1280, 720); Win32Window::Size size(1280, 720);
if (!window.Create(L"notube", origin, size)) { if (!window.Create(L"noTube", origin, size)) {
return EXIT_FAILURE; return EXIT_FAILURE;
} }
window.SetQuitOnClose(true); window.SetQuitOnClose(true);