mirror of
https://github.com/madari-media/madari-oss.git
synced 2026-03-11 21:26:56 +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 Function()? onRemoved;
|
||||||
final String? listName;
|
final String? listName;
|
||||||
final Widget? label;
|
final Widget? label;
|
||||||
|
final bool minimal;
|
||||||
|
|
||||||
const AddToListButton({
|
const AddToListButton({
|
||||||
super.key,
|
super.key,
|
||||||
|
|
@ -26,6 +27,7 @@ class AddToListButton extends StatefulWidget {
|
||||||
this.onRemoved,
|
this.onRemoved,
|
||||||
this.listName,
|
this.listName,
|
||||||
this.label,
|
this.label,
|
||||||
|
this.minimal = false,
|
||||||
}) : assert(
|
}) : assert(
|
||||||
listName != null || child != null || icon != null,
|
listName != null || child != null || icon != null,
|
||||||
'Either listName, child, or icon must be provided',
|
'Either listName, child, or icon must be provided',
|
||||||
|
|
@ -320,6 +322,51 @@ class _AddToListButtonState extends State<AddToListButton> {
|
||||||
final colorScheme = theme.colorScheme;
|
final colorScheme = theme.colorScheme;
|
||||||
|
|
||||||
if (widget.listName != null) {
|
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(
|
return ElevatedButton.icon(
|
||||||
onPressed: _isLoading
|
onPressed: _isLoading
|
||||||
? null
|
? null
|
||||||
|
|
@ -425,18 +472,33 @@ class _AddToListButtonState extends State<AddToListButton> {
|
||||||
_getListIcon(list.name),
|
_getListIcon(list.name),
|
||||||
size: 18,
|
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) {
|
IconData _getListIcon(String name) {
|
||||||
switch (name.toLowerCase()) {
|
switch (name.toLowerCase()) {
|
||||||
case 'watchlist':
|
case 'watchlist':
|
||||||
return Icons.bookmark_outlined;
|
return Icons.bookmark_add_outlined;
|
||||||
case 'favourites':
|
case 'favourites':
|
||||||
return Icons.favorite;
|
return Icons.favorite_outline;
|
||||||
case 'watch later':
|
case 'watch later':
|
||||||
return Icons.watch_later_outlined;
|
return Icons.watch_later_outlined;
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:logging/logging.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 'package:madari_client/features/streamio_addons/extension/query_extension.dart';
|
||||||
|
|
||||||
import '../../../../streamio_addons/models/stremio_base_types.dart';
|
import '../../../../streamio_addons/models/stremio_base_types.dart';
|
||||||
|
|
@ -267,8 +269,20 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
|
||||||
|
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: stream.url != null
|
onTap: stream.url != null
|
||||||
? () {
|
? () async {
|
||||||
if (stream.url != null) {
|
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 =
|
String url =
|
||||||
'/player/${widget.meta.type}/${widget.meta.id}/${Uri.encodeQueryComponent(stream.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) {
|
Widget build(BuildContext context) {
|
||||||
if (_isLoading) {
|
if (_isLoading) {
|
||||||
return const Scaffold(
|
return const Scaffold(
|
||||||
body: Center(child: CircularProgressIndicator()),
|
body: Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -174,20 +174,57 @@ class StreamioHeroSection extends StatelessWidget {
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 12,
|
height: 12,
|
||||||
),
|
),
|
||||||
AddToListButton(
|
OutlinedButton.icon(
|
||||||
label: const Row(
|
onPressed: () {
|
||||||
mainAxisSize: MainAxisSize.min,
|
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: [
|
children: [
|
||||||
Icon(Icons.playlist_add_outlined),
|
AddToListButton(
|
||||||
SizedBox(
|
meta: meta,
|
||||||
|
listName: "Favourites",
|
||||||
|
minimal: true,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
width: 8,
|
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_trailer_section.dart';
|
||||||
import 'package:madari_client/features/widgetter/plugins/stremio/containers/streamio_video_list.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');
|
final _logger = Logger('StreamioViewerContent');
|
||||||
|
|
||||||
class StreamioViewerContent extends StatefulWidget {
|
class StreamioViewerContent extends StatefulWidget {
|
||||||
|
|
@ -59,29 +57,6 @@ class _StreamioViewerContentState extends State<StreamioViewerContent> {
|
||||||
prefix: widget.prefix,
|
prefix: widget.prefix,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
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)
|
if (widget.meta.description != null)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue