mirror of
https://github.com/madari-media/madari-oss.git
synced 2026-01-11 22:40:23 +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")
|
||||
final int? moviedbId;
|
||||
@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")
|
||||
final List<Trailer>? trailers;
|
||||
@JsonKey(name: "popularity")
|
||||
|
|
@ -432,7 +445,7 @@ class Meta {
|
|||
this.logo,
|
||||
this.awards,
|
||||
this.moviedbId,
|
||||
this.runtime,
|
||||
this.runtime_,
|
||||
this.trailers,
|
||||
this.popularity,
|
||||
required this.id,
|
||||
|
|
@ -520,7 +533,7 @@ class Meta {
|
|||
logo: logo ?? this.logo,
|
||||
awards: awards ?? this.awards,
|
||||
moviedbId: moviedbId ?? this.moviedbId,
|
||||
runtime: runtime ?? this.runtime,
|
||||
runtime_: runtime ?? this.runtime,
|
||||
trailers: trailers ?? this.trailers,
|
||||
popularity: popularity ?? this.popularity,
|
||||
id: id ?? this.id,
|
||||
|
|
@ -898,6 +911,14 @@ class VideoStream {
|
|||
_$VideoStreamFromJson(json);
|
||||
|
||||
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 {
|
||||
|
|
@ -1002,8 +1023,10 @@ class StreamParser {
|
|||
final unratedMatch = _unratedRegex.hasMatch(name);
|
||||
final sizeMatch = _sizeRegex.firstMatch(name);
|
||||
|
||||
final res = resMatch?.group(1)?.toUpperCase();
|
||||
|
||||
return StreamInfo(
|
||||
resolution: resMatch?.group(1)?.toUpperCase(),
|
||||
resolution: res == "2160P" ? "4K" : res,
|
||||
quality: qualMatch?.group(1)?.toUpperCase(),
|
||||
codec: codecMatch?.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/features/streamio_addons/extension/query_extension.dart';
|
||||
import 'package:madari_client/utils/array-extension.dart';
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
import '../../pocketbase/service/pocketbase.service.dart';
|
||||
import '../../widgetter/plugins/stremio/models/cast_info.dart';
|
||||
|
|
@ -208,8 +209,22 @@ class StremioAddonService {
|
|||
|
||||
await getInstalledAddons().refetch();
|
||||
} catch (e, stack) {
|
||||
print(e);
|
||||
print(stack);
|
||||
_logger.warning("Error to save addon", e, 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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:cached_query_flutter/cached_query_flutter.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:madari_client/features/streamio_addons/extension/query_extension.dart';
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
import '../models/stremio_base_types.dart';
|
||||
import '../service/stremio_addon_service.dart';
|
||||
|
|
@ -65,7 +66,7 @@ class _AddAddonSheetState extends State<AddAddonSheet> {
|
|||
final manifest = await query.queryFn();
|
||||
if (!mounted) return;
|
||||
|
||||
_installAddon(manifest);
|
||||
await _installAddon(manifest);
|
||||
}
|
||||
|
||||
Future<void> _installAddon(StremioManifest manifest) async {
|
||||
|
|
@ -79,9 +80,17 @@ class _AddAddonSheetState extends State<AddAddonSheet> {
|
|||
Navigator.of(context).pop();
|
||||
}
|
||||
} catch (e) {
|
||||
print(e);
|
||||
if (mounted) {
|
||||
if (e is ClientException) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed to install addon: ${e.response}')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed to install addon: $e')),
|
||||
SnackBar(content: Text('Failed to install addon: ${e}')),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
|
|
@ -144,7 +153,9 @@ class _AddAddonSheetState extends State<AddAddonSheet> {
|
|||
FilledButton.icon(
|
||||
onPressed: () => _validateManifest(),
|
||||
icon: const Icon(Icons.check),
|
||||
label: const Text('Validate'),
|
||||
label: _isInstalling
|
||||
? const Text("Installing")
|
||||
: const Text('Install'),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
|
|||
Set<String> _selectedAudios = {};
|
||||
Set<String> _selectedSizes = {};
|
||||
final Set<String> _selectedAddons = {};
|
||||
final Map<String, List<StreamWithAddon>> streamsByAddon = {};
|
||||
|
||||
Set<String> _resolutions = {};
|
||||
Set<String> _qualities = {};
|
||||
|
|
@ -53,7 +54,6 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
|
|||
_logger.info('Loading streams for ${widget.meta.id}');
|
||||
|
||||
final addons = service.getInstalledAddons();
|
||||
|
||||
final result = await addons.queryFn();
|
||||
|
||||
final count = result
|
||||
|
|
@ -63,7 +63,6 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
|
|||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
.toList()
|
||||
|
|
@ -86,7 +85,6 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
|
|||
callback: (items, addonName, error) {
|
||||
setState(() {
|
||||
left -= 1;
|
||||
|
||||
if (left <= 0) {
|
||||
_isLoading = false;
|
||||
}
|
||||
|
|
@ -98,7 +96,7 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
|
|||
return;
|
||||
}
|
||||
|
||||
if (items != null) {
|
||||
if (items != null && addonName != null) {
|
||||
final Set<String> resSet = {};
|
||||
final Set<String> qualSet = {};
|
||||
final Set<String> codecSet = {};
|
||||
|
|
@ -106,12 +104,10 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
|
|||
final Set<String> sizeSet = {};
|
||||
|
||||
final streamsWithAddon = items
|
||||
.map(
|
||||
(stream) => StreamWithAddon(
|
||||
stream: stream,
|
||||
addonName: addonName,
|
||||
),
|
||||
)
|
||||
.map((stream) => StreamWithAddon(
|
||||
stream: stream,
|
||||
addonName: addonName,
|
||||
))
|
||||
.toList();
|
||||
|
||||
for (var streamData in streamsWithAddon) {
|
||||
|
|
@ -135,14 +131,13 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
|
|||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
streams.addAll(streamsWithAddon);
|
||||
if (addonName != null) _addons.add(addonName);
|
||||
_resolutions = resSet;
|
||||
_qualities = qualSet;
|
||||
_codecs = codecSet;
|
||||
_audios = audioSet;
|
||||
_sizes = sizeSet;
|
||||
_isLoading = false;
|
||||
streamsByAddon[addonName] = streamsWithAddon;
|
||||
_addons.add(addonName);
|
||||
_resolutions.addAll(resSet);
|
||||
_qualities.addAll(qualSet);
|
||||
_codecs.addAll(codecSet);
|
||||
_audios.addAll(audioSet);
|
||||
_sizes.addAll(sizeSet);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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() {
|
||||
return streams.where((streamData) {
|
||||
if (streamData.stream.name == null) return false;
|
||||
|
||||
if (_selectedAddons.isNotEmpty &&
|
||||
!_selectedAddons.contains(streamData.addonName)) {
|
||||
return false;
|
||||
List<StreamWithAddon> allStreams = [];
|
||||
if (_selectedAddons.isEmpty) {
|
||||
streamsByAddon.values.forEach(allStreams.addAll);
|
||||
} else {
|
||||
for (var addon in _selectedAddons) {
|
||||
if (streamsByAddon.containsKey(addon)) {
|
||||
allStreams.addAll(streamsByAddon[addon]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allStreams.where((streamData) {
|
||||
if (streamData.stream.name == null) return false;
|
||||
|
||||
try {
|
||||
final info = StreamParser.parseStreamName(streamData.stream.name!);
|
||||
|
|
@ -195,7 +184,9 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
|
|||
matchesSize;
|
||||
} catch (e) {
|
||||
_logger.warning(
|
||||
'Error parsing stream info: ${streamData.stream.name}', e);
|
||||
'Error parsing stream info: ${streamData.stream.name}',
|
||||
e,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}).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
|
||||
Widget build(BuildContext context) {
|
||||
if (_isLoading) {
|
||||
|
|
@ -490,18 +333,13 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
|
|||
value: addon,
|
||||
child: Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: _selectedAddons.contains(addon),
|
||||
onChanged: (bool? value) {
|
||||
setState(() {
|
||||
if (value == true) {
|
||||
_selectedAddons.add(addon);
|
||||
} else {
|
||||
_selectedAddons.remove(addon);
|
||||
}
|
||||
});
|
||||
Navigator.pop(context);
|
||||
},
|
||||
Icon(
|
||||
_selectedAddons.contains(addon)
|
||||
? Icons.check_circle
|
||||
: Icons.circle_outlined,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 6,
|
||||
),
|
||||
Text(addon),
|
||||
],
|
||||
|
|
@ -538,11 +376,16 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
|
|||
)
|
||||
: ListView.separated(
|
||||
itemCount: filteredStreams.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
const Divider(height: 1),
|
||||
separatorBuilder: (context, index) => const Divider(
|
||||
height: 1,
|
||||
),
|
||||
itemBuilder: (context, 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;
|
||||
|
||||
StreamWithAddon({required this.stream, this.addonName});
|
||||
|
||||
StreamWithAddon copy() {
|
||||
return StreamWithAddon(
|
||||
stream: stream.copyWith(),
|
||||
addonName: addonName,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
return Scaffold(
|
||||
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(
|
||||
onPressed: () {
|
||||
_logger.info('Play button pressed for ${meta.name}');
|
||||
if (meta.type != "series")
|
||||
IconButton.filled(
|
||||
onPressed: () {
|
||||
_logger.info('Play button pressed for ${meta.name}');
|
||||
|
||||
openVideoStream(context, meta);
|
||||
},
|
||||
icon: const Icon(Icons.play_arrow, size: 32),
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onPrimary,
|
||||
openVideoStream(context, meta);
|
||||
},
|
||||
icon: const Icon(Icons.play_arrow, size: 32),
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -149,11 +155,18 @@ class StreamioHeroSection extends StatelessWidget {
|
|||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'${meta.year ?? ''} • ${meta.runtime ?? ''} • ${meta.genres?.join(', ') ?? ''}',
|
||||
'${meta.year ?? ''} • ${meta.genres?.join(', ') ?? ''}',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
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 &&
|
||||
meta.imdbRating.toString() != "null") ...[
|
||||
const SizedBox(height: 16),
|
||||
|
|
@ -171,19 +184,21 @@ class StreamioHeroSection extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
],
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
openVideoStream(
|
||||
context,
|
||||
meta,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.play_arrow),
|
||||
label: const Text("Play"),
|
||||
),
|
||||
if (meta.type != "series") ...[
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
openVideoStream(
|
||||
context,
|
||||
meta,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.play_arrow),
|
||||
label: const Text("Play"),
|
||||
),
|
||||
],
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
|
|
|
|||
Loading…
Reference in a new issue