mirror of
https://github.com/madari-media/madari-oss.git
synced 2026-04-21 15:11:57 +00:00
fix: small issues
This commit is contained in:
parent
1910432364
commit
309be2be2c
5 changed files with 352 additions and 235 deletions
|
|
@ -348,7 +348,20 @@ class Meta {
|
||||||
@JsonKey(name: "moviedb_id")
|
@JsonKey(name: "moviedb_id")
|
||||||
final int? moviedbId;
|
final int? moviedbId;
|
||||||
@JsonKey(name: "runtime")
|
@JsonKey(name: "runtime")
|
||||||
final String? runtime;
|
final String? runtime_;
|
||||||
|
|
||||||
|
String? get runtime {
|
||||||
|
try {
|
||||||
|
if (runtime_ == null) {
|
||||||
|
return runtime_;
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatTimeFromMinutes(runtime_!);
|
||||||
|
} catch (e) {
|
||||||
|
return runtime_;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@JsonKey(name: "trailers")
|
@JsonKey(name: "trailers")
|
||||||
final List<Trailer>? trailers;
|
final List<Trailer>? trailers;
|
||||||
@JsonKey(name: "popularity")
|
@JsonKey(name: "popularity")
|
||||||
|
|
@ -432,7 +445,7 @@ class Meta {
|
||||||
this.logo,
|
this.logo,
|
||||||
this.awards,
|
this.awards,
|
||||||
this.moviedbId,
|
this.moviedbId,
|
||||||
this.runtime,
|
this.runtime_,
|
||||||
this.trailers,
|
this.trailers,
|
||||||
this.popularity,
|
this.popularity,
|
||||||
required this.id,
|
required this.id,
|
||||||
|
|
@ -520,7 +533,7 @@ class Meta {
|
||||||
logo: logo ?? this.logo,
|
logo: logo ?? this.logo,
|
||||||
awards: awards ?? this.awards,
|
awards: awards ?? this.awards,
|
||||||
moviedbId: moviedbId ?? this.moviedbId,
|
moviedbId: moviedbId ?? this.moviedbId,
|
||||||
runtime: runtime ?? this.runtime,
|
runtime_: runtime ?? this.runtime,
|
||||||
trailers: trailers ?? this.trailers,
|
trailers: trailers ?? this.trailers,
|
||||||
popularity: popularity ?? this.popularity,
|
popularity: popularity ?? this.popularity,
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
|
|
@ -898,6 +911,14 @@ class VideoStream {
|
||||||
_$VideoStreamFromJson(json);
|
_$VideoStreamFromJson(json);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$VideoStreamToJson(this);
|
Map<String, dynamic> toJson() => _$VideoStreamToJson(this);
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => "$url$name$title$description".length;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return super.hashCode == other.hashCode;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class StreamInfo {
|
class StreamInfo {
|
||||||
|
|
@ -1002,8 +1023,10 @@ class StreamParser {
|
||||||
final unratedMatch = _unratedRegex.hasMatch(name);
|
final unratedMatch = _unratedRegex.hasMatch(name);
|
||||||
final sizeMatch = _sizeRegex.firstMatch(name);
|
final sizeMatch = _sizeRegex.firstMatch(name);
|
||||||
|
|
||||||
|
final res = resMatch?.group(1)?.toUpperCase();
|
||||||
|
|
||||||
return StreamInfo(
|
return StreamInfo(
|
||||||
resolution: resMatch?.group(1)?.toUpperCase(),
|
resolution: res == "2160P" ? "4K" : res,
|
||||||
quality: qualMatch?.group(1)?.toUpperCase(),
|
quality: qualMatch?.group(1)?.toUpperCase(),
|
||||||
codec: codecMatch?.group(1)?.toUpperCase(),
|
codec: codecMatch?.group(1)?.toUpperCase(),
|
||||||
audio: audioMatch?.group(1)?.toUpperCase(),
|
audio: audioMatch?.group(1)?.toUpperCase(),
|
||||||
|
|
@ -1014,3 +1037,28 @@ class StreamParser {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String formatTimeFromMinutes(String minutesInput) {
|
||||||
|
int? minutes = int.tryParse(minutesInput);
|
||||||
|
|
||||||
|
if (minutes == null) {
|
||||||
|
return minutesInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
int hours = minutes ~/ 60;
|
||||||
|
int remainingMinutes = minutes % 60;
|
||||||
|
|
||||||
|
if (hours == 0) {
|
||||||
|
return '$remainingMinutes minutes';
|
||||||
|
}
|
||||||
|
if (remainingMinutes == 0) {
|
||||||
|
return '$hours hours';
|
||||||
|
}
|
||||||
|
return '$hours hours $remainingMinutes minutes';
|
||||||
|
|
||||||
|
// Format 2 (Alternative): Compact format
|
||||||
|
// return '${hours}h ${remainingMinutes}m';
|
||||||
|
|
||||||
|
// Format 3 (Alternative): Digital clock format
|
||||||
|
// return '${hours.toString().padLeft(2, '0')}:${remainingMinutes.toString().padLeft(2, '0')}';
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import 'package:logging/logging.dart';
|
||||||
import 'package:madari_client/data/db.dart';
|
import 'package:madari_client/data/db.dart';
|
||||||
import 'package:madari_client/features/streamio_addons/extension/query_extension.dart';
|
import 'package:madari_client/features/streamio_addons/extension/query_extension.dart';
|
||||||
import 'package:madari_client/utils/array-extension.dart';
|
import 'package:madari_client/utils/array-extension.dart';
|
||||||
|
import 'package:pocketbase/pocketbase.dart';
|
||||||
|
|
||||||
import '../../pocketbase/service/pocketbase.service.dart';
|
import '../../pocketbase/service/pocketbase.service.dart';
|
||||||
import '../../widgetter/plugins/stremio/models/cast_info.dart';
|
import '../../widgetter/plugins/stremio/models/cast_info.dart';
|
||||||
|
|
@ -208,8 +209,22 @@ class StremioAddonService {
|
||||||
|
|
||||||
await getInstalledAddons().refetch();
|
await getInstalledAddons().refetch();
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
print(e);
|
_logger.warning("Error to save addon", e, stack);
|
||||||
print(stack);
|
|
||||||
|
if (e is ClientException) {
|
||||||
|
try {
|
||||||
|
if (e.response.values.first["url"]["code"] ==
|
||||||
|
"validation_not_unique") {
|
||||||
|
throw Exception(
|
||||||
|
"Addon already installed make sure you don't have this in disabled addons.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (ex, stack) {
|
||||||
|
_logger.warning("Error find error", ex, stack);
|
||||||
|
throw Exception(e.response.values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
throw Exception('Failed to save addon: $e');
|
throw Exception('Failed to save addon: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:cached_query_flutter/cached_query_flutter.dart';
|
import 'package:cached_query_flutter/cached_query_flutter.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:madari_client/features/streamio_addons/extension/query_extension.dart';
|
import 'package:madari_client/features/streamio_addons/extension/query_extension.dart';
|
||||||
|
import 'package:pocketbase/pocketbase.dart';
|
||||||
|
|
||||||
import '../models/stremio_base_types.dart';
|
import '../models/stremio_base_types.dart';
|
||||||
import '../service/stremio_addon_service.dart';
|
import '../service/stremio_addon_service.dart';
|
||||||
|
|
@ -65,7 +66,7 @@ class _AddAddonSheetState extends State<AddAddonSheet> {
|
||||||
final manifest = await query.queryFn();
|
final manifest = await query.queryFn();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
_installAddon(manifest);
|
await _installAddon(manifest);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _installAddon(StremioManifest manifest) async {
|
Future<void> _installAddon(StremioManifest manifest) async {
|
||||||
|
|
@ -79,9 +80,17 @@ class _AddAddonSheetState extends State<AddAddonSheet> {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
print(e);
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
if (e is ClientException) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Failed to install addon: ${e.response}')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('Failed to install addon: $e')),
|
SnackBar(content: Text('Failed to install addon: ${e}')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -144,7 +153,9 @@ class _AddAddonSheetState extends State<AddAddonSheet> {
|
||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
onPressed: () => _validateManifest(),
|
onPressed: () => _validateManifest(),
|
||||||
icon: const Icon(Icons.check),
|
icon: const Icon(Icons.check),
|
||||||
label: const Text('Validate'),
|
label: _isInstalling
|
||||||
|
? const Text("Installing")
|
||||||
|
: const Text('Install'),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
const Text(
|
const Text(
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
|
||||||
Set<String> _selectedAudios = {};
|
Set<String> _selectedAudios = {};
|
||||||
Set<String> _selectedSizes = {};
|
Set<String> _selectedSizes = {};
|
||||||
final Set<String> _selectedAddons = {};
|
final Set<String> _selectedAddons = {};
|
||||||
|
final Map<String, List<StreamWithAddon>> streamsByAddon = {};
|
||||||
|
|
||||||
Set<String> _resolutions = {};
|
Set<String> _resolutions = {};
|
||||||
Set<String> _qualities = {};
|
Set<String> _qualities = {};
|
||||||
|
|
@ -53,7 +54,6 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
|
||||||
_logger.info('Loading streams for ${widget.meta.id}');
|
_logger.info('Loading streams for ${widget.meta.id}');
|
||||||
|
|
||||||
final addons = service.getInstalledAddons();
|
final addons = service.getInstalledAddons();
|
||||||
|
|
||||||
final result = await addons.queryFn();
|
final result = await addons.queryFn();
|
||||||
|
|
||||||
final count = result
|
final count = result
|
||||||
|
|
@ -63,7 +63,6 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
})
|
})
|
||||||
.toList()
|
.toList()
|
||||||
|
|
@ -86,7 +85,6 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
|
||||||
callback: (items, addonName, error) {
|
callback: (items, addonName, error) {
|
||||||
setState(() {
|
setState(() {
|
||||||
left -= 1;
|
left -= 1;
|
||||||
|
|
||||||
if (left <= 0) {
|
if (left <= 0) {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
}
|
}
|
||||||
|
|
@ -98,7 +96,7 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (items != null) {
|
if (items != null && addonName != null) {
|
||||||
final Set<String> resSet = {};
|
final Set<String> resSet = {};
|
||||||
final Set<String> qualSet = {};
|
final Set<String> qualSet = {};
|
||||||
final Set<String> codecSet = {};
|
final Set<String> codecSet = {};
|
||||||
|
|
@ -106,12 +104,10 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
|
||||||
final Set<String> sizeSet = {};
|
final Set<String> sizeSet = {};
|
||||||
|
|
||||||
final streamsWithAddon = items
|
final streamsWithAddon = items
|
||||||
.map(
|
.map((stream) => StreamWithAddon(
|
||||||
(stream) => StreamWithAddon(
|
stream: stream,
|
||||||
stream: stream,
|
addonName: addonName,
|
||||||
addonName: addonName,
|
))
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
for (var streamData in streamsWithAddon) {
|
for (var streamData in streamsWithAddon) {
|
||||||
|
|
@ -135,14 +131,13 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
streams.addAll(streamsWithAddon);
|
streamsByAddon[addonName] = streamsWithAddon;
|
||||||
if (addonName != null) _addons.add(addonName);
|
_addons.add(addonName);
|
||||||
_resolutions = resSet;
|
_resolutions.addAll(resSet);
|
||||||
_qualities = qualSet;
|
_qualities.addAll(qualSet);
|
||||||
_codecs = codecSet;
|
_codecs.addAll(codecSet);
|
||||||
_audios = audioSet;
|
_audios.addAll(audioSet);
|
||||||
_sizes = sizeSet;
|
_sizes.addAll(sizeSet);
|
||||||
_isLoading = false;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -150,26 +145,20 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Color _getQualityColor(String resolution) {
|
|
||||||
final res = resolution.toUpperCase();
|
|
||||||
if (res.contains('2160P') || res.contains('4K') || res.contains('UHD')) {
|
|
||||||
return Colors.amberAccent;
|
|
||||||
} else if (res.contains('1080P')) {
|
|
||||||
return Colors.blue;
|
|
||||||
} else if (res.contains('720P')) {
|
|
||||||
return Colors.green;
|
|
||||||
}
|
|
||||||
return Colors.grey;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<StreamWithAddon> _getFilteredStreams() {
|
List<StreamWithAddon> _getFilteredStreams() {
|
||||||
return streams.where((streamData) {
|
List<StreamWithAddon> allStreams = [];
|
||||||
if (streamData.stream.name == null) return false;
|
if (_selectedAddons.isEmpty) {
|
||||||
|
streamsByAddon.values.forEach(allStreams.addAll);
|
||||||
if (_selectedAddons.isNotEmpty &&
|
} else {
|
||||||
!_selectedAddons.contains(streamData.addonName)) {
|
for (var addon in _selectedAddons) {
|
||||||
return false;
|
if (streamsByAddon.containsKey(addon)) {
|
||||||
|
allStreams.addAll(streamsByAddon[addon]!);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allStreams.where((streamData) {
|
||||||
|
if (streamData.stream.name == null) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final info = StreamParser.parseStreamName(streamData.stream.name!);
|
final info = StreamParser.parseStreamName(streamData.stream.name!);
|
||||||
|
|
@ -195,7 +184,9 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
|
||||||
matchesSize;
|
matchesSize;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_logger.warning(
|
_logger.warning(
|
||||||
'Error parsing stream info: ${streamData.stream.name}', e);
|
'Error parsing stream info: ${streamData.stream.name}',
|
||||||
|
e,
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
@ -263,154 +254,6 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStreamCard(StreamWithAddon streamData, ThemeData theme) {
|
|
||||||
final stream = streamData.stream;
|
|
||||||
final info = StreamParser.parseStreamName(stream.name ?? '');
|
|
||||||
|
|
||||||
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!)}?';
|
|
||||||
|
|
||||||
final List<String> query = [];
|
|
||||||
|
|
||||||
if (widget.meta.selectedVideoIndex != null) {
|
|
||||||
query.add("index=${widget.meta.selectedVideoIndex}");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stream.behaviorHints?["bingeGroup"] != null) {
|
|
||||||
query.add(
|
|
||||||
"binge-group=${Uri.encodeQueryComponent(stream.behaviorHints?["bingeGroup"])}",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mounted) {
|
|
||||||
context.push(
|
|
||||||
url + query.join("&"),
|
|
||||||
extra: {
|
|
||||||
"meta": widget.meta,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
if (info.resolution != null)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 8),
|
|
||||||
child: StreamTag(
|
|
||||||
text: info.resolution!,
|
|
||||||
color: _getQualityColor(info.resolution!),
|
|
||||||
outlined: true,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
(stream.name ?? 'Unknown Title') +
|
|
||||||
(stream.url != null ? "" : " (Not supported)"),
|
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
stream.title ?? 'Unknown Title',
|
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (stream.description != null)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 4),
|
|
||||||
child: Text(
|
|
||||||
stream.description!,
|
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (streamData.addonName != null)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 4),
|
|
||||||
child: Text(
|
|
||||||
'From: ${streamData.addonName}',
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (info.quality != null &&
|
|
||||||
info.codec != null &&
|
|
||||||
info.audio != null &&
|
|
||||||
info.size != null &&
|
|
||||||
info.unrated)
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Wrap(
|
|
||||||
spacing: 8,
|
|
||||||
runSpacing: 8,
|
|
||||||
children: [
|
|
||||||
if (info.quality != null)
|
|
||||||
StreamTag(
|
|
||||||
text: info.quality!,
|
|
||||||
color: theme.colorScheme.secondary,
|
|
||||||
),
|
|
||||||
if (info.codec != null)
|
|
||||||
StreamTag(
|
|
||||||
text: info.codec!,
|
|
||||||
color: theme.colorScheme.tertiary,
|
|
||||||
),
|
|
||||||
if (info.audio != null)
|
|
||||||
StreamTag(
|
|
||||||
text: info.audio!,
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
),
|
|
||||||
if (info.size != null)
|
|
||||||
StreamTag(
|
|
||||||
text: StreamParser.getSizeCategory(info.size),
|
|
||||||
color: theme.colorScheme.secondary,
|
|
||||||
),
|
|
||||||
if (info.unrated)
|
|
||||||
StreamTag(
|
|
||||||
text: 'UNRATED',
|
|
||||||
color: theme.colorScheme.error,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_isLoading) {
|
if (_isLoading) {
|
||||||
|
|
@ -490,18 +333,13 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
|
||||||
value: addon,
|
value: addon,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Checkbox(
|
Icon(
|
||||||
value: _selectedAddons.contains(addon),
|
_selectedAddons.contains(addon)
|
||||||
onChanged: (bool? value) {
|
? Icons.check_circle
|
||||||
setState(() {
|
: Icons.circle_outlined,
|
||||||
if (value == true) {
|
),
|
||||||
_selectedAddons.add(addon);
|
const SizedBox(
|
||||||
} else {
|
width: 6,
|
||||||
_selectedAddons.remove(addon);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
Text(addon),
|
Text(addon),
|
||||||
],
|
],
|
||||||
|
|
@ -538,11 +376,16 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
|
||||||
)
|
)
|
||||||
: ListView.separated(
|
: ListView.separated(
|
||||||
itemCount: filteredStreams.length,
|
itemCount: filteredStreams.length,
|
||||||
separatorBuilder: (context, index) =>
|
separatorBuilder: (context, index) => const Divider(
|
||||||
const Divider(height: 1),
|
height: 1,
|
||||||
|
),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final streamData = filteredStreams[index];
|
final streamData = filteredStreams[index];
|
||||||
return _buildStreamCard(streamData, theme);
|
|
||||||
|
return StreamCard(
|
||||||
|
streamWithAddon: streamData,
|
||||||
|
meta: widget.meta,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -557,6 +400,13 @@ class StreamWithAddon {
|
||||||
final String? addonName;
|
final String? addonName;
|
||||||
|
|
||||||
StreamWithAddon({required this.stream, this.addonName});
|
StreamWithAddon({required this.stream, this.addonName});
|
||||||
|
|
||||||
|
StreamWithAddon copy() {
|
||||||
|
return StreamWithAddon(
|
||||||
|
stream: stream.copyWith(),
|
||||||
|
addonName: addonName,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class StreamTag extends StatelessWidget {
|
class StreamTag extends StatelessWidget {
|
||||||
|
|
@ -597,3 +447,181 @@ class StreamTag extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class StreamCard extends StatelessWidget {
|
||||||
|
final StreamWithAddon streamWithAddon;
|
||||||
|
final Meta meta;
|
||||||
|
|
||||||
|
const StreamCard({
|
||||||
|
super.key,
|
||||||
|
required this.meta,
|
||||||
|
required this.streamWithAddon,
|
||||||
|
});
|
||||||
|
|
||||||
|
VideoStream get stream {
|
||||||
|
return streamWithAddon.stream.copyWith();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
final info = StreamParser.parseStreamName(stream.name ?? '');
|
||||||
|
|
||||||
|
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/${meta.type}/${meta.id}/${Uri.encodeQueryComponent(stream.url!)}?';
|
||||||
|
|
||||||
|
final List<String> query = [];
|
||||||
|
|
||||||
|
if (meta.selectedVideoIndex != null) {
|
||||||
|
query.add("index=${meta.selectedVideoIndex}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stream.behaviorHints?["bingeGroup"] != null) {
|
||||||
|
query.add(
|
||||||
|
"binge-group=${Uri.encodeQueryComponent(stream.behaviorHints?["bingeGroup"])}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
context.push(
|
||||||
|
url + query.join("&"),
|
||||||
|
extra: {
|
||||||
|
"meta": meta,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (info.resolution != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
|
child: StreamTag(
|
||||||
|
text: info.resolution!,
|
||||||
|
color: _getQualityColor(info.resolution!),
|
||||||
|
outlined: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
(stream.name ?? 'Unknown Title') +
|
||||||
|
(stream.url != null ? "" : " (Not supported)"),
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (stream.title != null)
|
||||||
|
Text(
|
||||||
|
stream.title!,
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (stream.description != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 4),
|
||||||
|
child: Text(
|
||||||
|
stream.description!,
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (streamWithAddon.addonName != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 4),
|
||||||
|
child: Text(
|
||||||
|
'From: ${streamWithAddon.addonName}',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (info.quality != null &&
|
||||||
|
info.codec != null &&
|
||||||
|
info.audio != null &&
|
||||||
|
info.size != null &&
|
||||||
|
info.unrated)
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: [
|
||||||
|
if (info.quality != null)
|
||||||
|
StreamTag(
|
||||||
|
text: info.quality!,
|
||||||
|
color: theme.colorScheme.secondary,
|
||||||
|
),
|
||||||
|
if (info.codec != null)
|
||||||
|
StreamTag(
|
||||||
|
text: info.codec!,
|
||||||
|
color: theme.colorScheme.tertiary,
|
||||||
|
),
|
||||||
|
if (info.audio != null)
|
||||||
|
StreamTag(
|
||||||
|
text: info.audio!,
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
if (info.size != null)
|
||||||
|
StreamTag(
|
||||||
|
text: StreamParser.getSizeCategory(info.size),
|
||||||
|
color: theme.colorScheme.secondary,
|
||||||
|
),
|
||||||
|
if (info.unrated)
|
||||||
|
StreamTag(
|
||||||
|
text: 'UNRATED',
|
||||||
|
color: theme.colorScheme.error,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getQualityColor(String resolution) {
|
||||||
|
final res = resolution.toUpperCase();
|
||||||
|
if (res.contains('2160P') || res.contains('4K') || res.contains('UHD')) {
|
||||||
|
return Colors.amberAccent;
|
||||||
|
} else if (res.contains('1080P')) {
|
||||||
|
return Colors.blue;
|
||||||
|
} else if (res.contains('720P')) {
|
||||||
|
return Colors.green;
|
||||||
|
}
|
||||||
|
return Colors.grey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,11 @@ Future<void> openVideoStream(BuildContext context, Meta meta) async {
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: StreamioStreamList(
|
body: StreamioStreamList(
|
||||||
meta: meta,
|
meta: meta.type == "series"
|
||||||
|
? meta.copyWith(
|
||||||
|
selectedVideoIndex: meta.selectedVideoIndex ?? 0,
|
||||||
|
)
|
||||||
|
: meta,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -115,19 +119,21 @@ class StreamioHeroSection extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton.filled(
|
if (meta.type != "series")
|
||||||
onPressed: () {
|
IconButton.filled(
|
||||||
_logger.info('Play button pressed for ${meta.name}');
|
onPressed: () {
|
||||||
|
_logger.info('Play button pressed for ${meta.name}');
|
||||||
|
|
||||||
openVideoStream(context, meta);
|
openVideoStream(context, meta);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.play_arrow, size: 32),
|
icon: const Icon(Icons.play_arrow, size: 32),
|
||||||
style: IconButton.styleFrom(
|
style: IconButton.styleFrom(
|
||||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
backgroundColor:
|
||||||
foregroundColor:
|
Theme.of(context).colorScheme.primary,
|
||||||
Theme.of(context).colorScheme.onPrimary,
|
foregroundColor:
|
||||||
|
Theme.of(context).colorScheme.onPrimary,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -149,11 +155,18 @@ class StreamioHeroSection extends StatelessWidget {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'${meta.year ?? ''} • ${meta.runtime ?? ''} • ${meta.genres?.join(', ') ?? ''}',
|
'${meta.year ?? ''} • ${meta.genres?.join(', ') ?? ''}',
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
color: Colors.white.withValues(alpha: 0.7),
|
color: Colors.white.withValues(alpha: 0.7),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (meta.runtime != null)
|
||||||
|
Text(
|
||||||
|
meta.runtime!,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
color: Colors.white.withValues(alpha: 0.7),
|
||||||
|
),
|
||||||
|
),
|
||||||
if (meta.imdbRating.isNotEmpty &&
|
if (meta.imdbRating.isNotEmpty &&
|
||||||
meta.imdbRating.toString() != "null") ...[
|
meta.imdbRating.toString() != "null") ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
@ -171,19 +184,21 @@ class StreamioHeroSection extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(
|
if (meta.type != "series") ...[
|
||||||
height: 12,
|
const SizedBox(
|
||||||
),
|
height: 12,
|
||||||
OutlinedButton.icon(
|
),
|
||||||
onPressed: () {
|
OutlinedButton.icon(
|
||||||
openVideoStream(
|
onPressed: () {
|
||||||
context,
|
openVideoStream(
|
||||||
meta,
|
context,
|
||||||
);
|
meta,
|
||||||
},
|
);
|
||||||
icon: const Icon(Icons.play_arrow),
|
},
|
||||||
label: const Text("Play"),
|
icon: const Icon(Icons.play_arrow),
|
||||||
),
|
label: const Text("Play"),
|
||||||
|
),
|
||||||
|
],
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 12,
|
height: 12,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue