Skip to content
Open
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
5 changes: 2 additions & 3 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import 'package:fladder/providers/crash_log_provider.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/providers/shared_provider.dart';
import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/providers/window_title_provider.dart';
import 'package:fladder/routes/auto_router.dart';
import 'package:fladder/theme.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
Expand Down Expand Up @@ -101,7 +102,7 @@ class _FladderApp extends ConsumerWidget {
light: lightTheme,
dark: darkTheme,
child: MaterialApp.router(
onGenerateTitle: (context) => ref.watch(currentTitleProvider),
onGenerateTitle: (context) => ref.watch(windowTitleProvider),
theme: lightTheme,
scrollBehavior: scrollBehaviour.copyWith(
dragDevices: {
Expand Down Expand Up @@ -153,5 +154,3 @@ class _FladderApp extends ConsumerWidget {
);
}
}

final currentTitleProvider = StateProvider<String>((ref) => "Fladder");
2 changes: 2 additions & 0 deletions lib/models/item_base_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ class ItemBaseModel with ItemBaseModelMappable {

String get title => name;

String windowTitle(AppLocalizations l10n) => name;

///Used for retrieving the correct id when fetching queue
String get streamId => id;

Expand Down
8 changes: 8 additions & 0 deletions lib/models/items/episode_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ class EpisodeModel extends ItemStreamModel with EpisodeModelMappable {
};
}

@override
String windowTitle(AppLocalizations l10n) {
final s = season.toString().padLeft(2, '0');
final e = episodeRange.padLeft(2, '0');
final prefix = seriesName != null ? '$seriesName • ' : '';
return '$prefix${l10n.season(1)[0]}$s${l10n.episode(1)[0]}$e $name';
}

@override
String? detailedName(AppLocalizations l10n) => "${subTextShort(l10n)} - $name";

Expand Down
6 changes: 6 additions & 0 deletions lib/models/items/season_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ class SeasonModel extends ItemBaseModel with SeasonModelMappable {
episodes.firstWhereOrNull((element) => element.userData.played == false);
}

@override
String windowTitle(AppLocalizations l10n) {
final prefix = seriesName.isNotEmpty ? '$seriesName • ' : '';
return '$prefix${localizedName(l10n)}';
}

@override
bool get syncAble => episodes.isNotEmpty && episodes.any((element) => element.syncAble);

Expand Down
73 changes: 73 additions & 0 deletions lib/providers/window_title_provider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/media_playback_model.dart';
import 'package:fladder/providers/video_player_provider.dart';
import 'package:window_manager/window_manager.dart';

/// Manages the context-aware window title.
final windowTitleProvider = StateNotifierProvider<WindowTitleNotifier, String>((ref) {
return WindowTitleNotifier(ref);
});

class WindowTitleNotifier extends StateNotifier<String> {
final Ref ref;
WindowTitleNotifier(this.ref) : super('Fladder') {
// Listen to player state changes to handle minimized <-> maximized transitions
ref.listen(mediaPlaybackProvider.select((v) => v.state), (_, __) => _update());
}

final Map<Object, String> _titles = {};
final List<Object> _stackKeys = [];
String? _playTitle;

void updateTitle(Object key, String title) {
_stackKeys.remove(key);
_stackKeys.add(key);
_titles[key] = title;
_update();
}

void removeTitle(Object key) {
final removed = _stackKeys.remove(key);
_titles.remove(key);
if (removed) {
_update();
}
}

void clearStack() {
_stackKeys.clear();
_titles.clear();
_update();
}

void setPlayTitle(String? title) {
_playTitle = title;
_update();
}

void _update() {
final nav = _stackKeys.isNotEmpty ? _titles[_stackKeys.last] : null;
final playerState = ref.read(mediaPlaybackProvider).state;

final isPlayerActive = playerState != VideoPlayerState.disposed;
final isPlayerMinimized = playerState == VideoPlayerState.minimized;

// Use playTitle if player is active and expanded/fullscreen.
// If player is minimized or inactive, prefer navigation title.
final title = (isPlayerActive && !isPlayerMinimized) ? (_playTitle ?? nav) : (nav ?? _playTitle);

final newState = kIsWeb ? (title != null ? 'Fladder • $title' : 'Fladder') : (title ?? 'Fladder');

if (state == newState) return;

Future.microtask(() {
state = newState;
});

if (!kIsWeb && (Platform.isLinux || Platform.isMacOS || Platform.isWindows)) {
windowManager.setTitle(newState);
}
}
}
1 change: 1 addition & 0 deletions lib/screens/details_screens/episode_detail_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class _ItemDetailScreenState extends ConsumerState<EpisodeDetailScreen> {

return DetailScaffold(
label: widget.item.name,
windowTitle: widget.item.windowTitle(context.localized),
item: details.episode,
actions: (context) => details.episode?.generateActions(
context,
Expand Down
1 change: 1 addition & 0 deletions lib/screens/details_screens/season_detail_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class _SeasonDetailScreenState extends ConsumerState<SeasonDetailScreen> {

return DetailScaffold(
label: details?.localizedName(context.localized) ?? "",
windowTitle: details?.windowTitle(context.localized),
item: details,
actions: (context) => details?.generateActions(context, ref, exclude: {
ItemActions.details,
Expand Down
99 changes: 63 additions & 36 deletions lib/screens/shared/detail_scaffold.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/providers/sync/sync_provider_helpers.dart';
import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/providers/window_title_provider.dart';
import 'package:fladder/routes/auto_router.gr.dart';
import 'package:fladder/screens/syncing/sync_button.dart';
import 'package:fladder/screens/syncing/sync_item_details.dart';
Expand Down Expand Up @@ -38,6 +39,7 @@ Future<Color?> getDominantColor(ImageProvider imageProvider) async {

class DetailScaffold extends ConsumerStatefulWidget {
final String label;
final String? windowTitle;
final ItemBaseModel? item;
final List<ItemAction>? Function(BuildContext context)? actions;
final Color? backgroundColor;
Expand All @@ -47,6 +49,7 @@ class DetailScaffold extends ConsumerStatefulWidget {
final bool posterFillsContent;
const DetailScaffold({
required this.label,
this.windowTitle,
this.item,
this.actions,
this.backgroundColor,
Expand All @@ -70,9 +73,37 @@ class _DetailScaffoldState extends ConsumerState<DetailScaffold> {
ImageProvider? _lastRequestedImage;
ImageData? _lastColorImage;

void _pushTitle() {
final isCurrent = ModalRoute.of(context)?.isCurrent ?? false;
if (!isCurrent) return;

final newTitle = widget.windowTitle ?? widget.item?.windowTitle(context.localized) ?? widget.label;
if (newTitle.isNotEmpty) {
ref.read(windowTitleProvider.notifier).updateTitle(this, newTitle);
}
}

@override
void initState() {
super.initState();
}

@override
void didChangeDependencies() {
super.didChangeDependencies();
_pushTitle();
}

@override
void dispose() {
ref.read(windowTitleProvider.notifier).removeTitle(this);
super.dispose();
}

@override
void didUpdateWidget(covariant DetailScaffold oldWidget) {
super.didUpdateWidget(oldWidget);
_pushTitle();
updateImage();
_updateDominantColor();
if (widget.item != null && widget.item?.id != item?.id) {
Expand Down Expand Up @@ -149,13 +180,13 @@ class _DetailScaffoldState extends ConsumerState<DetailScaffold> {
return PullToRefresh(
onRefresh: () async {
await widget.onRefresh?.call();
setState(() {
if (context.mounted) {
if (mounted) {
setState(() {
if (widget.backDrops?.backDrop?.contains(backgroundImage) == true) {
backgroundImage = widget.backDrops?.randomBackDrop;
}
}
});
});
}
},
refreshOnStart: true,
child: (context) => Scaffold(
Expand All @@ -180,31 +211,29 @@ class _DetailScaffoldState extends ConsumerState<DetailScaffold> {
alignment: Alignment.topCenter,
child: Padding(
padding: EdgeInsets.only(left: sideBarPadding / 1.5, top: topBarPadding / 1.5),
child: RepaintBoundary(
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: double.infinity,
minHeight: minHeight - 22,
maxHeight: maxHeight.clamp(minHeight, 2500) - (20 + topBarPadding),
),
child: FadeEdges(
leftFade: sideBarPadding > 0 ? 0.05 : 0.0,
topFade: topBarPadding > 0 ? 0.1 : 0.0,
bottomFade: 0.2,
child: FadeInImage(
placeholder: ResizeImage(
backgroundImage!.imageProvider,
height: maxHeight ~/ 1.5,
),
placeholderColor: Colors.transparent,
fit: BoxFit.cover,
alignment: Alignment.topCenter,
placeholderFit: BoxFit.cover,
excludeFromSemantics: true,
image: ResizeImage(
backgroundImage!.imageProvider,
height: maxHeight ~/ 1.5,
),
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: double.infinity,
minHeight: minHeight - 22,
maxHeight: maxHeight.clamp(minHeight, 2500) - (20 + topBarPadding),
),
child: FadeEdges(
leftFade: sideBarPadding > 0 ? 0.05 : 0.0,
topFade: topBarPadding > 0 ? 0.1 : 0.0,
bottomFade: 0.2,
child: FadeInImage(
placeholder: ResizeImage(
backgroundImage!.imageProvider,
height: maxHeight ~/ 1.5,
),
placeholderColor: Colors.transparent,
fit: BoxFit.cover,
alignment: Alignment.topCenter,
placeholderFit: BoxFit.cover,
excludeFromSemantics: true,
image: ResizeImage(
backgroundImage!.imageProvider,
height: maxHeight ~/ 1.5,
),
),
),
Expand Down Expand Up @@ -350,13 +379,11 @@ class _DetailScaffoldState extends ConsumerState<DetailScaffold> {
),
],
if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer)
Builder(
builder: (context) => Tooltip(
message: context.localized.refresh,
child: IconButton(
onPressed: () => context.refreshData(),
icon: const Icon(IconsaxPlusLinear.refresh),
),
Tooltip(
message: context.localized.refresh,
child: IconButton(
onPressed: () => context.refreshData(),
icon: const Icon(IconsaxPlusLinear.refresh),
),
),
if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ class _CurrentlyPlayingBarState extends ConsumerState<FloatingPlayerBar> {
Hero(
tag: videoPlayerHeroTag,
child: player.videoWidget(
UniqueKey(),
const ValueKey("mini_player_video"),
BoxFit.fitHeight,
) ??
const SizedBox.shrink(),
Expand Down
15 changes: 14 additions & 1 deletion lib/widgets/navigation_scaffold/navigation_scaffold.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:fladder/models/media_playback_model.dart';
import 'package:fladder/providers/connectivity_provider.dart';
import 'package:fladder/providers/video_player_provider.dart';
import 'package:fladder/providers/views_provider.dart';
import 'package:fladder/providers/window_title_provider.dart';
import 'package:fladder/routes/auto_router.dart';
import 'package:fladder/screens/home_screen.dart';
import 'package:fladder/screens/shared/animated_fade_size.dart';
Expand Down Expand Up @@ -54,6 +55,18 @@ class _NavigationScaffoldState extends ConsumerState<NavigationScaffold> {
});
}

@override
void didUpdateWidget(covariant NavigationScaffold oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.currentRouteName != oldWidget.currentRouteName && currentIndex != -1) {
Future.microtask(() {
if (mounted) {
ref.read(windowTitleProvider.notifier).clearStack();
}
});
}
}

@override
Widget build(BuildContext context) {
final views = ref.watch(viewsProvider.select((value) => value.views));
Expand Down Expand Up @@ -110,7 +123,7 @@ class _NavigationScaffoldState extends ConsumerState<NavigationScaffold> {
child: Builder(builder: (context) {
return Scaffold(
key: _key,
appBar: fullScreenChildRoute ? null : const FladderAppBar(),
appBar: fullScreenChildRoute ? null : FladderAppBar(label: currentIndex == -1 ? "" : null),
extendBodyBehindAppBar: true,
resizeToAvoidBottomInset: false,
extendBody: true,
Expand Down
7 changes: 7 additions & 0 deletions lib/wrappers/media_control_wrapper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/providers/settings/subtitle_settings_provider.dart';
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
import 'package:fladder/providers/video_player_provider.dart';
import 'package:fladder/providers/window_title_provider.dart';
import 'package:fladder/src/video_player_helper.g.dart' hide PlaybackState;
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/wrappers/players/base_player.dart';
Expand Down Expand Up @@ -120,6 +121,11 @@ class MediaControlsWrapper extends BaseAudioHandler implements VideoPlayerContro
}
await _player?.loadVideo(model.media?.url ?? "", play);
_player?.applySubtitleSettings(ref.read(subtitleSettingsProvider));

final context = ref.read(localizationContextProvider);
if (context != null) {
ref.read(windowTitleProvider.notifier).setPlayTitle(model.item.windowTitle(context.localized));
}
}

Future<void> updateTVGuide(TVGuideModel guide) async {
Expand Down Expand Up @@ -300,6 +306,7 @@ class MediaControlsWrapper extends BaseAudioHandler implements VideoPlayerContro
WakelockPlus.disable();
super.stop();
_player?.stop();
ref.read(windowTitleProvider.notifier).setPlayTitle(null);

final position = _player?.lastState.position;
final totalDuration = _player?.lastState.duration;
Expand Down
Loading
Loading