feat: fastlane & screenshot scripts

This commit is contained in:
edde746
2025-11-01 08:03:47 +01:00
parent 380a15442a
commit ad9502d3c7
27 changed files with 2498 additions and 750 deletions
-1
View File
@@ -28,7 +28,6 @@ class _HotKeyRecorderWidgetState extends State<HotKeyRecorderWidget> {
_recordedHotKey = widget.currentHotKey;
}
@override
Widget build(BuildContext context) {
return AlertDialog(
+134 -69
View File
@@ -97,72 +97,80 @@ class _MediaCardState extends State<MediaCard>
metadata: widget.item,
onRefresh: widget.onRefresh,
onTap: () => _handleTap(context),
child: InkWell(
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Poster
if (widget.height != null)
SizedBox(
width: double.infinity,
height: widget.height,
child: _buildPosterWithOverlay(context),
)
else
Expanded(child: _buildPosterWithOverlay(context)),
const SizedBox(height: 4),
// Text content
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
widget.item.displayTitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 13,
height: 1.1,
),
),
if (widget.item.displaySubtitle != null)
child: Semantics(
label: "media-card-${widget.item.ratingKey}",
identifier: "media-card-${widget.item.ratingKey}",
button: true,
child: InkWell(
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Poster
if (widget.height != null)
SizedBox(
width: double.infinity,
height: widget.height,
child: _buildPosterWithOverlay(context),
)
else
Expanded(child: _buildPosterWithOverlay(context)),
const SizedBox(height: 4),
// Text content
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
widget.item.displaySubtitle!,
widget.item.displayTitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: tokens(context).textMuted,
fontSize: 11,
height: 1.1,
),
)
else if (widget.item.parentTitle != null)
Text(
widget.item.parentTitle!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: tokens(context).textMuted,
fontSize: 11,
height: 1.1,
),
)
else if (widget.item.year != null)
Text(
'${widget.item.year}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: tokens(context).textMuted,
fontSize: 11,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 13,
height: 1.1,
),
),
],
),
],
if (widget.item.displaySubtitle != null)
Text(
widget.item.displaySubtitle!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall
?.copyWith(
color: tokens(context).textMuted,
fontSize: 11,
height: 1.1,
),
)
else if (widget.item.parentTitle != null)
Text(
widget.item.parentTitle!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall
?.copyWith(
color: tokens(context).textMuted,
fontSize: 11,
height: 1.1,
),
)
else if (widget.item.year != null)
Text(
'${widget.item.year}',
style: Theme.of(context).textTheme.bodySmall
?.copyWith(
color: tokens(context).textMuted,
fontSize: 11,
height: 1.1,
),
),
],
),
],
),
),
),
),
@@ -188,9 +196,10 @@ class _MediaCardState extends State<MediaCard>
builder: (context, clientProvider, child) {
final client = clientProvider.client;
if (client == null) {
return Container(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
child: const Center(child: Icon(Icons.movie, size: 40)),
return const SkeletonLoader(
child: Center(
child: Icon(Icons.movie, size: 40, color: Colors.white54),
),
);
}
@@ -201,9 +210,7 @@ class _MediaCardState extends State<MediaCard>
height: double.infinity,
filterQuality: FilterQuality.medium,
fadeInDuration: const Duration(milliseconds: 300),
placeholder: (context, url) => Container(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
),
placeholder: (context, url) => const SkeletonLoader(),
errorWidget: (context, url, error) => Container(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
child: const Center(child: Icon(Icons.broken_image, size: 40)),
@@ -212,9 +219,10 @@ class _MediaCardState extends State<MediaCard>
},
);
} else {
return Container(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
child: const Center(child: Icon(Icons.movie, size: 40)),
return const SkeletonLoader(
child: Center(
child: Icon(Icons.movie, size: 40, color: Colors.white54),
),
);
}
}
@@ -278,3 +286,60 @@ class _PosterOverlay extends StatelessWidget {
);
}
}
/// Skeleton loader widget with subtle opacity pulse animation
class SkeletonLoader extends StatefulWidget {
final Widget? child;
final BorderRadius? borderRadius;
const SkeletonLoader({super.key, this.child, this.borderRadius});
@override
State<SkeletonLoader> createState() => _SkeletonLoaderState();
}
class _SkeletonLoaderState extends State<SkeletonLoader>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
);
_animation = Tween<double>(begin: 0.3, end: 0.7).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
);
_animationController.repeat(reverse: true);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Semantics(
label: "skeleton-loader",
identifier: "skeleton-loader",
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest
.withValues(alpha: _animation.value),
borderRadius: widget.borderRadius ?? BorderRadius.circular(8),
),
child: widget.child,
),
);
},
);
}
}
+1 -5
View File
@@ -333,7 +333,6 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
}
}
@override
Widget build(BuildContext context) {
final isMobile = PlatformDetector.isMobile(context);
@@ -688,10 +687,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
final isMacOS = Platform.isMacOS;
final topBar = Padding(
padding: EdgeInsets.only(
left: leftPadding,
right: 16,
),
padding: EdgeInsets.only(left: leftPadding, right: 16),
child: Row(
children: [
AppBarBackButton(
+76 -58
View File
@@ -19,66 +19,84 @@ class ServerListTile extends StatelessWidget {
Widget build(BuildContext context) {
final isOnline = server.isOnline;
return ListTile(
leading: Icon(
Icons.dns,
color: isOnline
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
),
title: Text(server.name),
subtitle: Row(
children: [
Icon(
isOnline ? Icons.circle : Icons.circle_outlined,
size: 10,
color: isOnline ? Colors.green : Colors.grey,
),
const SizedBox(width: 4),
Text(
isOnline ? 'Online' : 'Offline',
style: TextStyle(
fontSize: 12,
color: isOnline ? Colors.green : Colors.grey,
),
),
const SizedBox(width: 8),
Text(
'',
style: TextStyle(
fontSize: 12,
color: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.5),
),
),
const SizedBox(width: 8),
Text(
server.owned ? 'Owned' : 'Shared',
style: const TextStyle(fontSize: 12),
),
],
),
trailing: isCurrentServer
? Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(12),
),
child: Text(
'CURRENT',
style: TextStyle(
fontSize: 10,
color: Theme.of(context).colorScheme.onPrimary,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
return Semantics(
selected: isCurrentServer,
identifier: server.name,
label: server.name,
child: ListTile(
key: ValueKey(server.name),
leading: Icon(
Icons.dns,
color: isOnline
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
semanticLabel: 'Server',
),
title: Text(server.name),
subtitle: Semantics(
label:
'${isOnline ? 'Online' : 'Offline'}, ${server.owned ? 'Owned' : 'Shared'}',
excludeSemantics: true,
child: Row(
children: [
Semantics(
excludeSemantics: true,
child: Icon(
isOnline ? Icons.circle : Icons.circle_outlined,
size: 10,
color: isOnline ? Colors.green : Colors.grey,
),
),
)
: (showTrailingIcon ? const Icon(Icons.chevron_right) : null),
onTap: isCurrentServer ? null : onTap,
enabled: !isCurrentServer,
const SizedBox(width: 4),
Text(
isOnline ? 'Online' : 'Offline',
style: TextStyle(
fontSize: 12,
color: isOnline ? Colors.green : Colors.grey,
),
),
const SizedBox(width: 8),
Semantics(
excludeSemantics: true,
child: Text(
'',
style: TextStyle(
fontSize: 12,
color: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.5),
),
),
),
const SizedBox(width: 8),
Text(
server.owned ? 'Owned' : 'Shared',
style: const TextStyle(fontSize: 12),
),
],
),
),
trailing: isCurrentServer
? Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(12),
),
child: Text(
'CURRENT',
style: TextStyle(
fontSize: 10,
color: Theme.of(context).colorScheme.onPrimary,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
),
)
: (showTrailingIcon ? const Icon(Icons.chevron_right) : null),
onTap: isCurrentServer ? null : onTap,
enabled: !isCurrentServer,
),
);
}
}