diff --git a/android/app/build.gradle b/android/app/build.gradle
index 96177047..b4afdf7f 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -67,7 +67,7 @@ android {
defaultConfig {
applicationId "com.github.lamarios.clipious"
- minSdkVersion 21
+ minSdkVersion 24
targetSdkVersion 34
versionCode buildNumber
versionName flutterVersionName
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 016ec939..84adf226 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -8,6 +8,7 @@
+
super.videoId;
@JsonKey(includeFromJson: false, includeToJson: false)
- Future get mediaPath async {
+ Future get downloadPath async {
Directory dir = await _getDownloadFolder();
- return "${dir.path}/$videoId.${audioOnly ? 'webm' : 'mp4'}";
+
+ return "${dir.path}/$videoId.${audioOnly ? 'webm' : 'webm'}";
+ }
+
+ /// Effective path for legacy reasons, old versions videos would be mp4, now it's all webm
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ Future get effectivePath async {
+ Directory dir = await _getDownloadFolder();
+
+ if (audioOnly) {
+ return "${dir.path}/$videoId.webm";
+ } else {
+ final path = "${dir.path}/$videoId.webm";
+ if (!await File(path).exists()) {
+ return "${dir.path}/$videoId.mp4";
+ }
+
+ return path;
+ }
}
@JsonKey(includeFromJson: false, includeToJson: false)
diff --git a/lib/downloads/states/download_manager.dart b/lib/downloads/states/download_manager.dart
index 3a0c2fa3..4a9a21c7 100644
--- a/lib/downloads/states/download_manager.dart
+++ b/lib/downloads/states/download_manager.dart
@@ -3,14 +3,17 @@ import 'dart:io';
import 'package:bloc/bloc.dart';
import 'package:dio/dio.dart';
+import 'package:downloadsfolder/downloadsfolder.dart';
import 'package:easy_debounce/easy_throttle.dart';
+import 'package:ffmpeg_kit_flutter_full/ffmpeg_kit.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:invidious/downloads/models/downloaded_video.dart';
import 'package:invidious/extensions.dart';
import 'package:invidious/globals.dart';
import 'package:invidious/utils/models/image_object.dart';
-import 'package:invidious/videos/models/format_stream.dart';
import 'package:logging/logging.dart';
+import 'package:path_provider/path_provider.dart';
+import 'package:path/path.dart' as p;
import '../../player/states/player.dart';
import '../../videos/models/adaptive_format.dart';
@@ -71,8 +74,9 @@ class DownloadManagerCubit extends Cubit {
.toList());
}
- onProgress(int count, int total, DownloadedVideo video) async {
- if (count == total) {
+ onProgress(int count, int total, DownloadedVideo video,
+ {required int step, required int totalSteps}) async {
+ if (count == total && step == totalSteps) {
var progresses =
Map.from(state.downloadProgresses);
var downloadProgress = progresses[video.videoId];
@@ -119,18 +123,23 @@ class DownloadManagerCubit extends Cubit {
quality: quality);
await db.upsertDownload(downloadedVideo);
- String contentUrl;
+ String? videoUrl;
if (!audioOnly) {
- FormatStream stream = vid.formatStreams
- .firstWhere((element) => element.resolution == quality);
- contentUrl = stream.url;
- } else {
- AdaptiveFormat audio = vid.adaptiveFormats
- .sortByReversed((e) => int.parse(e.bitrate ?? "0"))
- .firstWhere((element) => element.type.contains("audio"));
- contentUrl = audio.url;
+ // FormatStream stream = vid.formatStreams
+ // .firstWhere((element) => element.resolution == quality);
+
+ final stream = vid.adaptiveFormats.firstWhere((element) =>
+ element.encoding == 'vp9' &&
+ element.qualityLabel == quality &&
+ element.type.contains('video/webm'));
+
+ videoUrl = stream.url;
}
+ AdaptiveFormat audio = vid.adaptiveFormats
+ .sortByReversed((e) => int.parse(e.bitrate ?? "0"))
+ .firstWhere((element) => element.type.contains("audio"));
+ String audioUrl = audio.url;
Dio dio = Dio();
CancelToken cancelToken = CancelToken();
@@ -152,22 +161,74 @@ class DownloadManagerCubit extends Cubit {
}
// download video
- var videoPath = await downloadedVideo.mediaPath;
+ var mediaPath = await downloadedVideo.downloadPath;
+
+ final tempDir = await getTemporaryDirectory();
+ final audioPath = '${tempDir.path}/${videoId}_audio.webm';
+ final videoPath = '${tempDir.path}/${videoId}_video.webm';
log.info(
- "Downloading video ${vid.title}, audioOnly ? $audioOnly, quality: $quality (if not only audio) to path: $videoPath");
- dio
- .download(contentUrl, videoPath,
- onReceiveProgress: (count, total) =>
- onProgress(count, total, downloadedVideo),
- cancelToken: cancelToken,
- deleteOnError: true)
- .catchError((err) {
- onDownloadError(err, downloadedVideo);
- return Response(requestOptions: RequestOptions());
- });
+ "Downloading video ${vid.title}, audioOnly ? $audioOnly, quality: $quality to path: $tempDir");
+ try {
+ await dio
+ .download(audioUrl, audioPath,
+ onReceiveProgress: (count, total) => onProgress(
+ count, total, downloadedVideo,
+ step: 1, totalSteps: audioOnly ? 2 : 3),
+ cancelToken: cancelToken,
+ deleteOnError: true)
+ .catchError((err) {
+ onDownloadError(err, downloadedVideo);
+ return Response(requestOptions: RequestOptions());
+ });
+
+ if (videoUrl != null) {
+ await dio
+ .download(videoUrl, videoPath,
+ onReceiveProgress: (count, total) => onProgress(
+ count,
+ total,
+ downloadedVideo,
+ step: 2,
+ totalSteps: 3,
+ ),
+ cancelToken: cancelToken,
+ deleteOnError: true)
+ .catchError((err) {
+ onDownloadError(err, downloadedVideo);
+ return Response(requestOptions: RequestOptions());
+ });
+ }
+
+ if (audioOnly) {
+ final audio = File(audioPath);
+ await audio.copy(mediaPath);
+ onProgress(1, 1, downloadedVideo, step: 2, totalSteps: 2);
+ return true;
+ } else {
+ final session = await FFmpegKit.execute(
+ '-y -i $videoPath -i $audioPath -c:v copy -c:a copy $mediaPath');
+
+ final returnCode = await session.getReturnCode();
+
+ onProgress(1, 1, downloadedVideo, step: 3, totalSteps: 3);
+
+ return returnCode?.isValueSuccess() ?? false;
+ }
+ } catch (e) {
+ rethrow;
+ } finally {
+ final audio = File(audioPath);
+ final video = File(videoPath);
+
+ if (await audio.exists()) {
+ await audio.delete();
+ }
- return true;
+ if (await video.exists()) {
+ await video.delete();
+ }
+ }
}
}
@@ -182,7 +243,7 @@ class DownloadManagerCubit extends Cubit {
progresses.remove(vid.videoId);
try {
- String path = await vid.mediaPath;
+ String path = await vid.effectivePath;
await File(path).delete();
} catch (e) {
log.fine('File might not be available, that\'s ok');
@@ -210,7 +271,7 @@ class DownloadManagerCubit extends Cubit {
"Failed to download video ${vid.title}, removing it", err.stackTrace);
vid.downloadFailed = true;
vid.downloadComplete = false;
- onProgress(1, 1, vid);
+ onProgress(1, 1, vid, step: 1, totalSteps: 1);
var progresses =
Map.from(state.downloadProgresses);
progresses.remove(vid.videoId);
@@ -245,6 +306,24 @@ class DownloadManagerCubit extends Cubit {
bool canPlayAll() =>
state.videos.where((element) => element.downloadComplete).isNotEmpty;
+
+ Future copyToDownloadFolder(DownloadedVideo v) async {
+ final downloads = await getDownloadsDirectory();
+ if (downloads != null) {
+ if (!await downloads.exists()) {
+ downloads.create(recursive: true);
+ }
+ final file = File(await v.effectivePath);
+ if (await file.exists()) {
+ final fileName =
+ '${v.title.replaceAll(RegExp(r'[^a-zA-Z0-9]'), '_').replaceAll(RegExp(r'_{2,}'), '')}_${v.videoId}${p.extension(file.path)}';
+
+ bool? success = await copyFileIntoDownloadFolder(file.path, fileName);
+ log.info(
+ "file ${file.path} copied to download folder as $fileName (success ?$success)");
+ }
+ }
+ }
}
@freezed
diff --git a/lib/downloads/views/components/downloaded_video.dart b/lib/downloads/views/components/downloaded_video.dart
index 3393e0a6..215623eb 100644
--- a/lib/downloads/views/components/downloaded_video.dart
+++ b/lib/downloads/views/components/downloaded_video.dart
@@ -14,6 +14,61 @@ class DownloadedVideoView extends StatelessWidget {
const DownloadedVideoView({super.key, required this.video});
+ openVideoSheet(BuildContext context, DownloadedVideo v) {
+ var cubit = context.read();
+ final locals = AppLocalizations.of(context)!;
+ showModalBottomSheet(
+ enableDrag: true,
+ showDragHandle: true,
+ context: context,
+ builder: (ctx) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 32.0),
+ child: Wrap(
+ alignment: WrapAlignment.center,
+ runSpacing: 16,
+ spacing: 16,
+ children: [
+ Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ IconButton.filledTonal(
+ onPressed: () async {
+ Navigator.of(ctx).pop();
+ await cubit.copyToDownloadFolder(v);
+ if (context.mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(SnackBar(
+ content:
+ Text(locals.fileCopiedToDownloadFolder)));
+ }
+ },
+ icon: const Icon(Icons.copy)),
+ Text(locals.copyToDownloadFolder)
+ ],
+ ),
+ Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ IconButton.filledTonal(
+ onPressed: () async {
+ Navigator.of(ctx).pop();
+ await cubit.deleteVideo(v);
+ if (context.mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text(locals.videoDeleted)));
+ }
+ },
+ icon: const Icon(Icons.delete)),
+ Text(locals.delete)
+ ],
+ )
+ ],
+ ),
+ );
+ },
+ );
+ }
+
@override
Widget build(BuildContext context) {
var colors = Theme.of(context).colorScheme;
@@ -69,7 +124,16 @@ class DownloadedVideoView extends StatelessWidget {
);
}),
),
- )
+ ),
+ if (state.video?.downloadComplete ?? false)
+ IconButton(
+ onPressed: () => openVideoSheet(context, video),
+ icon: Icon(
+ Icons.more_vert,
+ color: colors.secondary,
+ ),
+ visualDensity: VisualDensity.compact,
+ )
],
),
),
diff --git a/lib/downloads/views/screens/download_manager.dart b/lib/downloads/views/screens/download_manager.dart
index 59fe9327..5fa655e6 100644
--- a/lib/downloads/views/screens/download_manager.dart
+++ b/lib/downloads/views/screens/download_manager.dart
@@ -2,7 +2,6 @@ import 'package:auto_route/annotations.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
-import 'package:flutter_swipe_action_cell/core/cell.dart';
import 'package:invidious/downloads/views/components/downloaded_video.dart';
import 'package:invidious/globals.dart';
@@ -48,24 +47,10 @@ class DownloadManagerScreen extends StatelessWidget {
itemCount: state.videos.length,
itemBuilder: (context, index) {
var v = state.videos[index];
- return SwipeActionCell(
- key:
- ValueKey('downloaded-video-${v.videoId}'),
- trailingActions: [
- SwipeAction(
- performsFirstActionWithFullSwipe: true,
- icon: const Icon(Icons.delete,
- color: Colors.white),
- onTap: (handler) async {
- await handler(true);
- cubit.deleteVideo(v);
- },
- )
- ],
- child: DownloadedVideoView(
- key: ValueKey(v.videoId),
- video: v,
- ));
+ return DownloadedVideoView(
+ key: ValueKey(v.videoId),
+ video: v,
+ );
},
),
),
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index 13694608..ac6f492e 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -1371,5 +1371,8 @@
"serverAlreadyExists": "Server already exists in settings",
"wrongThumbnailConfiguration": "The server is reachable but is not configured properly, the video and channel thumbnails will not be displayed properly. Disable the server test configuration if you are OK with this, fix your server otherwise",
"openWikiLink": "Open wiki for help",
- "serverUnreachable": "Server is unreachable, or is not a valid invidious server"
+ "serverUnreachable": "Server is unreachable, or is not a valid invidious server",
+ "copyToDownloadFolder": "Copy to download folder",
+ "fileCopiedToDownloadFolder": "File copied to download folder",
+ "videoDeleted": "Video deleted"
}
diff --git a/lib/player/states/audio_player.dart b/lib/player/states/audio_player.dart
index dc9cbbfa..ee48fdd4 100644
--- a/lib/player/states/audio_player.dart
+++ b/lib/player/states/audio_player.dart
@@ -139,7 +139,7 @@ class AudioPlayerCubit extends MediaPlayerCubit {
}
}
} else {
- String path = await state.offlineVideo!.mediaPath;
+ String path = await state.offlineVideo!.effectivePath;
source = AudioSource.file(path);
}
diff --git a/lib/player/states/video_player.dart b/lib/player/states/video_player.dart
index 4d20b2a2..e36155cb 100644
--- a/lib/player/states/video_player.dart
+++ b/lib/player/states/video_player.dart
@@ -246,7 +246,7 @@ class VideoPlayerCubit extends MediaPlayerCubit {
// getting data sources
if (offline) {
- String videoPath = await newState.offlineVideo!.mediaPath;
+ String videoPath = await newState.offlineVideo!.effectivePath;
betterPlayerDataSource = BetterPlayerDataSource(
BetterPlayerDataSourceType.file,
diff --git a/lib/videos/states/download_modal_sheet.dart b/lib/videos/states/download_modal_sheet.dart
index ad55eaae..dc57f152 100644
--- a/lib/videos/states/download_modal_sheet.dart
+++ b/lib/videos/states/download_modal_sheet.dart
@@ -1,17 +1,45 @@
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
+import 'package:invidious/extensions.dart';
+import 'package:invidious/globals.dart';
+import 'package:invidious/videos/models/adaptive_format.dart';
+
+import '../models/base_video.dart';
part 'download_modal_sheet.freezed.dart';
class DownloadModalSheetCubit extends Cubit {
- DownloadModalSheetCubit(super.initialState);
+ final BaseVideo video;
+
+ DownloadModalSheetCubit(super.initialState, this.video) {
+ init();
+ }
+
+ init() async {
+ emit(state.copyWith(loading: true));
+ final vid = await service.getVideo(video.videoId);
+
+ final qualities = vid.adaptiveFormats
+ .where((f) => f.encoding == 'vp9' && f.type.contains("video/webm"))
+ .sortBy((f) => int.parse(f.resolution?.replaceAll('p', '') ?? '0'))
+ .where(
+ (f) => f.qualityLabel != null && f.qualityLabel!.trim().isNotEmpty)
+ .toList();
+
+ emit(state.copyWith(
+ availableQualities: qualities,
+ loading: false,
+ quality: qualities.last.qualityLabel!));
+ }
setAudioOnly(bool value) {
emit(state.copyWith(audioOnly: value));
}
- setQuality(String quality) {
- emit(state.copyWith(quality: quality));
+ setQuality(String? quality) {
+ if (quality != null) {
+ emit(state.copyWith(quality: quality));
+ }
}
}
@@ -19,5 +47,7 @@ class DownloadModalSheetCubit extends Cubit {
class DownloadModalSheetState with _$DownloadModalSheetState {
const factory DownloadModalSheetState(
{@Default(false) bool audioOnly,
+ @Default(false) bool loading,
+ @Default([]) List availableQualities,
@Default('720p') String quality}) = _DownloadModalSheetState;
}
diff --git a/lib/videos/states/download_modal_sheet.freezed.dart b/lib/videos/states/download_modal_sheet.freezed.dart
index 8aea32fa..3efca7aa 100644
--- a/lib/videos/states/download_modal_sheet.freezed.dart
+++ b/lib/videos/states/download_modal_sheet.freezed.dart
@@ -17,6 +17,9 @@ final _privateConstructorUsedError = UnsupportedError(
/// @nodoc
mixin _$DownloadModalSheetState {
bool get audioOnly => throw _privateConstructorUsedError;
+ bool get loading => throw _privateConstructorUsedError;
+ List get availableQualities =>
+ throw _privateConstructorUsedError;
String get quality => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
@@ -30,7 +33,11 @@ abstract class $DownloadModalSheetStateCopyWith<$Res> {
$Res Function(DownloadModalSheetState) then) =
_$DownloadModalSheetStateCopyWithImpl<$Res, DownloadModalSheetState>;
@useResult
- $Res call({bool audioOnly, String quality});
+ $Res call(
+ {bool audioOnly,
+ bool loading,
+ List availableQualities,
+ String quality});
}
/// @nodoc
@@ -48,6 +55,8 @@ class _$DownloadModalSheetStateCopyWithImpl<$Res,
@override
$Res call({
Object? audioOnly = null,
+ Object? loading = null,
+ Object? availableQualities = null,
Object? quality = null,
}) {
return _then(_value.copyWith(
@@ -55,6 +64,14 @@ class _$DownloadModalSheetStateCopyWithImpl<$Res,
? _value.audioOnly
: audioOnly // ignore: cast_nullable_to_non_nullable
as bool,
+ loading: null == loading
+ ? _value.loading
+ : loading // ignore: cast_nullable_to_non_nullable
+ as bool,
+ availableQualities: null == availableQualities
+ ? _value.availableQualities
+ : availableQualities // ignore: cast_nullable_to_non_nullable
+ as List,
quality: null == quality
? _value.quality
: quality // ignore: cast_nullable_to_non_nullable
@@ -72,7 +89,11 @@ abstract class _$$DownloadModalSheetStateImplCopyWith<$Res>
__$$DownloadModalSheetStateImplCopyWithImpl<$Res>;
@override
@useResult
- $Res call({bool audioOnly, String quality});
+ $Res call(
+ {bool audioOnly,
+ bool loading,
+ List availableQualities,
+ String quality});
}
/// @nodoc
@@ -89,6 +110,8 @@ class __$$DownloadModalSheetStateImplCopyWithImpl<$Res>
@override
$Res call({
Object? audioOnly = null,
+ Object? loading = null,
+ Object? availableQualities = null,
Object? quality = null,
}) {
return _then(_$DownloadModalSheetStateImpl(
@@ -96,6 +119,14 @@ class __$$DownloadModalSheetStateImplCopyWithImpl<$Res>
? _value.audioOnly
: audioOnly // ignore: cast_nullable_to_non_nullable
as bool,
+ loading: null == loading
+ ? _value.loading
+ : loading // ignore: cast_nullable_to_non_nullable
+ as bool,
+ availableQualities: null == availableQualities
+ ? _value._availableQualities
+ : availableQualities // ignore: cast_nullable_to_non_nullable
+ as List,
quality: null == quality
? _value.quality
: quality // ignore: cast_nullable_to_non_nullable
@@ -108,18 +139,35 @@ class __$$DownloadModalSheetStateImplCopyWithImpl<$Res>
class _$DownloadModalSheetStateImpl implements _DownloadModalSheetState {
const _$DownloadModalSheetStateImpl(
- {this.audioOnly = false, this.quality = '720p'});
+ {this.audioOnly = false,
+ this.loading = false,
+ final List availableQualities = const [],
+ this.quality = '720p'})
+ : _availableQualities = availableQualities;
@override
@JsonKey()
final bool audioOnly;
+ @override
+ @JsonKey()
+ final bool loading;
+ final List _availableQualities;
+ @override
+ @JsonKey()
+ List get availableQualities {
+ if (_availableQualities is EqualUnmodifiableListView)
+ return _availableQualities;
+ // ignore: implicit_dynamic_type
+ return EqualUnmodifiableListView(_availableQualities);
+ }
+
@override
@JsonKey()
final String quality;
@override
String toString() {
- return 'DownloadModalSheetState(audioOnly: $audioOnly, quality: $quality)';
+ return 'DownloadModalSheetState(audioOnly: $audioOnly, loading: $loading, availableQualities: $availableQualities, quality: $quality)';
}
@override
@@ -129,11 +177,15 @@ class _$DownloadModalSheetStateImpl implements _DownloadModalSheetState {
other is _$DownloadModalSheetStateImpl &&
(identical(other.audioOnly, audioOnly) ||
other.audioOnly == audioOnly) &&
+ (identical(other.loading, loading) || other.loading == loading) &&
+ const DeepCollectionEquality()
+ .equals(other._availableQualities, _availableQualities) &&
(identical(other.quality, quality) || other.quality == quality));
}
@override
- int get hashCode => Object.hash(runtimeType, audioOnly, quality);
+ int get hashCode => Object.hash(runtimeType, audioOnly, loading,
+ const DeepCollectionEquality().hash(_availableQualities), quality);
@JsonKey(ignore: true)
@override
@@ -146,11 +198,17 @@ class _$DownloadModalSheetStateImpl implements _DownloadModalSheetState {
abstract class _DownloadModalSheetState implements DownloadModalSheetState {
const factory _DownloadModalSheetState(
{final bool audioOnly,
+ final bool loading,
+ final List availableQualities,
final String quality}) = _$DownloadModalSheetStateImpl;
@override
bool get audioOnly;
@override
+ bool get loading;
+ @override
+ List get availableQualities;
+ @override
String get quality;
@override
@JsonKey(ignore: true)
diff --git a/lib/videos/views/components/download_modal_sheet.dart b/lib/videos/views/components/download_modal_sheet.dart
index 21a0817a..74d682ea 100644
--- a/lib/videos/views/components/download_modal_sheet.dart
+++ b/lib/videos/views/components/download_modal_sheet.dart
@@ -1,14 +1,15 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:gap/gap.dart';
+import 'package:invidious/globals.dart';
import 'package:invidious/main.dart';
+import 'package:invidious/utils/pretty_bytes.dart';
import 'package:invidious/videos/states/download_modal_sheet.dart';
import '../../../downloads/states/download_manager.dart';
import '../../models/base_video.dart';
-const List qualities = ['144p', '360p', '720p'];
-
class DownloadModalSheet extends StatelessWidget {
final BaseVideo video;
@@ -66,49 +67,78 @@ class DownloadModalSheet extends StatelessWidget {
AppLocalizations locals = AppLocalizations.of(context)!;
return BlocProvider(
create: (BuildContext context) =>
- DownloadModalSheetCubit(const DownloadModalSheetState()),
+ DownloadModalSheetCubit(const DownloadModalSheetState(), video),
child: BlocBuilder(
builder: (context, state) {
var cubit = context.read();
- return Padding(
- padding: const EdgeInsets.all(8.0),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- Padding(
- padding: const EdgeInsets.only(bottom: 8.0),
- child: ToggleButtons(
- isSelected:
- qualities.map((e) => e == state.quality).toList(),
- onPressed: state.audioOnly
- ? null
- : (index) => cubit.setQuality(qualities[index]),
- children: qualities.map((e) => Text(e)).toList(),
- ),
- ),
- InkWell(
- onTap: () => cubit.setAudioOnly(!state.audioOnly),
- child: Row(
- children: [
- Text(locals.videoDownloadAudioOnly),
- Switch(
- value: state.audioOnly,
- onChanged: cubit.setAudioOnly,
- )
- ],
+ return AnimatedSwitcher(
+ duration: animationDuration,
+ switchInCurve: animationCurve,
+ switchOutCurve: animationCurve,
+ child: state.loading
+ ? const Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Padding(
+ padding: EdgeInsets.all(16.0),
+ child: CircularProgressIndicator(),
),
+ ],
+ )
+ : Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(bottom: 8.0),
+ child: Row(
+ children: [
+ Text(locals.quality),
+ const Gap(10),
+ DropdownButton(
+ value: state.availableQualities
+ .map((e) => e.qualityLabel)
+ .where((e) => e == state.quality)
+ .firstOrNull,
+ onChanged: state.audioOnly
+ ? null
+ : (value) => cubit.setQuality(value),
+ items: state.availableQualities
+ .map((e) => DropdownMenuItem(
+ value: e.qualityLabel,
+ child: Text(
+ '${e.qualityLabel ?? ''} (~${prettyBytes(double.parse(e.clen))})')))
+ .toList(),
+ ),
+ ],
+ ),
+ ),
+ InkWell(
+ onTap: () => cubit.setAudioOnly(!state.audioOnly),
+ child: Row(
+ children: [
+ Text(locals.videoDownloadAudioOnly),
+ const Gap(10),
+ Switch(
+ value: state.audioOnly,
+ onChanged: cubit.setAudioOnly,
+ )
+ ],
+ ),
+ ),
+ IconButton.filledTonal(
+ onPressed: () => downloadVideo(context, state),
+ icon: const Icon(Icons.download),
+ )
+ ],
+ ),
+ ],
),
- IconButton.filledTonal(
- onPressed: () => downloadVideo(context, state),
- icon: const Icon(Icons.download),
- )
- ],
- ),
- ],
- ),
+ ),
);
}),
);
diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift
index 79bd02b9..8dd8bad0 100644
--- a/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -10,7 +10,9 @@ import audio_session
import awesome_notifications
import awesome_notifications_core
import device_info_plus
+import downloadsfolder
import dynamic_color
+import ffmpeg_kit_flutter_full
import flutter_web_auth
import just_audio
import package_info_plus
@@ -26,7 +28,9 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AwesomeNotificationsPlugin.register(with: registry.registrar(forPlugin: "AwesomeNotificationsPlugin"))
AwesomeNotificationsCorePlugin.register(with: registry.registrar(forPlugin: "AwesomeNotificationsCorePlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
+ DownloadsfolderPlugin.register(with: registry.registrar(forPlugin: "DownloadsfolderPlugin"))
DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin"))
+ FFmpegKitFlutterPlugin.register(with: registry.registrar(forPlugin: "FFmpegKitFlutterPlugin"))
FlutterWebAuthPlugin.register(with: registry.registrar(forPlugin: "FlutterWebAuthPlugin"))
JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
diff --git a/pubspec.lock b/pubspec.lock
index e028229a..2f1ef444 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -374,6 +374,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.6"
+ dartx:
+ dependency: transitive
+ description:
+ name: dartx
+ sha256: "8b25435617027257d43e6508b5fe061012880ddfdaa75a71d607c3de2a13d244"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.2.0"
dbus:
dependency: transitive
description:
@@ -398,14 +406,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.0"
+ diacritic:
+ dependency: transitive
+ description:
+ name: diacritic
+ sha256: "96db5db6149cbe4aa3cfcbfd170aca9b7648639be7e48025f9d458517f807fe4"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.1.5"
dio:
dependency: "direct main"
description:
name: dio
- sha256: "11e40df547d418cc0c4900a9318b26304e665da6fa4755399a9ff9efd09034b5"
+ sha256: "0dfb6b6a1979dac1c1245e17cef824d7b452ea29bd33d3467269f9bef3715fb0"
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.6.0"
+ dio_web_adapter:
+ dependency: transitive
+ description:
+ name: dio_web_adapter
+ sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8"
url: "https://pub.dev"
source: hosted
- version: "5.4.3+1"
+ version: "2.0.0"
+ downloadsfolder:
+ dependency: "direct main"
+ description:
+ name: downloadsfolder
+ sha256: e9987e56b998e3788047f977d31a1b50b4af58c388aefc3d157b9ac5c5d786a2
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.0"
dynamic_color:
dependency: "direct main"
description:
@@ -446,6 +478,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.2"
+ ffmpeg_kit_flutter_full:
+ dependency: "direct main"
+ description:
+ name: ffmpeg_kit_flutter_full
+ sha256: c845c1bcf40a8869eab9e401526c6cf3fc8091d04c0a7ec9a0af854d6c7e354a
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.0.3"
+ ffmpeg_kit_flutter_platform_interface:
+ dependency: transitive
+ description:
+ name: ffmpeg_kit_flutter_platform_interface
+ sha256: addf046ae44e190ad0101b2fde2ad909a3cd08a2a109f6106d2f7048b7abedee
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.1"
file:
dependency: transitive
description:
@@ -961,10 +1009,10 @@ packages:
dependency: "direct main"
description:
name: path_provider
- sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161
+ sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378
url: "https://pub.dev"
source: hosted
- version: "2.1.3"
+ version: "2.1.4"
path_provider_android:
dependency: transitive
description:
@@ -1001,10 +1049,58 @@ packages:
dependency: transitive
description:
name: path_provider_windows
- sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
+ sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
- version: "2.2.1"
+ version: "2.3.0"
+ permission_handler:
+ dependency: transitive
+ description:
+ name: permission_handler
+ sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb"
+ url: "https://pub.dev"
+ source: hosted
+ version: "11.3.1"
+ permission_handler_android:
+ dependency: transitive
+ description:
+ name: permission_handler_android
+ sha256: "76e4ab092c1b240d31177bb64d2b0bea43f43d0e23541ec866151b9f7b2490fa"
+ url: "https://pub.dev"
+ source: hosted
+ version: "12.0.12"
+ permission_handler_apple:
+ dependency: transitive
+ description:
+ name: permission_handler_apple
+ sha256: e6f6d73b12438ef13e648c4ae56bd106ec60d17e90a59c4545db6781229082a0
+ url: "https://pub.dev"
+ source: hosted
+ version: "9.4.5"
+ permission_handler_html:
+ dependency: transitive
+ description:
+ name: permission_handler_html
+ sha256: af26edbbb1f2674af65a8f4b56e1a6f526156bc273d0e65dd8075fab51c78851
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.1.3+2"
+ permission_handler_platform_interface:
+ dependency: transitive
+ description:
+ name: permission_handler_platform_interface
+ sha256: e9c8eadee926c4532d0305dff94b85bf961f16759c3af791486613152af4b4f9
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.2.3"
+ permission_handler_windows:
+ dependency: transitive
+ description:
+ name: permission_handler_windows
+ sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.1"
petitparser:
dependency: transitive
description:
@@ -1348,6 +1444,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.0"
+ time:
+ dependency: transitive
+ description:
+ name: time
+ sha256: ad8e018a6c9db36cb917a031853a1aae49467a93e0d464683e029537d848c221
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.4"
timing:
dependency: transitive
description:
@@ -1525,13 +1629,13 @@ packages:
source: hosted
version: "1.1.0"
web:
- dependency: transitive
+ dependency: "direct overridden"
description:
name: web
- sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
+ sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062
url: "https://pub.dev"
source: hosted
- version: "0.5.1"
+ version: "1.0.0"
web_socket_channel:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 48de2113..567680ac 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -18,10 +18,12 @@ dependencies:
copy_with_extension: 5.0.4
cronet_http: 1.3.2
device_info_plus: 10.1.0
- dio: 5.4.3+1
+ dio: 5.6.0
+ downloadsfolder: 1.1.0
dynamic_color: 1.7.0
easy_debounce: 2.0.3
feedback: 3.1.0
+ ffmpeg_kit_flutter_full: 6.0.3
flutter_animate: 4.5.0
flutter_bloc: 8.1.5
flutter_displaymode: 0.6.0
@@ -40,7 +42,7 @@ dependencies:
logging: 1.2.0
package_info_plus: 8.0.0
path: any
- path_provider: 2.1.3
+ path_provider: 2.1.4
receive_sharing_intent: 1.8.0
sembast: 3.7.1
sembast_sqflite: 2.2.0
@@ -87,7 +89,7 @@ dependency_overrides:
# cupertino_icons: ^1.0.2
# js: 0.7.1
meta: 1.14.0
- # web: 0.5.0
+ web: 1.0.0
flutter:
assets:
- assets/icon.svg
diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc
index cd568901..827ecca1 100644
--- a/windows/flutter/generated_plugin_registrant.cc
+++ b/windows/flutter/generated_plugin_registrant.cc
@@ -8,7 +8,9 @@
#include
#include
+#include
#include
+#include
#include
#include
@@ -17,8 +19,12 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("AwesomeNotificationsPluginCApi"));
AwesomeNotificationsCorePluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("AwesomeNotificationsCorePluginCApi"));
+ DownloadsfolderPluginCApiRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("DownloadsfolderPluginCApi"));
DynamicColorPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("DynamicColorPluginCApi"));
+ PermissionHandlerWindowsPluginRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar(
diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake
index 08d150d2..2d0310fd 100644
--- a/windows/flutter/generated_plugins.cmake
+++ b/windows/flutter/generated_plugins.cmake
@@ -5,7 +5,9 @@
list(APPEND FLUTTER_PLUGIN_LIST
awesome_notifications
awesome_notifications_core
+ downloadsfolder
dynamic_color
+ permission_handler_windows
share_plus
url_launcher_windows
)