mirror of
https://github.com/madari-media/madari-oss.git
synced 2026-01-11 22:40:23 +00:00
fix: added external player support
Some checks are pending
Build and Deploy / build_windows (push) Waiting to run
Build and Deploy / build_android (push) Waiting to run
Build and Deploy / build_android_tv (push) Waiting to run
Build and Deploy / build_ipa (push) Waiting to run
Build and Deploy / build_linux (push) Waiting to run
Build and Deploy / build_macos (push) Waiting to run
Some checks are pending
Build and Deploy / build_windows (push) Waiting to run
Build and Deploy / build_android (push) Waiting to run
Build and Deploy / build_android_tv (push) Waiting to run
Build and Deploy / build_ipa (push) Waiting to run
Build and Deploy / build_linux (push) Waiting to run
Build and Deploy / build_macos (push) Waiting to run
This commit is contained in:
parent
4b95941e4a
commit
f8af47b165
5 changed files with 281 additions and 39 deletions
152
lib/features/external_player/service/external_player.dart
Normal file
152
lib/features/external_player/service/external_player.dart
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:android_intent_plus/android_intent.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
final _logger = Logger("ExternalPlayerService");
|
||||
|
||||
class ExternalPlayerService {
|
||||
static Future<void> openInExternalPlayer({
|
||||
required String videoUrl,
|
||||
String? playerPackage,
|
||||
}) async {
|
||||
switch (UniversalPlatform.value) {
|
||||
case UniversalPlatformType.Android:
|
||||
await _openAndroid(videoUrl, playerPackage);
|
||||
break;
|
||||
case UniversalPlatformType.IOS:
|
||||
await _openIOS(videoUrl, playerPackage);
|
||||
break;
|
||||
case UniversalPlatformType.MacOS:
|
||||
await _openMacOS(videoUrl, playerPackage);
|
||||
break;
|
||||
case UniversalPlatformType.Windows:
|
||||
await _openWindows(videoUrl);
|
||||
break;
|
||||
case UniversalPlatformType.Linux:
|
||||
await _openLinux(videoUrl);
|
||||
break;
|
||||
default:
|
||||
throw PlatformException(
|
||||
code: 'UNSUPPORTED_PLATFORM',
|
||||
message: 'Platform ${UniversalPlatform.value} is not supported',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _openAndroid(
|
||||
String videoUrl, String? playerPackage) async {
|
||||
final AndroidIntent intent = AndroidIntent(
|
||||
action: 'action_view',
|
||||
type: "video/*",
|
||||
package: playerPackage,
|
||||
data: videoUrl,
|
||||
flags: const <int>[268435456],
|
||||
arguments: {},
|
||||
);
|
||||
|
||||
await intent.launch();
|
||||
}
|
||||
|
||||
static Future<void> _openIOS(String videoUrl, String? customScheme) async {
|
||||
if (customScheme != null) {
|
||||
final encodedUrl = Uri.encodeComponent(videoUrl);
|
||||
String customUrl = '$customScheme://$encodedUrl';
|
||||
|
||||
switch (customScheme) {
|
||||
case "infuse":
|
||||
customUrl = "infuse://x-callback-url/play?url=$encodedUrl";
|
||||
break;
|
||||
case "open-vidhub":
|
||||
customUrl = "open-vidhub://x-callback-url/open?url=$encodedUrl";
|
||||
break;
|
||||
case "vlc":
|
||||
customUrl = "vlc://$encodedUrl";
|
||||
break;
|
||||
case "outplayer":
|
||||
customUrl = "outplayer://$encodedUrl";
|
||||
break;
|
||||
}
|
||||
|
||||
_logger.info("External player $customUrl");
|
||||
|
||||
if (await canLaunchUrl(Uri.parse(customUrl))) {
|
||||
await launchUrl(Uri.parse(customUrl));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await launchUrl(Uri.parse(videoUrl));
|
||||
}
|
||||
|
||||
static Future<void> _openMacOS(String videoUrl, String? customScheme) async {
|
||||
if (customScheme != null) {
|
||||
final encodedUrl = Uri.encodeComponent(videoUrl);
|
||||
|
||||
String customUrl = '$customScheme://$encodedUrl';
|
||||
|
||||
switch (customScheme) {
|
||||
case "infuse":
|
||||
customUrl = "infuse://x-callback-url/play?url=$encodedUrl";
|
||||
break;
|
||||
case "open-vidhub":
|
||||
customUrl = "open-vidhub://x-callback-url/open?url=$encodedUrl";
|
||||
break;
|
||||
case "iina":
|
||||
customUrl = "iina://weblink?url=$encodedUrl";
|
||||
break;
|
||||
case "omniplayer":
|
||||
customUrl = "omniplayer://$encodedUrl";
|
||||
break;
|
||||
case "nplayer-mac":
|
||||
customUrl = "nplayer-mac://$encodedUrl";
|
||||
break;
|
||||
}
|
||||
|
||||
_logger.info("External player $customUrl for $customScheme");
|
||||
|
||||
if (await canLaunchUrl(Uri.parse(customUrl))) {
|
||||
await launchUrl(Uri.parse(customUrl));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await Process.run('open', [videoUrl]);
|
||||
}
|
||||
|
||||
static Future<void> _openWindows(String videoUrl) async {
|
||||
await Process.run('cmd', ['/c', 'start', videoUrl]);
|
||||
}
|
||||
|
||||
static Future<void> _openLinux(String videoUrl) async {
|
||||
try {
|
||||
await Process.run('xdg-open', [videoUrl]);
|
||||
} catch (e) {
|
||||
final players = ['vlc', 'mpv', 'mplayer'];
|
||||
bool launched = false;
|
||||
|
||||
for (final player in players) {
|
||||
try {
|
||||
final result = await Process.run('which', [player]);
|
||||
if (result.exitCode == 0) {
|
||||
await Process.run(player, [videoUrl]);
|
||||
launched = true;
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!launched) {
|
||||
throw PlatformException(
|
||||
code: 'NO_PLAYER_FOUND',
|
||||
message: 'No suitable video player found on Linux',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ class AddToListButton extends StatefulWidget {
|
|||
final Function()? onRemoved;
|
||||
final String? listName;
|
||||
final Widget? label;
|
||||
final bool minimal;
|
||||
|
||||
const AddToListButton({
|
||||
super.key,
|
||||
|
|
@ -26,6 +27,7 @@ class AddToListButton extends StatefulWidget {
|
|||
this.onRemoved,
|
||||
this.listName,
|
||||
this.label,
|
||||
this.minimal = false,
|
||||
}) : assert(
|
||||
listName != null || child != null || icon != null,
|
||||
'Either listName, child, or icon must be provided',
|
||||
|
|
@ -320,6 +322,51 @@ class _AddToListButtonState extends State<AddToListButton> {
|
|||
final colorScheme = theme.colorScheme;
|
||||
|
||||
if (widget.listName != null) {
|
||||
final icon = _getListIcon(widget.listName!);
|
||||
|
||||
// Minimal mode UI
|
||||
if (widget.minimal) {
|
||||
return IconButton(
|
||||
onPressed: _isLoading
|
||||
? null
|
||||
: () {
|
||||
if (_existsInList &&
|
||||
_existingItemId != null &&
|
||||
_existingList != null) {
|
||||
_removeFromList(context, _existingItemId!, _existingList!);
|
||||
} else {
|
||||
_createAndAddToList(context, widget.listName!);
|
||||
}
|
||||
},
|
||||
icon: _isLoading
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
_existsInList
|
||||
? _getListIconUnselected(widget.listName!)
|
||||
: icon,
|
||||
color: _existsInList
|
||||
? widget.listName?.toLowerCase() == 'favourites'
|
||||
? Colors.red
|
||||
: colorScheme.primary
|
||||
: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: _existsInList
|
||||
? widget.listName?.toLowerCase() == 'favourites'
|
||||
? Colors.red.withOpacity(0.1)
|
||||
: colorScheme.primary.withOpacity(0.1)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Original UI for non-minimal mode
|
||||
return ElevatedButton.icon(
|
||||
onPressed: _isLoading
|
||||
? null
|
||||
|
|
@ -425,18 +472,33 @@ class _AddToListButtonState extends State<AddToListButton> {
|
|||
_getListIcon(list.name),
|
||||
size: 18,
|
||||
),
|
||||
child: Text(list.name),
|
||||
child: _getListIcon(list.name) == Icons.folder_outlined
|
||||
? Text(list.name)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
IconData _getListIconUnselected(String name) {
|
||||
switch (name.toLowerCase()) {
|
||||
case 'watchlist':
|
||||
return Icons.bookmark;
|
||||
case 'favourites':
|
||||
return Icons.favorite;
|
||||
case 'watch later':
|
||||
return Icons.watch_later;
|
||||
default:
|
||||
return Icons.folder;
|
||||
}
|
||||
}
|
||||
|
||||
IconData _getListIcon(String name) {
|
||||
switch (name.toLowerCase()) {
|
||||
case 'watchlist':
|
||||
return Icons.bookmark_outlined;
|
||||
return Icons.bookmark_add_outlined;
|
||||
case 'favourites':
|
||||
return Icons.favorite;
|
||||
return Icons.favorite_outline;
|
||||
case 'watch later':
|
||||
return Icons.watch_later_outlined;
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:madari_client/features/external_player/service/external_player.dart';
|
||||
import 'package:madari_client/features/settings/service/playback_setting_service.dart';
|
||||
import 'package:madari_client/features/streamio_addons/extension/query_extension.dart';
|
||||
|
||||
import '../../../../streamio_addons/models/stremio_base_types.dart';
|
||||
|
|
@ -267,8 +269,20 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
|
|||
|
||||
return InkWell(
|
||||
onTap: stream.url != null
|
||||
? () {
|
||||
? () async {
|
||||
if (stream.url != null) {
|
||||
final settings =
|
||||
await PlaybackSettingsService.instance.getSettings();
|
||||
|
||||
if (settings.externalPlayer) {
|
||||
await ExternalPlayerService.openInExternalPlayer(
|
||||
videoUrl: stream.url!,
|
||||
playerPackage: settings.selectedExternalPlayer,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
String url =
|
||||
'/player/${widget.meta.type}/${widget.meta.id}/${Uri.encodeQueryComponent(stream.url!)}?';
|
||||
|
||||
|
|
@ -399,7 +413,9 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
|
|||
Widget build(BuildContext context) {
|
||||
if (_isLoading) {
|
||||
return const Scaffold(
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
body: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -174,20 +174,57 @@ class StreamioHeroSection extends StatelessWidget {
|
|||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
AddToListButton(
|
||||
label: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
openVideoStream(
|
||||
context,
|
||||
meta,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.play_arrow),
|
||||
label: const Text("Play"),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
SizedBox(
|
||||
height: 40,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: [
|
||||
Icon(Icons.playlist_add_outlined),
|
||||
SizedBox(
|
||||
AddToListButton(
|
||||
meta: meta,
|
||||
listName: "Favourites",
|
||||
minimal: true,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
Text("Add to list"),
|
||||
AddToListButton(
|
||||
meta: meta,
|
||||
listName: "Watchlist",
|
||||
minimal: true,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
AddToListButton(
|
||||
label: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.playlist_add_outlined),
|
||||
SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
Text("Add to list"),
|
||||
],
|
||||
),
|
||||
meta: meta,
|
||||
icon: Icons.add,
|
||||
),
|
||||
],
|
||||
),
|
||||
meta: meta,
|
||||
icon: Icons.add,
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ import 'package:madari_client/features/widgetter/plugins/stremio/containers/stre
|
|||
import 'package:madari_client/features/widgetter/plugins/stremio/containers/streamio_trailer_section.dart';
|
||||
import 'package:madari_client/features/widgetter/plugins/stremio/containers/streamio_video_list.dart';
|
||||
|
||||
import '../../../../library/container/add_to_list_button.dart';
|
||||
|
||||
final _logger = Logger('StreamioViewerContent');
|
||||
|
||||
class StreamioViewerContent extends StatefulWidget {
|
||||
|
|
@ -59,29 +57,6 @@ class _StreamioViewerContentState extends State<StreamioViewerContent> {
|
|||
prefix: widget.prefix,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: [
|
||||
AddToListButton(
|
||||
meta: widget.meta,
|
||||
listName: "Favourites",
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
AddToListButton(
|
||||
meta: widget.meta,
|
||||
listName: "Watchlist",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (widget.meta.description != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
|
|
|
|||
Loading…
Reference in a new issue