Prepare Lunaris 1.1.2 experimental

This commit is contained in:
2026-05-26 11:43:25 +03:00
parent 369dee02be
commit 9cff9e5307
42 changed files with 2041 additions and 303 deletions
@@ -5,15 +5,31 @@ import '../../../core/utils/result.dart';
final postDetailsControllerProvider =
FutureProvider.family<Post?, PostDetailsArgs>((ref, args) async {
if (args.initialPost != null) return args.initialPost;
Future<Post?> enrich(Post? post) async {
if (post == null) return null;
final shouldEnrich = post.tagGroups.isEmpty ||
(post.tagGroups.length == 1 && post.tagGroups.containsKey('general'));
if (!shouldEnrich) return post;
final enriched =
await ref.watch(providerManagerProvider).enrichPostTags(post);
if (enriched is Success<Post>) {
await ref.watch(cacheServiceProvider).cachePosts([enriched.data]);
return enriched.data;
}
return post;
}
if (args.initialPost != null) return enrich(args.initialPost);
final cached = await ref
.watch(postRepositoryProvider)
.getCachedPost(args.postId, args.providerId);
if (cached is Success<Post?> && cached.data != null) return cached.data;
if (cached is Success<Post?> && cached.data != null) {
return enrich(cached.data);
}
final remote = await ref
.watch(providerManagerProvider)
.getPost(args.providerId, args.postId);
return remote is Success<Post?> ? remote.data : null;
return remote is Success<Post?> ? enrich(remote.data) : null;
});
class PostDetailsArgs {
@@ -400,7 +400,9 @@ class _PostMediaViewerState extends State<PostMediaViewer>
coverVideo: _coverVideo,
errorMessage: _videoError,
onRetry: _retryVideo,
onChanged: (_) => _emitPlaybackPreferences(),
onChanged: (snapshot) {
unawaited(_applyPlaybackSnapshot(snapshot));
},
),
transitionsBuilder: (_, animation, __, child) => FadeTransition(
opacity: animation,
@@ -410,22 +412,26 @@ class _PostMediaViewerState extends State<PostMediaViewer>
);
if (mounted) setState(() => _inFullscreen = false);
if (result != null) {
_playbackMemory[widget.post.cacheKey] = result;
setState(() {
_muted = result.muted;
_loopVideo = result.loopVideo;
_coverVideo = result.coverVideo;
_halfVolume = result.halfVolume;
});
await _applyVolume();
await player.setPlaylistMode(
_loopVideo ? PlaylistMode.single : PlaylistMode.none,
);
_emitPlaybackPreferences();
await _applyPlaybackSnapshot(result);
return;
}
}
Future<void> _applyPlaybackSnapshot(VideoPlaybackSnapshot snapshot) async {
if (!mounted) return;
setState(() {
_muted = snapshot.muted;
_loopVideo = snapshot.loopVideo;
_coverVideo = snapshot.coverVideo;
_halfVolume = snapshot.halfVolume;
});
await _applyVolume();
await _player?.setPlaylistMode(
_loopVideo ? PlaylistMode.single : PlaylistMode.none,
);
_emitPlaybackPreferences();
}
VideoPlaybackSnapshot _snapshot() {
final player = _player;
return VideoPlaybackSnapshot(
@@ -1,34 +1,42 @@
import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart';
import 'package:go_router/go_router.dart';
import '../../../../backend/backend.dart';
import '../../../../shared/widgets/tag_chip.dart';
class PostTagsPanel extends StatelessWidget {
class PostTagsPanel extends StatefulWidget {
const PostTagsPanel({required this.post, super.key});
final Post post;
@override
State<PostTagsPanel> createState() => _PostTagsPanelState();
}
class _PostTagsPanelState extends State<PostTagsPanel> {
final _expandedGroups = <String>{};
@override
Widget build(BuildContext context) {
final groups = _groups(post);
final groups = _groups(widget.post);
if (groups.isEmpty) return const SizedBox.shrink();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (final entry in groups.entries) ...[
Padding(
padding: const EdgeInsets.only(top: 8, bottom: 6),
child: Text(
_label(entry.key),
style: Theme.of(context).textTheme.labelLarge,
),
),
_HorizontalTagRow(
tags: entry.value.take(120).toList(),
for (final entry in groups.entries)
_TagGroupBlock(
label: _label(entry.key),
tags: entry.value,
expanded: _expandedGroups.contains(entry.key),
onToggleExpanded: () {
setState(() {
if (!_expandedGroups.add(entry.key)) {
_expandedGroups.remove(entry.key);
}
});
},
onTap: (tag) => context.go('/?q=${Uri.encodeQueryComponent(tag)}'),
),
],
],
);
}
@@ -48,11 +56,13 @@ class PostTagsPanel extends StatelessWidget {
if (tags != null && tags.isNotEmpty) ordered[key] = tags;
}
for (final entry in post.tagGroups.entries) {
ordered.putIfAbsent(entry.key, () => entry.value);
if (entry.value.isNotEmpty) {
ordered.putIfAbsent(entry.key, () => entry.value);
}
}
return ordered;
}
return {'general': post.tags};
return post.tags.isEmpty ? const {} : {'general': post.tags};
}
String _label(String key) {
@@ -68,51 +78,71 @@ class PostTagsPanel extends StatelessWidget {
}
}
class _HorizontalTagRow extends StatefulWidget {
const _HorizontalTagRow({
class _TagGroupBlock extends StatelessWidget {
const _TagGroupBlock({
required this.label,
required this.tags,
required this.expanded,
required this.onToggleExpanded,
required this.onTap,
});
static const _collapsedLimit = 36;
final String label;
final List<String> tags;
final bool expanded;
final VoidCallback onToggleExpanded;
final ValueChanged<String> onTap;
@override
State<_HorizontalTagRow> createState() => _HorizontalTagRowState();
}
class _HorizontalTagRowState extends State<_HorizontalTagRow> {
final _controller = ScrollController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: 36,
child: Listener(
onPointerSignal: (event) {
if (event is! PointerScrollEvent || !_controller.hasClients) return;
final target = (_controller.offset + event.scrollDelta.dy)
.clamp(0.0, _controller.position.maxScrollExtent)
.toDouble();
_controller.jumpTo(target);
},
child: ListView.separated(
controller: _controller,
scrollDirection: Axis.horizontal,
primary: false,
itemCount: widget.tags.length,
separatorBuilder: (_, __) => const SizedBox(width: 8),
itemBuilder: (context, index) => TagChip(
tag: widget.tags[index],
onTap: () => widget.onTap(widget.tags[index]),
final visibleTags = expanded || tags.length <= _collapsedLimit
? tags
: tags.take(_collapsedLimit).toList(growable: false);
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(label, style: Theme.of(context).textTheme.labelLarge),
const SizedBox(width: 8),
Text(
'${tags.length}',
style: Theme.of(context).textTheme.labelMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
const SizedBox(height: 7),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (final tag in visibleTags)
TagChip(
tag: tag,
onTap: () => onTap(tag),
),
if (tags.length > _collapsedLimit)
ActionChip(
avatar: Icon(
expanded
? Icons.keyboard_arrow_up_rounded
: Icons.keyboard_arrow_down_rounded,
size: 18,
),
label: Text(
expanded ? 'Collapse' : '+${tags.length - _collapsedLimit}',
),
onPressed: onToggleExpanded,
),
],
),
],
),
);
}