mirror of
https://github.com/madari-media/madari-oss.git
synced 2026-03-11 21:26:56 +00:00
Project import generated by Copybara.
GitOrigin-RevId: f4c147c7549de641b05ccec9b800842278b45b0f
This commit is contained in:
parent
420b4b1c12
commit
2cba15132b
14 changed files with 486 additions and 110 deletions
|
|
@ -5,6 +5,13 @@
|
|||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
|
||||
<queries>
|
||||
<package android:name="org.videolan.vlc" />
|
||||
<package android:name="com.mxtech.videoplayer.ad" />
|
||||
<package android:name="com.mxtech.videoplayer.pro" />
|
||||
<package android:name="com.brouken.player" />
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:label="madari"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
|
|
@ -27,6 +34,8 @@
|
|||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
<category android:name="android.intent.category.HOME"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
<resources>
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
<resources>
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -28,7 +28,19 @@ class StremioConnectionService extends BaseConnectionService {
|
|||
for (final addon in config.addons) {
|
||||
final manifest = await _getManifest(addon);
|
||||
|
||||
if (manifest.resources?.contains("meta") != true) {
|
||||
if (manifest.resources == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bool isMeta = false;
|
||||
for (final item in manifest.resources!) {
|
||||
if (item.name == "meta") {
|
||||
isMeta = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isMeta == false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -187,8 +199,9 @@ class StremioConnectionService extends BaseConnectionService {
|
|||
continue;
|
||||
}
|
||||
|
||||
final hasIdPrefix =
|
||||
(idPrefixes ?? []).where((item) => meta.id.startsWith(item));
|
||||
final hasIdPrefix = (idPrefixes ?? []).where(
|
||||
(item) => meta.id.startsWith(item),
|
||||
);
|
||||
|
||||
if (hasIdPrefix.isEmpty) {
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:madari_client/features/connection/types/stremio.dart';
|
||||
import 'package:madari_client/features/connections/service/base_connection_service.dart';
|
||||
import 'package:madari_client/features/doc_viewer/container/doc_viewer.dart';
|
||||
import 'package:madari_client/utils/external_player.dart';
|
||||
|
||||
import '../../../../utils/load_language.dart';
|
||||
import '../../../doc_viewer/types/doc_source.dart';
|
||||
import '../../../downloads/service/service.dart';
|
||||
|
||||
|
|
@ -226,6 +229,23 @@ class _RenderStreamListState extends State<RenderStreamList> {
|
|||
return;
|
||||
}
|
||||
|
||||
PlaybackConfig config = getPlaybackConfig();
|
||||
|
||||
if (config.externalPlayer) {
|
||||
if (!kIsWeb) {
|
||||
if (item.source is URLSource ||
|
||||
item.source is TorrentSource) {
|
||||
if (config.externalPlayer && Platform.isAndroid) {
|
||||
openVideoUrlInExternalPlayerAndroid(
|
||||
videoUrl: (item.source as URLSource).url,
|
||||
playerPackage: config.currentPlayerPackage,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (ctx) => DocViewer(
|
||||
|
|
|
|||
|
|
@ -274,10 +274,16 @@ class _StremioItemViewerState extends State<StremioItemViewer> {
|
|||
if (widget.original != null &&
|
||||
widget.original?.type == "series" &&
|
||||
widget.original?.videos?.isNotEmpty == true)
|
||||
StremioItemSeasonSelector(
|
||||
meta: item!,
|
||||
library: widget.library,
|
||||
service: widget.service,
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: isWideScreen ? (screenWidth - contentWidth) / 2 : 0,
|
||||
vertical: 0,
|
||||
),
|
||||
sliver: StremioItemSeasonSelector(
|
||||
meta: item!,
|
||||
library: widget.library,
|
||||
service: widget.service,
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
import 'package:madari_client/features/connection/types/stremio.dart';
|
||||
|
|
@ -89,6 +91,53 @@ class _StremioItemSeasonSelectorState extends State<StremioItemSeasonSelector>
|
|||
return groupBy(episodes, (Video video) => video.season);
|
||||
}
|
||||
|
||||
void openEpisode({
|
||||
required int currentSeason,
|
||||
required Video episode,
|
||||
}) async {
|
||||
if (widget.service == null) {
|
||||
return;
|
||||
}
|
||||
final onClose = showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
final meta = widget.meta.copyWith(
|
||||
id: episode.id,
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text("Streams for S$currentSeason E${episode.episode}"),
|
||||
),
|
||||
body: RenderStreamList(
|
||||
service: widget.service!,
|
||||
library: widget.library,
|
||||
id: meta,
|
||||
season: currentSeason.toString(),
|
||||
shouldPop: widget.shouldPop,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (widget.shouldPop) {
|
||||
final val = await onClose;
|
||||
|
||||
if (val is MediaURLSource && context.mounted) {
|
||||
Navigator.pop(
|
||||
context,
|
||||
val,
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
onClose.then((data) {
|
||||
getWatchHistory();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final seasons = seasonMap.keys.toList()..sort();
|
||||
|
|
@ -123,6 +172,26 @@ class _StremioItemSeasonSelectorState extends State<StremioItemSeasonSelector>
|
|||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 320),
|
||||
child: ElevatedButton.icon(
|
||||
icon: const Icon(Icons.shuffle),
|
||||
label: const Text("Random Episode"),
|
||||
onPressed: () {
|
||||
Random random = Random();
|
||||
int randomIndex = random.nextInt(
|
||||
widget.meta.videos!.length,
|
||||
);
|
||||
|
||||
openEpisode(
|
||||
currentSeason: widget.meta.videos![randomIndex].season,
|
||||
episode: widget.meta.videos![randomIndex],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
|
|
@ -162,46 +231,10 @@ class _StremioItemSeasonSelectorState extends State<StremioItemSeasonSelector>
|
|||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: () async {
|
||||
if (widget.service == null) {
|
||||
return;
|
||||
}
|
||||
final onClose = showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
final meta = widget.meta.copyWith(
|
||||
id: episode.id,
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("Streams"),
|
||||
),
|
||||
body: RenderStreamList(
|
||||
service: widget.service!,
|
||||
library: widget.library,
|
||||
id: meta,
|
||||
season: currentSeason.toString(),
|
||||
shouldPop: widget.shouldPop,
|
||||
),
|
||||
);
|
||||
},
|
||||
openEpisode(
|
||||
currentSeason: currentSeason,
|
||||
episode: episode,
|
||||
);
|
||||
|
||||
if (widget.shouldPop) {
|
||||
final val = await onClose;
|
||||
if (val is MediaURLSource && context.mounted) {
|
||||
Navigator.pop(
|
||||
context,
|
||||
val,
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
onClose.then((data) {
|
||||
getWatchHistory();
|
||||
});
|
||||
},
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
|
|
|||
|
|
@ -26,7 +26,9 @@ class _CreateConnectionStepState extends State<CreateConnectionStep> {
|
|||
final PocketBase pb = AppEngine.engine.pb;
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _urlController = TextEditingController();
|
||||
final _nameController = TextEditingController(text: "Stremio Addons");
|
||||
final _nameController = TextEditingController(
|
||||
text: "Stremio Addons",
|
||||
);
|
||||
Connection? _existingConnection;
|
||||
|
||||
bool _isLoading = false;
|
||||
|
|
@ -43,9 +45,10 @@ class _CreateConnectionStepState extends State<CreateConnectionStep> {
|
|||
|
||||
loadExistingConnection() async {
|
||||
try {
|
||||
final existingConnection = await pb
|
||||
.collection("connection")
|
||||
.getFirstListItem("type.type = 'stremio_addons'");
|
||||
final existingConnection =
|
||||
await pb.collection("connection").getFirstListItem(
|
||||
"type.type = 'stremio_addons'",
|
||||
);
|
||||
|
||||
final connection = Connection.fromRecord(existingConnection);
|
||||
|
||||
|
|
@ -54,7 +57,11 @@ class _CreateConnectionStepState extends State<CreateConnectionStep> {
|
|||
|
||||
if (config['addons'] != null) {
|
||||
for (var url in config['addons']) {
|
||||
_validateAddonUrl(url);
|
||||
try {
|
||||
await _validateAddonUrl(url);
|
||||
} catch (e) {
|
||||
print("Failed to load addon");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -99,6 +106,7 @@ class _CreateConnectionStepState extends State<CreateConnectionStep> {
|
|||
'icon': manifest['logo'] ?? manifest['icon'],
|
||||
'url': url,
|
||||
'addons': manifest,
|
||||
'manifestParsed': _manifest,
|
||||
'types': supportedTypes,
|
||||
});
|
||||
_urlController.clear();
|
||||
|
|
@ -117,9 +125,102 @@ class _CreateConnectionStepState extends State<CreateConnectionStep> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<bool> showAddonWarningDialog(
|
||||
BuildContext context, {
|
||||
required bool isMeta,
|
||||
required bool isAddon,
|
||||
}) async {
|
||||
bool continueAnyway = false;
|
||||
|
||||
if (isMeta && isAddon) {
|
||||
return true;
|
||||
}
|
||||
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Warning!'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (!isMeta || !isAddon)
|
||||
const Text(
|
||||
'You are missing the following addons:',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
if (!isMeta) const Text('🔴 Meta Addon'),
|
||||
if (!isAddon) const Text('🔴 Streaming Addon'),
|
||||
const SizedBox(height: 10),
|
||||
const Text(
|
||||
'Continuing without these addons may limit functionality. Are you sure you want to proceed?',
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
// User chooses to continue anyway
|
||||
Navigator.of(context).pop();
|
||||
continueAnyway = true;
|
||||
},
|
||||
child: const Text('CONTINUE ANYWAY'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
// User chooses to add addon
|
||||
Navigator.of(context).pop();
|
||||
continueAnyway = false;
|
||||
},
|
||||
child: const Text('ADD ADDON'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return continueAnyway;
|
||||
}
|
||||
|
||||
Future<void> _saveConnection() async {
|
||||
if (!_formKey.currentState!.validate() || _addons.isEmpty) return;
|
||||
|
||||
bool hasMeta = false;
|
||||
bool hasStream = false;
|
||||
|
||||
for (final item in _addons) {
|
||||
final manifest = item['manifestParsed'] as StremioManifest;
|
||||
|
||||
if (manifest.resources == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (final resource in manifest.resources!) {
|
||||
if (resource.name == "meta") {
|
||||
hasMeta = true;
|
||||
}
|
||||
|
||||
if (resource.name == "stream") {
|
||||
hasStream = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final result = await showAddonWarningDialog(
|
||||
context,
|
||||
isAddon: hasStream,
|
||||
isMeta: hasMeta,
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
|
|
@ -192,6 +293,16 @@ class _CreateConnectionStepState extends State<CreateConnectionStep> {
|
|||
});
|
||||
}
|
||||
|
||||
void _reorderAddon(int oldIndex, int newIndex) {
|
||||
setState(() {
|
||||
if (oldIndex < newIndex) {
|
||||
newIndex -= 1;
|
||||
}
|
||||
final item = _addons.removeAt(oldIndex);
|
||||
_addons.insert(newIndex, item);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Form(
|
||||
|
|
@ -268,7 +379,13 @@ class _CreateConnectionStepState extends State<CreateConnectionStep> {
|
|||
},
|
||||
),
|
||||
),
|
||||
if (_isLoading) const Center(child: CircularProgressIndicator()),
|
||||
if (_isLoading)
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 12),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
if (_errorMessage != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
|
|
@ -289,10 +406,11 @@ class _CreateConnectionStepState extends State<CreateConnectionStep> {
|
|||
const SizedBox(height: 10),
|
||||
Flexible(
|
||||
fit: FlexFit.loose,
|
||||
child: ListView.builder(
|
||||
child: ReorderableListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: _addons.length,
|
||||
onReorder: _reorderAddon,
|
||||
itemBuilder: (context, index) {
|
||||
final addon = _addons[index];
|
||||
final name = utf8.decode(
|
||||
|
|
@ -300,6 +418,7 @@ class _CreateConnectionStepState extends State<CreateConnectionStep> {
|
|||
);
|
||||
|
||||
return Card(
|
||||
key: Key('$index'),
|
||||
margin: EdgeInsets.only(
|
||||
bottom: index + 1 != _addons.length ? 10 : 0,
|
||||
),
|
||||
|
|
@ -363,26 +482,27 @@ class _CreateConnectionStepState extends State<CreateConnectionStep> {
|
|||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 10.0,
|
||||
),
|
||||
child: ElevatedButton(
|
||||
onPressed: _addons.isEmpty ? null : _saveConnection,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.white70,
|
||||
foregroundColor: Colors.black,
|
||||
),
|
||||
child: Text(
|
||||
'Next',
|
||||
style: GoogleFonts.exo2().copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 12.0,
|
||||
top: 12.0,
|
||||
),
|
||||
child: ElevatedButton(
|
||||
onPressed: _addons.isEmpty ? null : _saveConnection,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.white70,
|
||||
foregroundColor: Colors.black,
|
||||
),
|
||||
child: Text(
|
||||
'Next',
|
||||
style: GoogleFonts.exo2().copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -47,36 +47,13 @@ class _StremioStreamSelectorState extends State<StremioStreamSelector> {
|
|||
|
||||
if (!kIsWeb) _downloadService = DownloadService.instance;
|
||||
|
||||
_stream = widget.stremio
|
||||
.getStreams(
|
||||
_stream = widget.stremio.getStreams(
|
||||
widget.item.type,
|
||||
widget.id,
|
||||
episode: widget.episode,
|
||||
season: widget.season,
|
||||
)
|
||||
.map((item) {
|
||||
return [
|
||||
if (widget.item.type == "movie")
|
||||
for (final item in (widget.stremio.configParsed.movieIframe ?? []))
|
||||
VideoStream(
|
||||
title: widget.item.name,
|
||||
behaviorHints: {
|
||||
"filename": widget.item.name,
|
||||
"iframe": item,
|
||||
},
|
||||
),
|
||||
if (widget.item.type == "series")
|
||||
for (final item in (widget.stremio.configParsed.seriesIframe ?? []))
|
||||
VideoStream(
|
||||
title: widget.item.name,
|
||||
behaviorHints: {
|
||||
"filename": widget.item.name,
|
||||
"iframe": item,
|
||||
},
|
||||
),
|
||||
...item,
|
||||
];
|
||||
});
|
||||
);
|
||||
|
||||
_setupDownloadListener();
|
||||
_checkExistingDownloads();
|
||||
}
|
||||
|
|
@ -223,8 +200,6 @@ class _StremioStreamSelectorState extends State<StremioStreamSelector> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
print("Neo");
|
||||
|
||||
return StreamBuilder(
|
||||
stream: _stream,
|
||||
builder: (context, snapshot) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:madari_client/utils/external_player.dart';
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
import '../../../engine/engine.dart';
|
||||
|
|
@ -22,6 +23,8 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
|
|||
double _playbackSpeed = 1.0;
|
||||
String _defaultAudioTrack = 'eng';
|
||||
String _defaultSubtitleTrack = 'eng';
|
||||
bool _enableExternalPlayer = true;
|
||||
String? _defaultPlayerId;
|
||||
|
||||
Map<String, String> _availableLanguages = {};
|
||||
|
||||
|
|
@ -51,9 +54,14 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
|
|||
final playbackConfig = getPlaybackConfig();
|
||||
|
||||
_autoPlay = playbackConfig.autoPlay ?? true;
|
||||
_playbackSpeed = playbackConfig.playbackSpeed.toDouble() ?? 1.0;
|
||||
_defaultAudioTrack = playbackConfig.defaultAudioTrack ?? 'eng';
|
||||
_defaultSubtitleTrack = playbackConfig.defaultSubtitleTrack ?? 'eng';
|
||||
_playbackSpeed = playbackConfig.playbackSpeed.toDouble();
|
||||
_defaultAudioTrack = playbackConfig.defaultAudioTrack;
|
||||
_defaultSubtitleTrack = playbackConfig.defaultSubtitleTrack;
|
||||
_enableExternalPlayer = playbackConfig.externalPlayer;
|
||||
_defaultPlayerId =
|
||||
playbackConfig.externalPlayerId?.containsKey(currentPlatform) == true
|
||||
? playbackConfig.externalPlayerId![currentPlatform]
|
||||
: null;
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -77,6 +85,10 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
|
|||
|
||||
final currentConfig = user.data['config'] as Map<String, dynamic>? ?? {};
|
||||
|
||||
final extranalId = currentConfig['externalPlayerId'] ?? {};
|
||||
|
||||
extranalId[currentPlatform] = _defaultPlayerId;
|
||||
|
||||
final updatedConfig = {
|
||||
...currentConfig,
|
||||
'playback': {
|
||||
|
|
@ -84,12 +96,16 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
|
|||
'playbackSpeed': _playbackSpeed,
|
||||
'defaultAudioTrack': _defaultAudioTrack,
|
||||
'defaultSubtitleTrack': _defaultSubtitleTrack,
|
||||
'externalPlayer': _enableExternalPlayer,
|
||||
'externalPlayerId': extranalId,
|
||||
},
|
||||
};
|
||||
|
||||
await _engine.collection('users').update(
|
||||
user.id,
|
||||
body: {'config': updatedConfig},
|
||||
body: {
|
||||
'config': updatedConfig,
|
||||
},
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
|
|
@ -109,6 +125,8 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
|
|||
}
|
||||
}
|
||||
|
||||
final currentPlatform = getPlatformInString();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_error != null) {
|
||||
|
|
@ -128,6 +146,8 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
|
|||
);
|
||||
}
|
||||
|
||||
print(_defaultPlayerId);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Playback Settings'),
|
||||
|
|
@ -190,6 +210,35 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
|
|||
},
|
||||
),
|
||||
),
|
||||
if (!isWeb)
|
||||
SwitchListTile(
|
||||
title: const Text('External Player'),
|
||||
subtitle: const Text('Always open video in external player?'),
|
||||
value: _enableExternalPlayer,
|
||||
onChanged: (value) {
|
||||
setState(() => _enableExternalPlayer = value);
|
||||
_debouncedSave();
|
||||
},
|
||||
),
|
||||
if (_enableExternalPlayer &&
|
||||
externalPlayers[currentPlatform]?.isNotEmpty == true)
|
||||
ListTile(
|
||||
title: const Text('Default Player'),
|
||||
trailing: DropdownButton<String>(
|
||||
value: _defaultPlayerId,
|
||||
items: externalPlayers[currentPlatform]!
|
||||
.map(
|
||||
(item) => item.toDropdownMenuItem(),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setState(() => _defaultPlayerId = value);
|
||||
_debouncedSave();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
116
lib/utils/external_player.dart
Normal file
116
lib/utils/external_player.dart
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:android_intent_plus/android_intent.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
class ExternalMediaPlayer {
|
||||
final String name;
|
||||
final String id;
|
||||
|
||||
ExternalMediaPlayer({
|
||||
required this.id,
|
||||
required this.name,
|
||||
});
|
||||
|
||||
DropdownMenuItem<String> toDropdownMenuItem() {
|
||||
return DropdownMenuItem<String>(
|
||||
value: id,
|
||||
child: Text(name),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final Map<String, List<ExternalMediaPlayer>> externalPlayers = {
|
||||
"android": [
|
||||
ExternalMediaPlayer(id: "", name: "App chooser"),
|
||||
ExternalMediaPlayer(id: "org.videolan.vlc", name: "VLC"),
|
||||
ExternalMediaPlayer(id: "com.mxtech.videoplayer.ad", name: "MX Player"),
|
||||
ExternalMediaPlayer(
|
||||
id: "com.mxtech.videoplayer.pro",
|
||||
name: "MX Player Pro",
|
||||
),
|
||||
ExternalMediaPlayer(
|
||||
id: "com.brouken.player",
|
||||
name: "JustPlayer",
|
||||
),
|
||||
],
|
||||
"ios": [
|
||||
ExternalMediaPlayer(
|
||||
id: "open-vidhub",
|
||||
name: "VidHub",
|
||||
),
|
||||
ExternalMediaPlayer(
|
||||
id: "infuse",
|
||||
name: "Infuse",
|
||||
),
|
||||
ExternalMediaPlayer(
|
||||
id: "vlc",
|
||||
name: "VLC",
|
||||
),
|
||||
ExternalMediaPlayer(
|
||||
id: "outplayer",
|
||||
name: "Outplayer",
|
||||
),
|
||||
],
|
||||
"macos": [
|
||||
ExternalMediaPlayer(
|
||||
id: "open-vidhub",
|
||||
name: "VidHub",
|
||||
),
|
||||
ExternalMediaPlayer(
|
||||
id: "infuse",
|
||||
name: "Infuse",
|
||||
),
|
||||
ExternalMediaPlayer(
|
||||
id: "iina",
|
||||
name: "IINA",
|
||||
),
|
||||
ExternalMediaPlayer(
|
||||
id: "omniplayer",
|
||||
name: "OmniPlayer",
|
||||
),
|
||||
ExternalMediaPlayer(
|
||||
id: "nplayer-mac",
|
||||
name: "nPlayer",
|
||||
),
|
||||
]
|
||||
};
|
||||
|
||||
String getPlatformInString() {
|
||||
if (isWeb) {
|
||||
return "web";
|
||||
}
|
||||
if (Platform.isAndroid) {
|
||||
return "android";
|
||||
}
|
||||
if (Platform.isIOS) {
|
||||
return "ios";
|
||||
}
|
||||
if (Platform.isMacOS) {
|
||||
return "macos";
|
||||
}
|
||||
if (Platform.isWindows) {
|
||||
return "windows";
|
||||
}
|
||||
if (Platform.isLinux) {
|
||||
return "linux";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
Future<void> openVideoUrlInExternalPlayerAndroid({
|
||||
required String videoUrl,
|
||||
String? playerPackage,
|
||||
}) async {
|
||||
AndroidIntent intent = AndroidIntent(
|
||||
action: 'action_view',
|
||||
type: "video/*",
|
||||
package: playerPackage,
|
||||
data: videoUrl,
|
||||
flags: const <int>[268435456],
|
||||
arguments: {},
|
||||
);
|
||||
await intent.launch();
|
||||
}
|
||||
|
|
@ -1,9 +1,13 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:madari_client/utils/external_player.dart';
|
||||
|
||||
import '../engine/engine.dart';
|
||||
|
||||
part 'load_language.g.dart';
|
||||
|
||||
Future<Map<String, String>> loadLanguages(BuildContext context) async {
|
||||
final data = await DefaultAssetBundle.of(context)
|
||||
.loadString("assets/data/languages.json");
|
||||
|
|
@ -29,24 +33,33 @@ PlaybackConfig getPlaybackConfig() {
|
|||
final config = user.data['config'] as Map<String, dynamic>? ?? {};
|
||||
final playbackConfig = config['playback'] as Map<String, dynamic>? ?? {};
|
||||
|
||||
return PlaybackConfig(
|
||||
autoPlay: playbackConfig['autoPlay'] ?? true,
|
||||
playbackSpeed: playbackConfig['playbackSpeed']?.toDouble() ?? 1,
|
||||
defaultAudioTrack: playbackConfig['defaultAudioTrack'] ?? 'eng',
|
||||
defaultSubtitleTrack: playbackConfig['defaultSubtitleTrack'] ?? 'eng',
|
||||
);
|
||||
return PlaybackConfig.fromJson(playbackConfig);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class PlaybackConfig {
|
||||
final bool autoPlay;
|
||||
final double playbackSpeed;
|
||||
final String defaultAudioTrack;
|
||||
final String defaultSubtitleTrack;
|
||||
final bool externalPlayer;
|
||||
final Map<String, String>? externalPlayerId;
|
||||
|
||||
PlaybackConfig({
|
||||
required this.autoPlay,
|
||||
required this.playbackSpeed,
|
||||
required this.defaultAudioTrack,
|
||||
required this.defaultSubtitleTrack,
|
||||
required this.externalPlayer,
|
||||
this.externalPlayerId,
|
||||
});
|
||||
|
||||
String? get currentPlayerPackage {
|
||||
return externalPlayerId?.containsKey(getPlatformInString()) == true
|
||||
? externalPlayerId![getPlatformInString()]
|
||||
: null;
|
||||
}
|
||||
|
||||
factory PlaybackConfig.fromJson(Map<String, dynamic> config) =>
|
||||
_$PlaybackConfigFromJson(config);
|
||||
}
|
||||
|
|
|
|||
16
pubspec.lock
16
pubspec.lock
|
|
@ -30,6 +30,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.11.3"
|
||||
android_intent_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: android_intent_plus
|
||||
sha256: "53136214d506d3128c9f4e5bfce3d026abe7e8038958629811a8d3223b1757c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.2.1"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -430,6 +438,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.0"
|
||||
external_app_launcher:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: external_app_launcher
|
||||
sha256: "69d843ae16598cbf86be8d65ae5f206bb403fbfb75ca9aaaa9ea91b15b040571"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.1"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -57,6 +57,8 @@ dependencies:
|
|||
fetch_client: ^1.1.2
|
||||
cast: ^2.1.0
|
||||
permission_handler: ^11.3.1
|
||||
android_intent_plus: ^5.2.1
|
||||
external_app_launcher: ^4.0.1
|
||||
|
||||
dependency_overrides:
|
||||
media_kit:
|
||||
|
|
|
|||
Loading…
Reference in a new issue