Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions CLAUDE.md

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions lib/data/models/json.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ typedef Json = Map<String, dynamic>;
/// [value], or null when it is null or blank.
String? nonEmpty(String? value) => value == null || value.trim().isEmpty ? null : value;

/// A performer's, studio's or tag's image, or null when Stash only has its
/// placeholder: Stash marks those URLs with `default=true` and serves an SVG,
/// which the image decoder can't read (Android logs every failed attempt).
String? stashImageUrl(String? path) {
final url = nonEmpty(path);
if (url == null) return null;
return Uri.tryParse(url)?.queryParameters['default'] == 'true' ? null : url;
}

DateTime? parseDate(String? value) => value == null || value.isEmpty ? null : DateTime.tryParse(value);

/// Whether [a] and [b] are the same JSON value (maps, lists, numbers
Expand Down
4 changes: 2 additions & 2 deletions lib/data/models/performer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class Performer {
factory Performer.fromRef(Fragment$PerformerRef p, {String? country, bool favorite = false}) => Performer(
id: p.id,
name: p.name,
imageUrl: nonEmpty(p.image_path),
imageUrl: stashImageUrl(p.image_path),
country: nonEmpty(country),
favorite: favorite,
);
Expand All @@ -44,7 +44,7 @@ class Performer {
id: p.id,
name: p.name,
disambiguation: nonEmpty(p.disambiguation),
imageUrl: nonEmpty(p.image_path),
imageUrl: stashImageUrl(p.image_path),
country: nonEmpty(p.country),
birthdate: parseDate(p.birthdate),
gender: p.gender == null ? null : toJson$Enum$GenderEnum(p.gender!),
Expand Down
4 changes: 2 additions & 2 deletions lib/data/models/studio.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ class Studio {
final List<Studio> children;

factory Studio.fromRef(Fragment$StudioRef s, {int sceneCount = 0}) =>
Studio(id: s.id, name: s.name, imageUrl: nonEmpty(s.image_path), sceneCount: sceneCount);
Studio(id: s.id, name: s.name, imageUrl: stashImageUrl(s.image_path), sceneCount: sceneCount);

factory Studio.fromFields(Fragment$StudioFields s) {
final parent = s.parent_studio;
final details = s is Fragment$StudioDetails ? s : null;
return Studio(
id: s.id,
name: s.name,
imageUrl: nonEmpty(s.image_path),
imageUrl: stashImageUrl(s.image_path),
url: nonEmpty(s.url),
details: nonEmpty(details?.details),
sceneCount: s.scene_count,
Expand Down
2 changes: 1 addition & 1 deletion lib/data/models/tag.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class Tag {
factory Tag.fromFields(Fragment$TagFields t, {String? description}) => Tag(
id: t.id,
name: t.name,
imageUrl: nonEmpty(t.image_path),
imageUrl: stashImageUrl(t.image_path),
description: nonEmpty(description),
sceneCount: t.scene_count,
);
Expand Down
15 changes: 8 additions & 7 deletions lib/features/player/drag_to_minimize.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart';
import 'package:miniplayer/miniplayer.dart';

import 'sliding_panel.dart';

/// Lets a downward swipe on the expanded player minimize it, following the
/// finger and snapping on release, like YouTube.
///
/// The miniplayer package only drags the panel when no inner widget claims
/// the gesture, but the video controls and the details list both do. So this
/// The expanded [SlidingPanel] takes no drags itself, and the video controls
/// and the details list both claim vertical drags. So this
/// watches raw pointer events (outside the gesture arena) and drives the
/// panel through its [controller]. A swipe starts the drag when it begins on
/// the video, or in the details while they are scrolled to the top.
Expand All @@ -23,7 +24,7 @@ class DragToMinimize extends StatefulWidget {

/// Only start drags while the panel is fully expanded.
final bool enabled;
final MiniplayerController controller;
final SlidingPanelController controller;
final double minHeight;
final double maxHeight;

Expand Down Expand Up @@ -53,7 +54,7 @@ class _DragToMinimizeState extends State<DragToMinimize> {
_pointers++;
if (_pointers > 1) {
// A second finger: a pinch on the video, not a swipe down.
if (_dragging) widget.controller.animateToHeight(state: PanelState.MAX);
if (_dragging) widget.controller.expand();
_dragging = false;
_candidate = false;
return;
Expand Down Expand Up @@ -82,7 +83,7 @@ class _DragToMinimizeState extends State<DragToMinimize> {
}
}
final height = (widget.maxHeight - delta.dy).clamp(widget.minHeight, widget.maxHeight);
widget.controller.animateToHeight(height: height.roundToDouble(), duration: Duration.zero);
widget.controller.jumpTo(height.roundToDouble());
}

void _onEnd(PointerEvent e) {
Expand All @@ -95,7 +96,7 @@ class _DragToMinimizeState extends State<DragToMinimize> {
velocity: velocity,
maxHeight: widget.maxHeight,
);
widget.controller.animateToHeight(state: collapse ? PanelState.MIN : PanelState.MAX);
collapse ? widget.controller.collapse() : widget.controller.expand();
}
_start = null;
_dragging = false;
Expand Down
20 changes: 0 additions & 20 deletions lib/features/player/player_controls.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,26 +23,6 @@ import 'preview_seek_bar.dart';
import 'video_controls.dart';
import '../../l10n/l10n.dart';

/// Claims single taps inside the expanded player.
///
/// The miniplayer package wraps the whole panel in a `GestureDetector` whose
/// `onTap` collapses an expanded panel. media_kit detects single taps with a
/// raw `Listener`, so without this guard every tap on the video (or on empty
/// space in the details) ends up collapsing the player. A deeper tap
/// recognizer wins the gesture arena; buttons, double-tap and drag still work.
class PanelTapGuard extends StatelessWidget {
const PanelTapGuard({super.key, this.enabled = true, required this.child});

/// When false, taps reach the miniplayer again (e.g. tap-to-expand on the
/// collapsed bar). Toggling keeps the widget tree stable.
final bool enabled;
final Widget child;

@override
Widget build(BuildContext context) =>
GestureDetector(behavior: HitTestBehavior.opaque, onTap: enabled ? () {} : null, child: child);
}

/// media_kit's state of the one [PlayerVideo], to enter fullscreen when the
/// phone is turned.
final playerVideoKey = GlobalKey<VideoState>();
Expand Down
12 changes: 4 additions & 8 deletions lib/features/player/player_providers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:miniplayer/miniplayer.dart';

import '../../core/config/server_config.dart';
import '../../data/models/scene.dart';
Expand All @@ -16,6 +15,7 @@ import '../cast/cast_media.dart';
import '../cast/cast_providers.dart';
import '../cast/cast_service.dart' show CastKind;
import 'playback_tracker.dart';
import 'sliding_panel.dart';
import 'video_zoom.dart';

const double kMiniPlayerHeight = 64;
Expand All @@ -32,11 +32,7 @@ final videoControllerProvider = Provider<VideoController>(
(ref) => VideoController(ref.watch(playerProvider)),
);

final miniplayerControllerProvider = Provider<MiniplayerController>((ref) {
final controller = MiniplayerController();
ref.onDispose(controller.dispose);
return controller;
});
final miniplayerControllerProvider = Provider<SlidingPanelController>((ref) => SlidingPanelController());

/// Current miniplayer height in pixels; drives the bottom navigation bar.
final miniplayerHeightProvider = Provider<ValueNotifier<double>>((ref) {
Expand Down Expand Up @@ -469,9 +465,9 @@ class NowPlayingNotifier extends Notifier<Scene?> {
);
}

void expand() => ref.read(miniplayerControllerProvider).animateToHeight(state: PanelState.MAX);
void expand() => ref.read(miniplayerControllerProvider).expand();

void collapse() => ref.read(miniplayerControllerProvider).animateToHeight(state: PanelState.MIN);
void collapse() => ref.read(miniplayerControllerProvider).collapse();

/// Shows edited metadata for the playing scene without reopening it.
void replaceScene(Scene scene) {
Expand Down
4 changes: 2 additions & 2 deletions lib/features/player/player_transition.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ class PlayerTransition {
bool get isExpanded => progress >= 0.999;
bool get isCollapsed => progress <= 0.001;

/// Whether details exist at all; skipped while collapsed so the "Up next"
/// list isn't loaded for a closed player.
/// Whether the details are shown (they stay built but hidden while
/// collapsed, see `PlayerPanel`).
bool get showDetails => progress > 0.01;
}
Loading
Loading