mirror of
https://github.com/madari-media/madari-oss.git
synced 2026-04-21 06:51: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.READ_MEDIA_AUDIO"/>
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
<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
|
<application
|
||||||
android:label="madari"
|
android:label="madari"
|
||||||
android:icon="@mipmap/launcher_icon"
|
android:icon="@mipmap/launcher_icon"
|
||||||
|
|
@ -27,6 +34,8 @@
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<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>
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,10 @@
|
||||||
<resources>
|
<resources>
|
||||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||||
</style>
|
</style>
|
||||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
<item name="android:windowBackground">?android:colorBackground</item>
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,10 @@
|
||||||
<resources>
|
<resources>
|
||||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||||
</style>
|
</style>
|
||||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
<item name="android:windowBackground">?android:colorBackground</item>
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,19 @@ class StremioConnectionService extends BaseConnectionService {
|
||||||
for (final addon in config.addons) {
|
for (final addon in config.addons) {
|
||||||
final manifest = await _getManifest(addon);
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -187,8 +199,9 @@ class StremioConnectionService extends BaseConnectionService {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
final hasIdPrefix =
|
final hasIdPrefix = (idPrefixes ?? []).where(
|
||||||
(idPrefixes ?? []).where((item) => meta.id.startsWith(item));
|
(item) => meta.id.startsWith(item),
|
||||||
|
);
|
||||||
|
|
||||||
if (hasIdPrefix.isEmpty) {
|
if (hasIdPrefix.isEmpty) {
|
||||||
continue;
|
continue;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:background_downloader/background_downloader.dart';
|
import 'package:background_downloader/background_downloader.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:madari_client/features/connection/types/stremio.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/connections/service/base_connection_service.dart';
|
||||||
import 'package:madari_client/features/doc_viewer/container/doc_viewer.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 '../../../doc_viewer/types/doc_source.dart';
|
||||||
import '../../../downloads/service/service.dart';
|
import '../../../downloads/service/service.dart';
|
||||||
|
|
||||||
|
|
@ -226,6 +229,23 @@ class _RenderStreamListState extends State<RenderStreamList> {
|
||||||
return;
|
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(
|
Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (ctx) => DocViewer(
|
builder: (ctx) => DocViewer(
|
||||||
|
|
|
||||||
|
|
@ -274,10 +274,16 @@ class _StremioItemViewerState extends State<StremioItemViewer> {
|
||||||
if (widget.original != null &&
|
if (widget.original != null &&
|
||||||
widget.original?.type == "series" &&
|
widget.original?.type == "series" &&
|
||||||
widget.original?.videos?.isNotEmpty == true)
|
widget.original?.videos?.isNotEmpty == true)
|
||||||
StremioItemSeasonSelector(
|
SliverPadding(
|
||||||
meta: item!,
|
padding: EdgeInsets.symmetric(
|
||||||
library: widget.library,
|
horizontal: isWideScreen ? (screenWidth - contentWidth) / 2 : 0,
|
||||||
service: widget.service,
|
vertical: 0,
|
||||||
|
),
|
||||||
|
sliver: StremioItemSeasonSelector(
|
||||||
|
meta: item!,
|
||||||
|
library: widget.library,
|
||||||
|
service: widget.service,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
SliverPadding(
|
SliverPadding(
|
||||||
padding: EdgeInsets.symmetric(
|
padding: EdgeInsets.symmetric(
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart' as intl;
|
import 'package:intl/intl.dart' as intl;
|
||||||
import 'package:madari_client/features/connection/types/stremio.dart';
|
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);
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final seasons = seasonMap.keys.toList()..sort();
|
final seasons = seasonMap.keys.toList()..sort();
|
||||||
|
|
@ -123,6 +172,26 @@ class _StremioItemSeasonSelectorState extends State<StremioItemSeasonSelector>
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 12,
|
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(
|
Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|
@ -162,46 +231,10 @@ class _StremioItemSeasonSelectorState extends State<StremioItemSeasonSelector>
|
||||||
return InkWell(
|
return InkWell(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
if (widget.service == null) {
|
openEpisode(
|
||||||
return;
|
currentSeason: currentSeason,
|
||||||
}
|
episode: episode,
|
||||||
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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (widget.shouldPop) {
|
|
||||||
final val = await onClose;
|
|
||||||
if (val is MediaURLSource && context.mounted) {
|
|
||||||
Navigator.pop(
|
|
||||||
context,
|
|
||||||
val,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
onClose.then((data) {
|
|
||||||
getWatchHistory();
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,9 @@ class _CreateConnectionStepState extends State<CreateConnectionStep> {
|
||||||
final PocketBase pb = AppEngine.engine.pb;
|
final PocketBase pb = AppEngine.engine.pb;
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
final _urlController = TextEditingController();
|
final _urlController = TextEditingController();
|
||||||
final _nameController = TextEditingController(text: "Stremio Addons");
|
final _nameController = TextEditingController(
|
||||||
|
text: "Stremio Addons",
|
||||||
|
);
|
||||||
Connection? _existingConnection;
|
Connection? _existingConnection;
|
||||||
|
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
|
|
@ -43,9 +45,10 @@ class _CreateConnectionStepState extends State<CreateConnectionStep> {
|
||||||
|
|
||||||
loadExistingConnection() async {
|
loadExistingConnection() async {
|
||||||
try {
|
try {
|
||||||
final existingConnection = await pb
|
final existingConnection =
|
||||||
.collection("connection")
|
await pb.collection("connection").getFirstListItem(
|
||||||
.getFirstListItem("type.type = 'stremio_addons'");
|
"type.type = 'stremio_addons'",
|
||||||
|
);
|
||||||
|
|
||||||
final connection = Connection.fromRecord(existingConnection);
|
final connection = Connection.fromRecord(existingConnection);
|
||||||
|
|
||||||
|
|
@ -54,7 +57,11 @@ class _CreateConnectionStepState extends State<CreateConnectionStep> {
|
||||||
|
|
||||||
if (config['addons'] != null) {
|
if (config['addons'] != null) {
|
||||||
for (var url in config['addons']) {
|
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'],
|
'icon': manifest['logo'] ?? manifest['icon'],
|
||||||
'url': url,
|
'url': url,
|
||||||
'addons': manifest,
|
'addons': manifest,
|
||||||
|
'manifestParsed': _manifest,
|
||||||
'types': supportedTypes,
|
'types': supportedTypes,
|
||||||
});
|
});
|
||||||
_urlController.clear();
|
_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 {
|
Future<void> _saveConnection() async {
|
||||||
if (!_formKey.currentState!.validate() || _addons.isEmpty) return;
|
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(() {
|
setState(() {
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
_errorMessage = null;
|
_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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Form(
|
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)
|
if (_errorMessage != null)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
|
@ -289,10 +406,11 @@ class _CreateConnectionStepState extends State<CreateConnectionStep> {
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Flexible(
|
Flexible(
|
||||||
fit: FlexFit.loose,
|
fit: FlexFit.loose,
|
||||||
child: ListView.builder(
|
child: ReorderableListView.builder(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
itemCount: _addons.length,
|
itemCount: _addons.length,
|
||||||
|
onReorder: _reorderAddon,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final addon = _addons[index];
|
final addon = _addons[index];
|
||||||
final name = utf8.decode(
|
final name = utf8.decode(
|
||||||
|
|
@ -300,6 +418,7 @@ class _CreateConnectionStepState extends State<CreateConnectionStep> {
|
||||||
);
|
);
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
|
key: Key('$index'),
|
||||||
margin: EdgeInsets.only(
|
margin: EdgeInsets.only(
|
||||||
bottom: index + 1 != _addons.length ? 10 : 0,
|
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;
|
if (!kIsWeb) _downloadService = DownloadService.instance;
|
||||||
|
|
||||||
_stream = widget.stremio
|
_stream = widget.stremio.getStreams(
|
||||||
.getStreams(
|
|
||||||
widget.item.type,
|
widget.item.type,
|
||||||
widget.id,
|
widget.id,
|
||||||
episode: widget.episode,
|
episode: widget.episode,
|
||||||
season: widget.season,
|
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();
|
_setupDownloadListener();
|
||||||
_checkExistingDownloads();
|
_checkExistingDownloads();
|
||||||
}
|
}
|
||||||
|
|
@ -223,8 +200,6 @@ class _StremioStreamSelectorState extends State<StremioStreamSelector> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
print("Neo");
|
|
||||||
|
|
||||||
return StreamBuilder(
|
return StreamBuilder(
|
||||||
stream: _stream,
|
stream: _stream,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:madari_client/utils/external_player.dart';
|
||||||
import 'package:pocketbase/pocketbase.dart';
|
import 'package:pocketbase/pocketbase.dart';
|
||||||
|
|
||||||
import '../../../engine/engine.dart';
|
import '../../../engine/engine.dart';
|
||||||
|
|
@ -22,6 +23,8 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
|
||||||
double _playbackSpeed = 1.0;
|
double _playbackSpeed = 1.0;
|
||||||
String _defaultAudioTrack = 'eng';
|
String _defaultAudioTrack = 'eng';
|
||||||
String _defaultSubtitleTrack = 'eng';
|
String _defaultSubtitleTrack = 'eng';
|
||||||
|
bool _enableExternalPlayer = true;
|
||||||
|
String? _defaultPlayerId;
|
||||||
|
|
||||||
Map<String, String> _availableLanguages = {};
|
Map<String, String> _availableLanguages = {};
|
||||||
|
|
||||||
|
|
@ -51,9 +54,14 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
|
||||||
final playbackConfig = getPlaybackConfig();
|
final playbackConfig = getPlaybackConfig();
|
||||||
|
|
||||||
_autoPlay = playbackConfig.autoPlay ?? true;
|
_autoPlay = playbackConfig.autoPlay ?? true;
|
||||||
_playbackSpeed = playbackConfig.playbackSpeed.toDouble() ?? 1.0;
|
_playbackSpeed = playbackConfig.playbackSpeed.toDouble();
|
||||||
_defaultAudioTrack = playbackConfig.defaultAudioTrack ?? 'eng';
|
_defaultAudioTrack = playbackConfig.defaultAudioTrack;
|
||||||
_defaultSubtitleTrack = playbackConfig.defaultSubtitleTrack ?? 'eng';
|
_defaultSubtitleTrack = playbackConfig.defaultSubtitleTrack;
|
||||||
|
_enableExternalPlayer = playbackConfig.externalPlayer;
|
||||||
|
_defaultPlayerId =
|
||||||
|
playbackConfig.externalPlayerId?.containsKey(currentPlatform) == true
|
||||||
|
? playbackConfig.externalPlayerId![currentPlatform]
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -77,6 +85,10 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
|
||||||
|
|
||||||
final currentConfig = user.data['config'] as Map<String, dynamic>? ?? {};
|
final currentConfig = user.data['config'] as Map<String, dynamic>? ?? {};
|
||||||
|
|
||||||
|
final extranalId = currentConfig['externalPlayerId'] ?? {};
|
||||||
|
|
||||||
|
extranalId[currentPlatform] = _defaultPlayerId;
|
||||||
|
|
||||||
final updatedConfig = {
|
final updatedConfig = {
|
||||||
...currentConfig,
|
...currentConfig,
|
||||||
'playback': {
|
'playback': {
|
||||||
|
|
@ -84,12 +96,16 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
|
||||||
'playbackSpeed': _playbackSpeed,
|
'playbackSpeed': _playbackSpeed,
|
||||||
'defaultAudioTrack': _defaultAudioTrack,
|
'defaultAudioTrack': _defaultAudioTrack,
|
||||||
'defaultSubtitleTrack': _defaultSubtitleTrack,
|
'defaultSubtitleTrack': _defaultSubtitleTrack,
|
||||||
|
'externalPlayer': _enableExternalPlayer,
|
||||||
|
'externalPlayerId': extranalId,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
await _engine.collection('users').update(
|
await _engine.collection('users').update(
|
||||||
user.id,
|
user.id,
|
||||||
body: {'config': updatedConfig},
|
body: {
|
||||||
|
'config': updatedConfig,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
|
@ -109,6 +125,8 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final currentPlatform = getPlatformInString();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_error != null) {
|
if (_error != null) {
|
||||||
|
|
@ -128,6 +146,8 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
print(_defaultPlayerId);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Playback Settings'),
|
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 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
import 'package:madari_client/utils/external_player.dart';
|
||||||
|
|
||||||
import '../engine/engine.dart';
|
import '../engine/engine.dart';
|
||||||
|
|
||||||
|
part 'load_language.g.dart';
|
||||||
|
|
||||||
Future<Map<String, String>> loadLanguages(BuildContext context) async {
|
Future<Map<String, String>> loadLanguages(BuildContext context) async {
|
||||||
final data = await DefaultAssetBundle.of(context)
|
final data = await DefaultAssetBundle.of(context)
|
||||||
.loadString("assets/data/languages.json");
|
.loadString("assets/data/languages.json");
|
||||||
|
|
@ -29,24 +33,33 @@ PlaybackConfig getPlaybackConfig() {
|
||||||
final config = user.data['config'] as Map<String, dynamic>? ?? {};
|
final config = user.data['config'] as Map<String, dynamic>? ?? {};
|
||||||
final playbackConfig = config['playback'] as Map<String, dynamic>? ?? {};
|
final playbackConfig = config['playback'] as Map<String, dynamic>? ?? {};
|
||||||
|
|
||||||
return PlaybackConfig(
|
return PlaybackConfig.fromJson(playbackConfig);
|
||||||
autoPlay: playbackConfig['autoPlay'] ?? true,
|
|
||||||
playbackSpeed: playbackConfig['playbackSpeed']?.toDouble() ?? 1,
|
|
||||||
defaultAudioTrack: playbackConfig['defaultAudioTrack'] ?? 'eng',
|
|
||||||
defaultSubtitleTrack: playbackConfig['defaultSubtitleTrack'] ?? 'eng',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
class PlaybackConfig {
|
class PlaybackConfig {
|
||||||
final bool autoPlay;
|
final bool autoPlay;
|
||||||
final double playbackSpeed;
|
final double playbackSpeed;
|
||||||
final String defaultAudioTrack;
|
final String defaultAudioTrack;
|
||||||
final String defaultSubtitleTrack;
|
final String defaultSubtitleTrack;
|
||||||
|
final bool externalPlayer;
|
||||||
|
final Map<String, String>? externalPlayerId;
|
||||||
|
|
||||||
PlaybackConfig({
|
PlaybackConfig({
|
||||||
required this.autoPlay,
|
required this.autoPlay,
|
||||||
required this.playbackSpeed,
|
required this.playbackSpeed,
|
||||||
required this.defaultAudioTrack,
|
required this.defaultAudioTrack,
|
||||||
required this.defaultSubtitleTrack,
|
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"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.11.3"
|
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:
|
archive:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -430,6 +438,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.0"
|
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:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,8 @@ dependencies:
|
||||||
fetch_client: ^1.1.2
|
fetch_client: ^1.1.2
|
||||||
cast: ^2.1.0
|
cast: ^2.1.0
|
||||||
permission_handler: ^11.3.1
|
permission_handler: ^11.3.1
|
||||||
|
android_intent_plus: ^5.2.1
|
||||||
|
external_app_launcher: ^4.0.1
|
||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
media_kit:
|
media_kit:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue