mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-09 07:28:14 -07:00
feat: UI Improvements (#428)
This commit is contained in:
commit
83c38d4042
37 changed files with 947 additions and 521 deletions
|
|
@ -1289,5 +1289,7 @@
|
||||||
"syncResumeAll": "Resume all",
|
"syncResumeAll": "Resume all",
|
||||||
"syncStopAll": "Stop all",
|
"syncStopAll": "Stop all",
|
||||||
"syncDeleteAll": "Delete all files",
|
"syncDeleteAll": "Delete all files",
|
||||||
"syncAllFiles": "Sync all files"
|
"syncAllFiles": "Sync all files",
|
||||||
|
"usePostersForLibraryIconsTitle": "Show posters for library icons",
|
||||||
|
"usePostersForLibraryIconsDesc": "Show posters instead of icons for libraries"
|
||||||
}
|
}
|
||||||
|
|
@ -181,7 +181,6 @@ class FladderCupertinoLocalizationsDelegate extends LocalizationsDelegate<Cupert
|
||||||
dayFormat = intl.DateFormat.d(correctedLocale);
|
dayFormat = intl.DateFormat.d(correctedLocale);
|
||||||
weekdayFormat = intl.DateFormat.E(correctedLocale);
|
weekdayFormat = intl.DateFormat.E(correctedLocale);
|
||||||
mediumDateFormat = intl.DateFormat.MMMEd(correctedLocale);
|
mediumDateFormat = intl.DateFormat.MMMEd(correctedLocale);
|
||||||
// TODO(xster): fix when https://github.com/dart-lang/intl/issues/207 is resolved.
|
|
||||||
singleDigitHourFormat = intl.DateFormat('HH', correctedLocale);
|
singleDigitHourFormat = intl.DateFormat('HH', correctedLocale);
|
||||||
singleDigitMinuteFormat = intl.DateFormat.m(correctedLocale);
|
singleDigitMinuteFormat = intl.DateFormat.m(correctedLocale);
|
||||||
doubleDigitMinuteFormat = intl.DateFormat('mm', correctedLocale);
|
doubleDigitMinuteFormat = intl.DateFormat('mm', correctedLocale);
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||||
import 'package:fladder/models/item_base_model.dart';
|
import 'package:fladder/models/item_base_model.dart';
|
||||||
import 'package:fladder/models/items/chapters_model.dart';
|
import 'package:fladder/models/items/chapters_model.dart';
|
||||||
|
import 'package:fladder/models/items/item_shared_models.dart';
|
||||||
import 'package:fladder/models/items/media_segments_model.dart';
|
import 'package:fladder/models/items/media_segments_model.dart';
|
||||||
import 'package:fladder/models/items/media_streams_model.dart';
|
import 'package:fladder/models/items/media_streams_model.dart';
|
||||||
import 'package:fladder/models/items/trick_play_model.dart';
|
import 'package:fladder/models/items/trick_play_model.dart';
|
||||||
|
|
@ -115,6 +116,15 @@ class DirectPlaybackModel extends PlaybackModel {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
DirectPlaybackModel? updateUserData(UserData userData) {
|
||||||
|
return copyWith(
|
||||||
|
item: item.copyWith(
|
||||||
|
userData: userData,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'DirectPlaybackModel(item: $item, playbackInfo: $playbackInfo)';
|
String toString() => 'DirectPlaybackModel(item: $item, playbackInfo: $playbackInfo)';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import 'package:fladder/models/item_base_model.dart';
|
import 'package:fladder/models/item_base_model.dart';
|
||||||
import 'package:fladder/models/items/chapters_model.dart';
|
import 'package:fladder/models/items/chapters_model.dart';
|
||||||
|
import 'package:fladder/models/items/item_shared_models.dart';
|
||||||
import 'package:fladder/models/items/media_segments_model.dart';
|
import 'package:fladder/models/items/media_segments_model.dart';
|
||||||
import 'package:fladder/models/items/media_streams_model.dart';
|
import 'package:fladder/models/items/media_streams_model.dart';
|
||||||
import 'package:fladder/models/items/trick_play_model.dart';
|
import 'package:fladder/models/items/trick_play_model.dart';
|
||||||
|
|
@ -96,6 +97,15 @@ class OfflinePlaybackModel extends PlaybackModel {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
OfflinePlaybackModel? updateUserData(UserData userData) {
|
||||||
|
return copyWith(
|
||||||
|
item: item.copyWith(
|
||||||
|
userData: userData,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'OfflinePlaybackModel(item: $item, syncedItem: $syncedItem)';
|
String toString() => 'OfflinePlaybackModel(item: $item, syncedItem: $syncedItem)';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||||
import 'package:fladder/models/item_base_model.dart';
|
import 'package:fladder/models/item_base_model.dart';
|
||||||
import 'package:fladder/models/items/chapters_model.dart';
|
import 'package:fladder/models/items/chapters_model.dart';
|
||||||
import 'package:fladder/models/items/episode_model.dart';
|
import 'package:fladder/models/items/episode_model.dart';
|
||||||
|
import 'package:fladder/models/items/item_shared_models.dart';
|
||||||
import 'package:fladder/models/items/media_segments_model.dart';
|
import 'package:fladder/models/items/media_segments_model.dart';
|
||||||
import 'package:fladder/models/items/media_streams_model.dart';
|
import 'package:fladder/models/items/media_streams_model.dart';
|
||||||
import 'package:fladder/models/items/season_model.dart';
|
import 'package:fladder/models/items/season_model.dart';
|
||||||
|
|
@ -84,6 +85,8 @@ class PlaybackModel {
|
||||||
|
|
||||||
Future<Duration>? startDuration() async => item.userData.playBackPosition;
|
Future<Duration>? startDuration() async => item.userData.playBackPosition;
|
||||||
|
|
||||||
|
PlaybackModel? updateUserData(UserData userData) => throw UnimplementedError();
|
||||||
|
|
||||||
Future<PlaybackModel>? setSubtitle(SubStreamModel? model, MediaControlsWrapper player) => throw UnimplementedError();
|
Future<PlaybackModel>? setSubtitle(SubStreamModel? model, MediaControlsWrapper player) => throw UnimplementedError();
|
||||||
Future<PlaybackModel>? setAudio(AudioStreamModel? model, MediaControlsWrapper player) => throw UnimplementedError();
|
Future<PlaybackModel>? setAudio(AudioStreamModel? model, MediaControlsWrapper player) => throw UnimplementedError();
|
||||||
Future<PlaybackModel>? setQualityOption(Map<Bitrate, bool> map) => throw UnimplementedError();
|
Future<PlaybackModel>? setQualityOption(Map<Bitrate, bool> map) => throw UnimplementedError();
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||||
import 'package:fladder/models/item_base_model.dart';
|
import 'package:fladder/models/item_base_model.dart';
|
||||||
import 'package:fladder/models/items/chapters_model.dart';
|
import 'package:fladder/models/items/chapters_model.dart';
|
||||||
|
import 'package:fladder/models/items/item_shared_models.dart';
|
||||||
import 'package:fladder/models/items/media_segments_model.dart';
|
import 'package:fladder/models/items/media_segments_model.dart';
|
||||||
import 'package:fladder/models/items/media_streams_model.dart';
|
import 'package:fladder/models/items/media_streams_model.dart';
|
||||||
import 'package:fladder/models/items/trick_play_model.dart';
|
import 'package:fladder/models/items/trick_play_model.dart';
|
||||||
|
|
@ -51,7 +52,7 @@ class TranscodePlaybackModel extends PlaybackModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<TranscodePlaybackModel>? setQualityOption(Map<Bitrate, bool> map) async => copyWith(bitRateOptions: map);
|
Future<TranscodePlaybackModel>? setQualityOption(Map<Bitrate, bool> map) async => copyWith(bitRateOptions: map);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<PlaybackModel?> playbackStarted(Duration position, Ref ref) async {
|
Future<PlaybackModel?> playbackStarted(Duration position, Ref ref) async {
|
||||||
|
|
@ -114,6 +115,15 @@ class TranscodePlaybackModel extends PlaybackModel {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
TranscodePlaybackModel? updateUserData(UserData userData) {
|
||||||
|
return copyWith(
|
||||||
|
item: item.copyWith(
|
||||||
|
userData: userData,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'TranscodePlaybackModel(item: $item, playbackInfo: $playbackInfo)';
|
String toString() => 'TranscodePlaybackModel(item: $item, playbackInfo: $playbackInfo)';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ class ClientSettingsModel with _$ClientSettingsModel {
|
||||||
@Default(DynamicSchemeVariant.rainbow) DynamicSchemeVariant schemeVariant,
|
@Default(DynamicSchemeVariant.rainbow) DynamicSchemeVariant schemeVariant,
|
||||||
@Default(true) bool backgroundPosters,
|
@Default(true) bool backgroundPosters,
|
||||||
@Default(true) bool checkForUpdates,
|
@Default(true) bool checkForUpdates,
|
||||||
|
@Default(false) bool usePosterForLibrary,
|
||||||
String? lastViewedUpdate,
|
String? lastViewedUpdate,
|
||||||
int? libraryPageSize,
|
int? libraryPageSize,
|
||||||
}) = _ClientSettingsModel;
|
}) = _ClientSettingsModel;
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ mixin _$ClientSettingsModel {
|
||||||
DynamicSchemeVariant get schemeVariant => throw _privateConstructorUsedError;
|
DynamicSchemeVariant get schemeVariant => throw _privateConstructorUsedError;
|
||||||
bool get backgroundPosters => throw _privateConstructorUsedError;
|
bool get backgroundPosters => throw _privateConstructorUsedError;
|
||||||
bool get checkForUpdates => throw _privateConstructorUsedError;
|
bool get checkForUpdates => throw _privateConstructorUsedError;
|
||||||
|
bool get usePosterForLibrary => throw _privateConstructorUsedError;
|
||||||
String? get lastViewedUpdate => throw _privateConstructorUsedError;
|
String? get lastViewedUpdate => throw _privateConstructorUsedError;
|
||||||
int? get libraryPageSize => throw _privateConstructorUsedError;
|
int? get libraryPageSize => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
|
@ -83,6 +84,7 @@ abstract class $ClientSettingsModelCopyWith<$Res> {
|
||||||
DynamicSchemeVariant schemeVariant,
|
DynamicSchemeVariant schemeVariant,
|
||||||
bool backgroundPosters,
|
bool backgroundPosters,
|
||||||
bool checkForUpdates,
|
bool checkForUpdates,
|
||||||
|
bool usePosterForLibrary,
|
||||||
String? lastViewedUpdate,
|
String? lastViewedUpdate,
|
||||||
int? libraryPageSize});
|
int? libraryPageSize});
|
||||||
}
|
}
|
||||||
|
|
@ -123,6 +125,7 @@ class _$ClientSettingsModelCopyWithImpl<$Res, $Val extends ClientSettingsModel>
|
||||||
Object? schemeVariant = null,
|
Object? schemeVariant = null,
|
||||||
Object? backgroundPosters = null,
|
Object? backgroundPosters = null,
|
||||||
Object? checkForUpdates = null,
|
Object? checkForUpdates = null,
|
||||||
|
Object? usePosterForLibrary = null,
|
||||||
Object? lastViewedUpdate = freezed,
|
Object? lastViewedUpdate = freezed,
|
||||||
Object? libraryPageSize = freezed,
|
Object? libraryPageSize = freezed,
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -211,6 +214,10 @@ class _$ClientSettingsModelCopyWithImpl<$Res, $Val extends ClientSettingsModel>
|
||||||
? _value.checkForUpdates
|
? _value.checkForUpdates
|
||||||
: checkForUpdates // ignore: cast_nullable_to_non_nullable
|
: checkForUpdates // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,
|
as bool,
|
||||||
|
usePosterForLibrary: null == usePosterForLibrary
|
||||||
|
? _value.usePosterForLibrary
|
||||||
|
: usePosterForLibrary // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
lastViewedUpdate: freezed == lastViewedUpdate
|
lastViewedUpdate: freezed == lastViewedUpdate
|
||||||
? _value.lastViewedUpdate
|
? _value.lastViewedUpdate
|
||||||
: lastViewedUpdate // ignore: cast_nullable_to_non_nullable
|
: lastViewedUpdate // ignore: cast_nullable_to_non_nullable
|
||||||
|
|
@ -253,6 +260,7 @@ abstract class _$$ClientSettingsModelImplCopyWith<$Res>
|
||||||
DynamicSchemeVariant schemeVariant,
|
DynamicSchemeVariant schemeVariant,
|
||||||
bool backgroundPosters,
|
bool backgroundPosters,
|
||||||
bool checkForUpdates,
|
bool checkForUpdates,
|
||||||
|
bool usePosterForLibrary,
|
||||||
String? lastViewedUpdate,
|
String? lastViewedUpdate,
|
||||||
int? libraryPageSize});
|
int? libraryPageSize});
|
||||||
}
|
}
|
||||||
|
|
@ -291,6 +299,7 @@ class __$$ClientSettingsModelImplCopyWithImpl<$Res>
|
||||||
Object? schemeVariant = null,
|
Object? schemeVariant = null,
|
||||||
Object? backgroundPosters = null,
|
Object? backgroundPosters = null,
|
||||||
Object? checkForUpdates = null,
|
Object? checkForUpdates = null,
|
||||||
|
Object? usePosterForLibrary = null,
|
||||||
Object? lastViewedUpdate = freezed,
|
Object? lastViewedUpdate = freezed,
|
||||||
Object? libraryPageSize = freezed,
|
Object? libraryPageSize = freezed,
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -379,6 +388,10 @@ class __$$ClientSettingsModelImplCopyWithImpl<$Res>
|
||||||
? _value.checkForUpdates
|
? _value.checkForUpdates
|
||||||
: checkForUpdates // ignore: cast_nullable_to_non_nullable
|
: checkForUpdates // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,
|
as bool,
|
||||||
|
usePosterForLibrary: null == usePosterForLibrary
|
||||||
|
? _value.usePosterForLibrary
|
||||||
|
: usePosterForLibrary // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
lastViewedUpdate: freezed == lastViewedUpdate
|
lastViewedUpdate: freezed == lastViewedUpdate
|
||||||
? _value.lastViewedUpdate
|
? _value.lastViewedUpdate
|
||||||
: lastViewedUpdate // ignore: cast_nullable_to_non_nullable
|
: lastViewedUpdate // ignore: cast_nullable_to_non_nullable
|
||||||
|
|
@ -417,6 +430,7 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
|
||||||
this.schemeVariant = DynamicSchemeVariant.rainbow,
|
this.schemeVariant = DynamicSchemeVariant.rainbow,
|
||||||
this.backgroundPosters = true,
|
this.backgroundPosters = true,
|
||||||
this.checkForUpdates = true,
|
this.checkForUpdates = true,
|
||||||
|
this.usePosterForLibrary = false,
|
||||||
this.lastViewedUpdate,
|
this.lastViewedUpdate,
|
||||||
this.libraryPageSize})
|
this.libraryPageSize})
|
||||||
: super._();
|
: super._();
|
||||||
|
|
@ -485,13 +499,16 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
|
||||||
@JsonKey()
|
@JsonKey()
|
||||||
final bool checkForUpdates;
|
final bool checkForUpdates;
|
||||||
@override
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final bool usePosterForLibrary;
|
||||||
|
@override
|
||||||
final String? lastViewedUpdate;
|
final String? lastViewedUpdate;
|
||||||
@override
|
@override
|
||||||
final int? libraryPageSize;
|
final int? libraryPageSize;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
||||||
return 'ClientSettingsModel(syncPath: $syncPath, position: $position, size: $size, timeOut: $timeOut, nextUpDateCutoff: $nextUpDateCutoff, themeMode: $themeMode, themeColor: $themeColor, amoledBlack: $amoledBlack, blurPlaceHolders: $blurPlaceHolders, blurUpcomingEpisodes: $blurUpcomingEpisodes, selectedLocale: $selectedLocale, enableMediaKeys: $enableMediaKeys, posterSize: $posterSize, pinchPosterZoom: $pinchPosterZoom, mouseDragSupport: $mouseDragSupport, requireWifi: $requireWifi, showAllCollectionTypes: $showAllCollectionTypes, maxConcurrentDownloads: $maxConcurrentDownloads, schemeVariant: $schemeVariant, backgroundPosters: $backgroundPosters, checkForUpdates: $checkForUpdates, lastViewedUpdate: $lastViewedUpdate, libraryPageSize: $libraryPageSize)';
|
return 'ClientSettingsModel(syncPath: $syncPath, position: $position, size: $size, timeOut: $timeOut, nextUpDateCutoff: $nextUpDateCutoff, themeMode: $themeMode, themeColor: $themeColor, amoledBlack: $amoledBlack, blurPlaceHolders: $blurPlaceHolders, blurUpcomingEpisodes: $blurUpcomingEpisodes, selectedLocale: $selectedLocale, enableMediaKeys: $enableMediaKeys, posterSize: $posterSize, pinchPosterZoom: $pinchPosterZoom, mouseDragSupport: $mouseDragSupport, requireWifi: $requireWifi, showAllCollectionTypes: $showAllCollectionTypes, maxConcurrentDownloads: $maxConcurrentDownloads, schemeVariant: $schemeVariant, backgroundPosters: $backgroundPosters, checkForUpdates: $checkForUpdates, usePosterForLibrary: $usePosterForLibrary, lastViewedUpdate: $lastViewedUpdate, libraryPageSize: $libraryPageSize)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -522,6 +539,7 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
|
||||||
..add(DiagnosticsProperty('schemeVariant', schemeVariant))
|
..add(DiagnosticsProperty('schemeVariant', schemeVariant))
|
||||||
..add(DiagnosticsProperty('backgroundPosters', backgroundPosters))
|
..add(DiagnosticsProperty('backgroundPosters', backgroundPosters))
|
||||||
..add(DiagnosticsProperty('checkForUpdates', checkForUpdates))
|
..add(DiagnosticsProperty('checkForUpdates', checkForUpdates))
|
||||||
|
..add(DiagnosticsProperty('usePosterForLibrary', usePosterForLibrary))
|
||||||
..add(DiagnosticsProperty('lastViewedUpdate', lastViewedUpdate))
|
..add(DiagnosticsProperty('lastViewedUpdate', lastViewedUpdate))
|
||||||
..add(DiagnosticsProperty('libraryPageSize', libraryPageSize));
|
..add(DiagnosticsProperty('libraryPageSize', libraryPageSize));
|
||||||
}
|
}
|
||||||
|
|
@ -566,6 +584,7 @@ abstract class _ClientSettingsModel extends ClientSettingsModel {
|
||||||
final DynamicSchemeVariant schemeVariant,
|
final DynamicSchemeVariant schemeVariant,
|
||||||
final bool backgroundPosters,
|
final bool backgroundPosters,
|
||||||
final bool checkForUpdates,
|
final bool checkForUpdates,
|
||||||
|
final bool usePosterForLibrary,
|
||||||
final String? lastViewedUpdate,
|
final String? lastViewedUpdate,
|
||||||
final int? libraryPageSize}) = _$ClientSettingsModelImpl;
|
final int? libraryPageSize}) = _$ClientSettingsModelImpl;
|
||||||
_ClientSettingsModel._() : super._();
|
_ClientSettingsModel._() : super._();
|
||||||
|
|
@ -617,6 +636,8 @@ abstract class _ClientSettingsModel extends ClientSettingsModel {
|
||||||
@override
|
@override
|
||||||
bool get checkForUpdates;
|
bool get checkForUpdates;
|
||||||
@override
|
@override
|
||||||
|
bool get usePosterForLibrary;
|
||||||
|
@override
|
||||||
String? get lastViewedUpdate;
|
String? get lastViewedUpdate;
|
||||||
@override
|
@override
|
||||||
int? get libraryPageSize;
|
int? get libraryPageSize;
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ _$ClientSettingsModelImpl _$$ClientSettingsModelImplFromJson(
|
||||||
DynamicSchemeVariant.rainbow,
|
DynamicSchemeVariant.rainbow,
|
||||||
backgroundPosters: json['backgroundPosters'] as bool? ?? true,
|
backgroundPosters: json['backgroundPosters'] as bool? ?? true,
|
||||||
checkForUpdates: json['checkForUpdates'] as bool? ?? true,
|
checkForUpdates: json['checkForUpdates'] as bool? ?? true,
|
||||||
|
usePosterForLibrary: json['usePosterForLibrary'] as bool? ?? false,
|
||||||
lastViewedUpdate: json['lastViewedUpdate'] as String?,
|
lastViewedUpdate: json['lastViewedUpdate'] as String?,
|
||||||
libraryPageSize: (json['libraryPageSize'] as num?)?.toInt(),
|
libraryPageSize: (json['libraryPageSize'] as num?)?.toInt(),
|
||||||
);
|
);
|
||||||
|
|
@ -71,6 +72,7 @@ Map<String, dynamic> _$$ClientSettingsModelImplToJson(
|
||||||
'schemeVariant': _$DynamicSchemeVariantEnumMap[instance.schemeVariant]!,
|
'schemeVariant': _$DynamicSchemeVariantEnumMap[instance.schemeVariant]!,
|
||||||
'backgroundPosters': instance.backgroundPosters,
|
'backgroundPosters': instance.backgroundPosters,
|
||||||
'checkForUpdates': instance.checkForUpdates,
|
'checkForUpdates': instance.checkForUpdates,
|
||||||
|
'usePosterForLibrary': instance.usePosterForLibrary,
|
||||||
'lastViewedUpdate': instance.lastViewedUpdate,
|
'lastViewedUpdate': instance.lastViewedUpdate,
|
||||||
'libraryPageSize': instance.libraryPageSize,
|
'libraryPageSize': instance.libraryPageSize,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,7 @@ class ViewModel {
|
||||||
FutureOr Function() action, {
|
FutureOr Function() action, {
|
||||||
FutureOr Function()? onLongPress,
|
FutureOr Function()? onLongPress,
|
||||||
List<ItemAction>? trailing,
|
List<ItemAction>? trailing,
|
||||||
|
Widget? customIcon,
|
||||||
}) {
|
}) {
|
||||||
return NavigationButton(
|
return NavigationButton(
|
||||||
label: name,
|
label: name,
|
||||||
|
|
@ -121,6 +122,7 @@ class ViewModel {
|
||||||
onLongPress: onLongPress,
|
onLongPress: onLongPress,
|
||||||
horizontal: horizontal,
|
horizontal: horizontal,
|
||||||
expanded: expanded,
|
expanded: expanded,
|
||||||
|
customIcon: customIcon,
|
||||||
trailing: trailing ?? [],
|
trailing: trailing ?? [],
|
||||||
selectedIcon: Icon(collectionType.icon),
|
selectedIcon: Icon(collectionType.icon),
|
||||||
icon: Icon(collectionType.iconOutlined),
|
icon: Icon(collectionType.iconOutlined),
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,10 @@ class AuthGuard extends AutoRouteGuard {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onNavigation(NavigationResolver resolver, StackRouter router) async {
|
Future<void> onNavigation(NavigationResolver resolver, StackRouter router) async {
|
||||||
|
if (resolver.route == router.current.route) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (ref.read(userProvider) != null ||
|
if (ref.read(userProvider) != null ||
|
||||||
resolver.routeName == const LoginRoute().routeName ||
|
resolver.routeName == const LoginRoute().routeName ||
|
||||||
resolver.routeName == SplashRoute().routeName) {
|
resolver.routeName == SplashRoute().routeName) {
|
||||||
|
|
|
||||||
|
|
@ -107,15 +107,12 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||||
),
|
),
|
||||||
if (homeBanner && homeCarouselItems.isNotEmpty) ...{
|
if (homeBanner && homeCarouselItems.isNotEmpty) ...{
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Transform.translate(
|
child: Padding(
|
||||||
offset: Offset(0, AdaptiveLayout.layoutOf(context) == ViewSize.phone ? -14 : 0),
|
padding: AdaptiveLayout.adaptivePadding(
|
||||||
child: Padding(
|
context,
|
||||||
padding: AdaptiveLayout.adaptivePadding(
|
horizontalPadding: 0,
|
||||||
context,
|
|
||||||
horizontalPadding: 0,
|
|
||||||
),
|
|
||||||
child: HomeBannerWidget(posters: homeCarouselItems),
|
|
||||||
),
|
),
|
||||||
|
child: HomeBannerWidget(posters: homeCarouselItems),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,6 @@ class FavouritesScreen extends ConsumerWidget {
|
||||||
slivers: [
|
slivers: [
|
||||||
if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone)
|
if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone)
|
||||||
NestedSliverAppBar(
|
NestedSliverAppBar(
|
||||||
searchTitle: "${context.localized.search} ${context.localized.favorites.toLowerCase()}...",
|
|
||||||
parent: context,
|
parent: context,
|
||||||
route: LibrarySearchRoute(favourites: true),
|
route: LibrarySearchRoute(favourites: true),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,13 @@ class HomeScreen extends ConsumerWidget {
|
||||||
selectedIcon: Icon(e.selectedIcon),
|
selectedIcon: Icon(e.selectedIcon),
|
||||||
route: const LibraryRoute(),
|
route: const LibraryRoute(),
|
||||||
action: () => e.navigate(context),
|
action: () => e.navigate(context),
|
||||||
|
floatingActionButton: AdaptiveFab(
|
||||||
|
context: context,
|
||||||
|
title: context.localized.search,
|
||||||
|
key: Key(e.name.capitalize()),
|
||||||
|
onPressed: () => context.router.navigate(LibrarySearchRoute()),
|
||||||
|
child: const Icon(IconsaxPlusLinear.search_status),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,18 @@ List<Widget> buildClientSettingsVisual(
|
||||||
ref.read(clientSettingsProvider.notifier).update((cb) => cb.copyWith(backgroundPosters: value)),
|
ref.read(clientSettingsProvider.notifier).update((cb) => cb.copyWith(backgroundPosters: value)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
SettingsListTile(
|
||||||
|
label: Text(context.localized.usePostersForLibraryIconsTitle),
|
||||||
|
subLabel: Text(context.localized.usePostersForLibraryIconsDesc),
|
||||||
|
onTap: () => ref
|
||||||
|
.read(clientSettingsProvider.notifier)
|
||||||
|
.update((cb) => cb.copyWith(usePosterForLibrary: !clientSettings.usePosterForLibrary)),
|
||||||
|
trailing: Switch(
|
||||||
|
value: clientSettings.usePosterForLibrary,
|
||||||
|
onChanged: (value) =>
|
||||||
|
ref.read(clientSettingsProvider.notifier).update((cb) => cb.copyWith(usePosterForLibrary: value)),
|
||||||
|
),
|
||||||
|
),
|
||||||
SettingsListTile(
|
SettingsListTile(
|
||||||
label: Text(context.localized.settingsNextUpCutoffDays),
|
label: Text(context.localized.settingsNextUpCutoffDays),
|
||||||
trailing: SizedBox(
|
trailing: SizedBox(
|
||||||
|
|
|
||||||
|
|
@ -249,6 +249,21 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
|
||||||
onChanged: (value) => provider.setUseLibass(value),
|
onChanged: (value) => provider.setUseLibass(value),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (!videoSettings.useLibass)
|
||||||
|
SettingsListTile(
|
||||||
|
label: Text(context.localized.settingsPlayerCustomSubtitlesTitle),
|
||||||
|
subLabel: Text(context.localized.settingsPlayerCustomSubtitlesDesc),
|
||||||
|
onTap: videoSettings.useLibass
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: true,
|
||||||
|
useSafeArea: false,
|
||||||
|
builder: (context) => const SubtitleEditor(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
AnimatedFadeSize(
|
AnimatedFadeSize(
|
||||||
child: videoSettings.useLibass && videoSettings.hardwareAccel && Platform.isAndroid
|
child: videoSettings.useLibass && videoSettings.hardwareAccel && Platform.isAndroid
|
||||||
? SettingsMessageBox(
|
? SettingsMessageBox(
|
||||||
|
|
@ -272,20 +287,6 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
|
||||||
},
|
},
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
SettingsListTile(
|
|
||||||
label: Text(context.localized.settingsPlayerCustomSubtitlesTitle),
|
|
||||||
subLabel: Text(context.localized.settingsPlayerCustomSubtitlesDesc),
|
|
||||||
onTap: videoSettings.useLibass
|
|
||||||
? null
|
|
||||||
: () {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
barrierDismissible: false,
|
|
||||||
useSafeArea: false,
|
|
||||||
builder: (context) => const SubtitleEditor(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
_ => SettingsMessageBox(
|
_ => SettingsMessageBox(
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,7 @@ import 'package:fladder/models/settings/subtitle_settings_model.dart';
|
||||||
import 'package:fladder/providers/settings/subtitle_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/settings/video_player_settings_provider.dart';
|
||||||
import 'package:fladder/screens/video_player/components/video_subtitle_controls.dart';
|
import 'package:fladder/screens/video_player/components/video_subtitle_controls.dart';
|
||||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
|
||||||
import 'package:fladder/util/localization_helper.dart';
|
import 'package:fladder/util/localization_helper.dart';
|
||||||
import 'package:fladder/widgets/navigation_scaffold/components/fladder_app_bar.dart';
|
|
||||||
|
|
||||||
class SubtitleEditor extends ConsumerStatefulWidget {
|
class SubtitleEditor extends ConsumerStatefulWidget {
|
||||||
const SubtitleEditor({super.key});
|
const SubtitleEditor({super.key});
|
||||||
|
|
@ -27,8 +25,9 @@ class _SubtitleEditorState extends ConsumerState<SubtitleEditor> {
|
||||||
final fakeText = context.localized.subtitleConfiguratorPlaceHolder;
|
final fakeText = context.localized.subtitleConfiguratorPlaceHolder;
|
||||||
double lastScale = 0.0;
|
double lastScale = 0.0;
|
||||||
|
|
||||||
return Scaffold(
|
return Padding(
|
||||||
body: Dialog.fullscreen(
|
padding: EdgeInsets.only(top: MediaQuery.paddingOf(context).top),
|
||||||
|
child: Dialog.fullscreen(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onScaleUpdate: (details) {
|
onScaleUpdate: (details) {
|
||||||
lastScale = details.scale;
|
lastScale = details.scale;
|
||||||
|
|
@ -73,7 +72,6 @@ class _SubtitleEditorState extends ConsumerState<SubtitleEditor> {
|
||||||
padding: MediaQuery.paddingOf(context),
|
padding: MediaQuery.paddingOf(context),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
if (AdaptiveLayout.of(context).isDesktop) const FladderAppBar(),
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const BackButton(),
|
const BackButton(),
|
||||||
|
|
|
||||||
|
|
@ -38,14 +38,21 @@ class _DefaultTitleBarState extends ConsumerState<DefaultTitleBar> with WindowLi
|
||||||
if (ref.watch(argumentsStateProvider.select((value) => value.htpcMode))) return const SizedBox.shrink();
|
if (ref.watch(argumentsStateProvider.select((value) => value.htpcMode))) return const SizedBox.shrink();
|
||||||
final brightness = widget.brightness ?? Theme.of(context).brightness;
|
final brightness = widget.brightness ?? Theme.of(context).brightness;
|
||||||
final iconColor = Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.65);
|
final iconColor = Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.65);
|
||||||
|
final surfaceColor = Theme.of(context).colorScheme.surface;
|
||||||
return MouseRegion(
|
return MouseRegion(
|
||||||
onEnter: (event) => setState(() => hovering = true),
|
onEnter: (event) => setState(() => hovering = true),
|
||||||
onExit: (event) => setState(() => hovering = false),
|
onExit: (event) => setState(() => hovering = false),
|
||||||
child: AnimatedContainer(
|
child: AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 250),
|
duration: const Duration(milliseconds: 250),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: hovering ? Colors.black.withValues(alpha: 0.15) : Colors.transparent,
|
gradient: LinearGradient(
|
||||||
),
|
colors: [
|
||||||
|
surfaceColor.withValues(alpha: 0.7),
|
||||||
|
surfaceColor.withValues(alpha: hovering ? 0.7 : 0),
|
||||||
|
],
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
)),
|
||||||
height: widget.height,
|
height: widget.height,
|
||||||
child: kIsWeb
|
child: kIsWeb
|
||||||
? const SizedBox.shrink()
|
? const SizedBox.shrink()
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,10 @@ class ItemLogo extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final logo = item.getPosters?.logo;
|
final logo = item.getPosters?.logo;
|
||||||
|
final size = MediaQuery.sizeOf(context);
|
||||||
|
final maxHeight = size.height * 0.45;
|
||||||
final textWidget = Container(
|
final textWidget = Container(
|
||||||
height: 512,
|
height: maxHeight,
|
||||||
alignment: imageAlignment,
|
alignment: imageAlignment,
|
||||||
child: Text(
|
child: Text(
|
||||||
item.parentBaseModel.name,
|
item.parentBaseModel.name,
|
||||||
|
|
@ -33,13 +35,14 @@ class ItemLogo extends StatelessWidget {
|
||||||
);
|
);
|
||||||
return logo != null
|
return logo != null
|
||||||
? ConstrainedBox(
|
? ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 500, maxHeight: 500),
|
constraints: BoxConstraints(maxWidth: size.width * 0.35, maxHeight: maxHeight),
|
||||||
child: FladderImage(
|
child: FladderImage(
|
||||||
image: logo,
|
image: logo,
|
||||||
disableBlur: true,
|
disableBlur: true,
|
||||||
alignment: imageAlignment,
|
stackFit: StackFit.passthrough,
|
||||||
|
alignment: Alignment.bottomLeft,
|
||||||
imageErrorBuilder: (context, object, stack) => textWidget,
|
imageErrorBuilder: (context, object, stack) => textWidget,
|
||||||
placeHolder: const SizedBox(height: 0),
|
placeHolder: textWidget,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,8 @@ class NestedScaffold extends ConsumerWidget {
|
||||||
begin: Alignment.topCenter,
|
begin: Alignment.topCenter,
|
||||||
end: Alignment.bottomCenter,
|
end: Alignment.bottomCenter,
|
||||||
colors: [
|
colors: [
|
||||||
Theme.of(context).colorScheme.surface.withValues(alpha: 0.98),
|
Theme.of(context).colorScheme.surface.withValues(alpha: 0.85),
|
||||||
Theme.of(context).colorScheme.surface.withValues(alpha: 0.8),
|
Theme.of(context).colorScheme.surface.withValues(alpha: 0.7),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -4,77 +4,62 @@ import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||||
|
|
||||||
import 'package:fladder/util/localization_helper.dart';
|
|
||||||
import 'package:fladder/widgets/navigation_scaffold/components/settings_user_icon.dart';
|
import 'package:fladder/widgets/navigation_scaffold/components/settings_user_icon.dart';
|
||||||
import 'package:fladder/widgets/shared/shapes.dart';
|
|
||||||
|
|
||||||
class NestedSliverAppBar extends ConsumerWidget {
|
class NestedSliverAppBar extends ConsumerWidget {
|
||||||
final BuildContext parent;
|
final BuildContext parent;
|
||||||
final String? searchTitle;
|
|
||||||
final PageRouteInfo? route;
|
final PageRouteInfo? route;
|
||||||
const NestedSliverAppBar({required this.parent, this.route, this.searchTitle, super.key});
|
const NestedSliverAppBar({required this.parent, this.route, super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final surfaceColor = Theme.of(context).colorScheme.surface;
|
||||||
|
final buttonStyle = Theme.of(context).filledButtonTheme.style?.copyWith(
|
||||||
|
backgroundColor: WidgetStatePropertyAll(
|
||||||
|
surfaceColor.withValues(alpha: 0.8),
|
||||||
|
),
|
||||||
|
);
|
||||||
return SliverAppBar(
|
return SliverAppBar(
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
elevation: 16,
|
elevation: 0,
|
||||||
forceElevated: true,
|
forceElevated: false,
|
||||||
surfaceTintColor: Colors.transparent,
|
surfaceTintColor: Colors.transparent,
|
||||||
shadowColor: Colors.transparent,
|
shadowColor: Colors.transparent,
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Colors.transparent,
|
||||||
shape: AppBarShape(),
|
flexibleSpace: Container(
|
||||||
title: SizedBox(
|
decoration: BoxDecoration(
|
||||||
height: 65,
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
surfaceColor.withValues(alpha: 0.7),
|
||||||
|
surfaceColor.withValues(alpha: 0),
|
||||||
|
],
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
)),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 24),
|
padding: MediaQuery.paddingOf(context).copyWith(bottom: 0),
|
||||||
child: Row(
|
child: Padding(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
padding: const EdgeInsets.all(14),
|
||||||
spacing: 10,
|
child: SizedBox(
|
||||||
children: [
|
height: 50,
|
||||||
SizedBox(
|
child: Row(
|
||||||
width: 30,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
child: IconButton(
|
spacing: 12,
|
||||||
onPressed: () => Scaffold.of(parent).openDrawer(),
|
children: [
|
||||||
icon: const Icon(IconsaxPlusLinear.menu),
|
AspectRatio(
|
||||||
padding: EdgeInsets.zero,
|
aspectRatio: 1.0,
|
||||||
),
|
child: IconButton.filledTonal(
|
||||||
),
|
style: buttonStyle,
|
||||||
Expanded(
|
onPressed: () => Scaffold.of(parent).openDrawer(),
|
||||||
child: Hero(
|
icon: const Icon(IconsaxPlusLinear.menu),
|
||||||
tag: "PrimarySearch",
|
padding: EdgeInsets.zero,
|
||||||
child: Card(
|
|
||||||
elevation: 3,
|
|
||||||
shadowColor: Colors.transparent,
|
|
||||||
child: InkWell(
|
|
||||||
onTap: route != null
|
|
||||||
? () {
|
|
||||||
route?.push(context);
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(10),
|
|
||||||
child: Opacity(
|
|
||||||
opacity: 0.65,
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const Icon(IconsaxPlusLinear.search_normal),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Transform.translate(
|
|
||||||
offset: const Offset(0, 1.5),
|
|
||||||
child: Text(searchTitle ?? "${context.localized.search}..."),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const Spacer(),
|
||||||
|
const SettingsUserIcon()
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const SettingsUserIcon()
|
),
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -92,8 +92,7 @@ class _SyncItemDetailsState extends ConsumerState<SyncItemDetails> {
|
||||||
children: [
|
children: [
|
||||||
if (baseItem != null) ...{
|
if (baseItem != null) ...{
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.max,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
spacing: 16,
|
spacing: 16,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
|
|
@ -193,7 +192,10 @@ class _SyncItemDetailsState extends ConsumerState<SyncItemDetails> {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
if (children?.isNotEmpty == true) ...[
|
if (children?.isNotEmpty == true) ...[
|
||||||
const Divider(),
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 16),
|
||||||
|
child: Divider(),
|
||||||
|
),
|
||||||
...children!.map(
|
...children!.map(
|
||||||
(e) => ChildSyncWidget(syncedChild: e),
|
(e) => ChildSyncWidget(syncedChild: e),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,6 @@ class _SyncedScreenState extends ConsumerState<SyncedScreen> {
|
||||||
slivers: [
|
slivers: [
|
||||||
if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone)
|
if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone)
|
||||||
NestedSliverAppBar(
|
NestedSliverAppBar(
|
||||||
searchTitle: "${context.localized.search} ...",
|
|
||||||
parent: context,
|
parent: context,
|
||||||
route: LibrarySearchRoute(),
|
route: LibrarySearchRoute(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:background_downloader/background_downloader.dart';
|
import 'package:background_downloader/background_downloader.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||||
|
|
@ -51,6 +52,19 @@ class SyncOptionsButton extends ConsumerWidget {
|
||||||
final enqueuedTasks = syncTasks.where((element) => element.status == TaskStatus.enqueued).toList();
|
final enqueuedTasks = syncTasks.where((element) => element.status == TaskStatus.enqueued).toList();
|
||||||
final pausedTasks = syncTasks.where((element) => element.status == TaskStatus.paused).toList();
|
final pausedTasks = syncTasks.where((element) => element.status == TaskStatus.paused).toList();
|
||||||
return <PopupMenuEntry>[
|
return <PopupMenuEntry>[
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
spacing: 12,
|
||||||
|
children: [
|
||||||
|
const Icon(IconsaxPlusLinear.arrow_right),
|
||||||
|
Text(context.localized.showDetails),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
syncedItem.itemModel?.navigateTo(context);
|
||||||
|
context.maybePop();
|
||||||
|
},
|
||||||
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
child: Row(
|
child: Row(
|
||||||
spacing: 12,
|
spacing: 12,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||||
|
|
||||||
import 'package:fladder/providers/settings/subtitle_settings_provider.dart';
|
import 'package:fladder/providers/settings/subtitle_settings_provider.dart';
|
||||||
import 'package:fladder/screens/shared/flat_button.dart';
|
import 'package:fladder/screens/shared/flat_button.dart';
|
||||||
|
|
@ -60,7 +62,8 @@ class _VideoSubtitleControlsState extends ConsumerState<VideoSubtitleControls> {
|
||||||
duration: const Duration(milliseconds: 250),
|
duration: const Duration(milliseconds: 250),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
color: controlsHidden ? DialogTheme.of(context).backgroundColor?.withValues(alpha: 0.75) : Colors.transparent,
|
color:
|
||||||
|
controlsHidden ? Theme.of(context).colorScheme.surfaceContainer.withValues(alpha: 0.8) : Colors.transparent,
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
|
|
@ -68,10 +71,19 @@ class _VideoSubtitleControlsState extends ConsumerState<VideoSubtitleControls> {
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (widget.label?.isNotEmpty == true)
|
if (widget.label?.isNotEmpty == true)
|
||||||
Text(
|
Row(
|
||||||
widget.label!,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
style: Theme.of(context).textTheme.headlineMedium,
|
children: [
|
||||||
),
|
Text(
|
||||||
|
widget.label!,
|
||||||
|
style: Theme.of(context).textTheme.headlineMedium,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => context.maybePop(),
|
||||||
|
icon: const Icon(IconsaxPlusBold.close_circle),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
).addVisiblity(activeKey == null ? controlsHidden : activeKey == const Key('title')),
|
||||||
IconButton.filledTonal(
|
IconButton.filledTonal(
|
||||||
isSelected: !hideControls,
|
isSelected: !hideControls,
|
||||||
onPressed: () => setState(() => hideControls = !hideControls),
|
onPressed: () => setState(() => hideControls = !hideControls),
|
||||||
|
|
|
||||||
|
|
@ -250,8 +250,9 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
spacing: 16,
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => minimizePlayer(context),
|
onPressed: () => minimizePlayer(context),
|
||||||
|
|
@ -260,26 +261,26 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
|
||||||
size: 24,
|
size: 24,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
|
||||||
if (currentItem != null)
|
if (currentItem != null)
|
||||||
Expanded(
|
ConstrainedBox(
|
||||||
child: ConstrainedBox(
|
constraints: BoxConstraints(
|
||||||
constraints: BoxConstraints(
|
maxHeight: 150.clamp(50, MediaQuery.sizeOf(context).height * 0.25).toDouble(),
|
||||||
maxHeight: 150.clamp(50, MediaQuery.sizeOf(context).height * 0.25).toDouble(),
|
),
|
||||||
),
|
child: ItemLogo(
|
||||||
child: ItemLogo(
|
item: currentItem,
|
||||||
item: currentItem,
|
imageAlignment: Alignment.topLeft,
|
||||||
imageAlignment: Alignment.topLeft,
|
textStyle: Theme.of(context).textTheme.headlineLarge,
|
||||||
textStyle: Theme.of(context).textTheme.headlineLarge,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const Spacer(),
|
||||||
if (AdaptiveLayout.of(context).inputDevice == InputDevice.touch)
|
if (AdaptiveLayout.of(context).inputDevice == InputDevice.touch)
|
||||||
Tooltip(
|
Align(
|
||||||
message: context.localized.stop,
|
alignment: Alignment.centerRight,
|
||||||
child: IconButton(
|
child: Tooltip(
|
||||||
onPressed: () => closePlayer(), icon: const Icon(IconsaxPlusLinear.close_square))),
|
message: context.localized.stop,
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: () => closePlayer(), icon: const Icon(IconsaxPlusLinear.close_square))),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ class FladderImage extends ConsumerWidget {
|
||||||
final Widget Function(BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded)? frameBuilder;
|
final Widget Function(BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded)? frameBuilder;
|
||||||
final Widget Function(BuildContext context, Object object, StackTrace? stack)? imageErrorBuilder;
|
final Widget Function(BuildContext context, Object object, StackTrace? stack)? imageErrorBuilder;
|
||||||
final Widget? placeHolder;
|
final Widget? placeHolder;
|
||||||
|
final StackFit stackFit;
|
||||||
final BoxFit fit;
|
final BoxFit fit;
|
||||||
final BoxFit? blurFit;
|
final BoxFit? blurFit;
|
||||||
final AlignmentGeometry? alignment;
|
final AlignmentGeometry? alignment;
|
||||||
|
|
@ -22,6 +23,7 @@ class FladderImage extends ConsumerWidget {
|
||||||
this.frameBuilder,
|
this.frameBuilder,
|
||||||
this.imageErrorBuilder,
|
this.imageErrorBuilder,
|
||||||
this.placeHolder,
|
this.placeHolder,
|
||||||
|
this.stackFit = StackFit.expand,
|
||||||
this.fit = BoxFit.cover,
|
this.fit = BoxFit.cover,
|
||||||
this.blurFit,
|
this.blurFit,
|
||||||
this.alignment,
|
this.alignment,
|
||||||
|
|
@ -39,7 +41,7 @@ class FladderImage extends ConsumerWidget {
|
||||||
} else {
|
} else {
|
||||||
return Stack(
|
return Stack(
|
||||||
key: Key(newImage.key),
|
key: Key(newImage.key),
|
||||||
fit: StackFit.expand,
|
fit: stackFit,
|
||||||
children: [
|
children: [
|
||||||
if (!disableBlur && useBluredPlaceHolder && newImage.hash.isNotEmpty || blurOnly)
|
if (!disableBlur && useBluredPlaceHolder && newImage.hash.isNotEmpty || blurOnly)
|
||||||
BlurHash(
|
BlurHash(
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,6 @@ extension WidgetExtensions on Widget {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget addVisiblity(bool visible) {
|
Widget addVisiblity(bool visible) {
|
||||||
return AnimatedOpacity(duration: const Duration(milliseconds: 250), opacity: visible ? 1 : 0, child: this);
|
return AnimatedOpacity(duration: const Duration(milliseconds: 250), opacity: visible ? 1 : 0.10, child: this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,13 +81,14 @@ class DestinationModel {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
NavigationButton toNavigationButton(bool selected, bool horizontal, bool expanded) {
|
NavigationButton toNavigationButton(bool selected, bool horizontal, bool expanded, {Widget? customIcon}) {
|
||||||
return NavigationButton(
|
return NavigationButton(
|
||||||
label: label,
|
label: label,
|
||||||
selected: selected,
|
selected: selected,
|
||||||
onPressed: action,
|
onPressed: action,
|
||||||
horizontal: horizontal,
|
horizontal: horizontal,
|
||||||
expanded: expanded,
|
expanded: expanded,
|
||||||
|
customIcon: customIcon,
|
||||||
selectedIcon: selectedIcon!,
|
selectedIcon: selectedIcon!,
|
||||||
icon: icon!,
|
icon: icon!,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import 'package:fladder/screens/shared/animated_fade_size.dart';
|
import 'package:fladder/screens/shared/animated_fade_size.dart';
|
||||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||||
import 'package:fladder/widgets/shared/item_actions.dart';
|
import 'package:fladder/widgets/shared/item_actions.dart';
|
||||||
import 'package:fladder/widgets/shared/modal_bottom_sheet.dart';
|
import 'package:fladder/widgets/shared/modal_bottom_sheet.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
|
|
||||||
class DrawerListButton extends ConsumerStatefulWidget {
|
class DrawerListButton extends ConsumerStatefulWidget {
|
||||||
final String label;
|
final String label;
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,11 @@ import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||||
|
import 'package:overflow_view/overflow_view.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
import 'package:fladder/models/media_playback_model.dart';
|
import 'package:fladder/models/media_playback_model.dart';
|
||||||
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
import 'package:fladder/providers/user_provider.dart';
|
||||||
import 'package:fladder/providers/video_player_provider.dart';
|
import 'package:fladder/providers/video_player_provider.dart';
|
||||||
import 'package:fladder/screens/shared/fladder_snackbar.dart';
|
import 'package:fladder/screens/shared/fladder_snackbar.dart';
|
||||||
import 'package:fladder/screens/shared/flat_button.dart';
|
import 'package:fladder/screens/shared/flat_button.dart';
|
||||||
|
|
@ -15,10 +16,16 @@ import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||||
import 'package:fladder/util/duration_extensions.dart';
|
import 'package:fladder/util/duration_extensions.dart';
|
||||||
import 'package:fladder/util/localization_helper.dart';
|
import 'package:fladder/util/localization_helper.dart';
|
||||||
import 'package:fladder/util/refresh_state.dart';
|
import 'package:fladder/util/refresh_state.dart';
|
||||||
|
import 'package:fladder/widgets/shared/fladder_slider.dart';
|
||||||
|
import 'package:fladder/widgets/shared/item_actions.dart';
|
||||||
|
|
||||||
const videoPlayerHeroTag = "HeroPlayer";
|
const videoPlayerHeroTag = "HeroPlayer";
|
||||||
|
|
||||||
const floatingPlayerHeight = 70.0;
|
double floatingPlayerHeight(BuildContext context) => switch (AdaptiveLayout.viewSizeOf(context)) {
|
||||||
|
ViewSize.phone => 75,
|
||||||
|
ViewSize.tablet => 85,
|
||||||
|
ViewSize.desktop => 95,
|
||||||
|
};
|
||||||
|
|
||||||
class FloatingPlayerBar extends ConsumerStatefulWidget {
|
class FloatingPlayerBar extends ConsumerStatefulWidget {
|
||||||
const FloatingPlayerBar({super.key});
|
const FloatingPlayerBar({super.key});
|
||||||
|
|
@ -29,6 +36,8 @@ class FloatingPlayerBar extends ConsumerStatefulWidget {
|
||||||
|
|
||||||
class _CurrentlyPlayingBarState extends ConsumerState<FloatingPlayerBar> {
|
class _CurrentlyPlayingBarState extends ConsumerState<FloatingPlayerBar> {
|
||||||
bool showExpandButton = false;
|
bool showExpandButton = false;
|
||||||
|
bool changingSliderValue = false;
|
||||||
|
Duration lastPosition = Duration.zero;
|
||||||
|
|
||||||
Future<void> openFullScreenPlayer() async {
|
Future<void> openFullScreenPlayer() async {
|
||||||
setState(() => showExpandButton = false);
|
setState(() => showExpandButton = false);
|
||||||
|
|
@ -58,155 +67,239 @@ class _CurrentlyPlayingBarState extends ConsumerState<FloatingPlayerBar> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final playbackInfo = ref.watch(mediaPlaybackProvider);
|
final playbackInfo = ref.watch(mediaPlaybackProvider);
|
||||||
final player = ref.watch(videoPlayerProvider);
|
final player = ref.watch(videoPlayerProvider);
|
||||||
final playbackModel = ref.watch(playBackModel.select((value) => value?.item));
|
final item = ref.watch(playBackModel.select((value) => value?.item));
|
||||||
final progress = playbackInfo.position.inMilliseconds / playbackInfo.duration.inMilliseconds;
|
if (!changingSliderValue) {
|
||||||
return Dismissible(
|
lastPosition = playbackInfo.position;
|
||||||
key: const Key("CurrentlyPlayingBar"),
|
}
|
||||||
confirmDismiss: (direction) async {
|
|
||||||
if (direction == DismissDirection.up) {
|
var isFavourite = item?.userData.isFavourite == true;
|
||||||
await openFullScreenPlayer();
|
|
||||||
} else {
|
final isDesktop = AdaptiveLayout.of(context).isDesktop;
|
||||||
await stopPlayer();
|
|
||||||
}
|
final itemActions = [
|
||||||
return false;
|
ItemActionButton(
|
||||||
},
|
label: Text(context.localized.audio),
|
||||||
direction: DismissDirection.vertical,
|
icon: Consumer(
|
||||||
child: InkWell(
|
builder: (context, ref, child) {
|
||||||
onLongPress: () => fladderSnackbar(context, title: "Swipe up/down to open/close the player"),
|
var volume = (player.lastState?.volume ?? 0) <= 0;
|
||||||
child: Card(
|
return Icon(
|
||||||
elevation: 5,
|
volume ? IconsaxPlusBold.volume_cross : IconsaxPlusBold.volume_high,
|
||||||
color: Theme.of(context).colorScheme.primaryContainer,
|
);
|
||||||
child: SizedBox(
|
},
|
||||||
height: floatingPlayerHeight,
|
),
|
||||||
child: LayoutBuilder(builder: (context, constraints) {
|
action: () {
|
||||||
return Row(
|
final volume = player.lastState?.volume == 0 ? 100.0 : 0.0;
|
||||||
children: [
|
player.setVolume(volume);
|
||||||
Flexible(
|
}),
|
||||||
child: Padding(
|
ItemActionButton(
|
||||||
padding: MediaQuery.paddingOf(context).copyWith(top: 0, bottom: 0),
|
label: Text(context.localized.stop),
|
||||||
child: Column(
|
action: () async => stopPlayer(),
|
||||||
mainAxisSize: MainAxisSize.min,
|
icon: const Icon(IconsaxPlusBold.stop),
|
||||||
children: [
|
),
|
||||||
Expanded(
|
ItemActionButton(
|
||||||
child: Padding(
|
label: Text(isFavourite ? context.localized.removeAsFavorite : context.localized.addAsFavorite),
|
||||||
padding: const EdgeInsets.all(6),
|
icon: Icon(
|
||||||
child: Row(
|
color: isFavourite ? Colors.red : null,
|
||||||
spacing: 7,
|
isFavourite ? IconsaxPlusBold.heart : IconsaxPlusLinear.heart,
|
||||||
children: [
|
),
|
||||||
if (playbackInfo.state == VideoPlayerState.minimized)
|
action: () async {
|
||||||
Card(
|
final result = (await ref.read(userProvider.notifier).setAsFavorite(
|
||||||
child: AspectRatio(
|
!isFavourite,
|
||||||
aspectRatio: 1.67,
|
item?.id ?? "",
|
||||||
child: MouseRegion(
|
))
|
||||||
onEnter: (event) => setState(() => showExpandButton = true),
|
?.body;
|
||||||
onExit: (event) => setState(() => showExpandButton = false),
|
|
||||||
child: Stack(
|
if (result != null) {
|
||||||
children: [
|
ref.read(playBackModel.notifier).update((state) => state?.updateUserData(result));
|
||||||
Hero(
|
}
|
||||||
tag: videoPlayerHeroTag,
|
},
|
||||||
child: player.videoWidget(
|
),
|
||||||
UniqueKey(),
|
];
|
||||||
BoxFit.fitHeight,
|
return Padding(
|
||||||
) ??
|
padding:
|
||||||
const SizedBox.shrink(),
|
MediaQuery.paddingOf(context).copyWith(top: 0, bottom: isDesktop ? 0 : MediaQuery.paddingOf(context).bottom),
|
||||||
),
|
child: Dismissible(
|
||||||
Positioned.fill(
|
key: const Key("CurrentlyPlayingBar"),
|
||||||
child: Tooltip(
|
confirmDismiss: (direction) async {
|
||||||
message: "Expand player",
|
if (direction == DismissDirection.up) {
|
||||||
waitDuration: const Duration(milliseconds: 500),
|
await openFullScreenPlayer();
|
||||||
child: AnimatedOpacity(
|
} else {
|
||||||
opacity: showExpandButton ? 1 : 0,
|
await stopPlayer();
|
||||||
duration: const Duration(milliseconds: 125),
|
}
|
||||||
child: Container(
|
return false;
|
||||||
color: Colors.black.withValues(alpha: 0.6),
|
},
|
||||||
child: FlatButton(
|
direction: DismissDirection.vertical,
|
||||||
onTap: () async => openFullScreenPlayer(),
|
child: InkWell(
|
||||||
child: const Icon(Icons.keyboard_arrow_up_rounded),
|
onLongPress: () => fladderSnackbar(context, title: "Swipe up/down to open/close the player"),
|
||||||
),
|
child: Card(
|
||||||
),
|
elevation: 5,
|
||||||
),
|
color: Theme.of(context).colorScheme.primaryContainer,
|
||||||
),
|
child: SizedBox(
|
||||||
)
|
height: floatingPlayerHeight(context),
|
||||||
],
|
child: LayoutBuilder(builder: (context, constraints) {
|
||||||
),
|
return Column(
|
||||||
),
|
mainAxisSize: MainAxisSize.min,
|
||||||
),
|
children: [
|
||||||
),
|
Expanded(
|
||||||
Expanded(
|
child: Padding(
|
||||||
child: Column(
|
padding: const EdgeInsets.all(6),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
spacing: 12,
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (playbackInfo.state == VideoPlayerState.minimized)
|
||||||
|
Card(
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 1.67,
|
||||||
|
child: MouseRegion(
|
||||||
|
onEnter: (event) => setState(() => showExpandButton = true),
|
||||||
|
onExit: (event) => setState(() => showExpandButton = false),
|
||||||
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Flexible(
|
Hero(
|
||||||
child: Text(
|
tag: videoPlayerHeroTag,
|
||||||
playbackModel?.title ?? "",
|
child: player.videoWidget(
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
UniqueKey(),
|
||||||
),
|
BoxFit.fitHeight,
|
||||||
|
) ??
|
||||||
|
const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
if (playbackModel?.detailedName(context)?.isNotEmpty == true)
|
Positioned.fill(
|
||||||
Flexible(
|
child: Tooltip(
|
||||||
child: Text(
|
message: "Expand player",
|
||||||
playbackModel?.detailedName(context) ?? "",
|
waitDuration: const Duration(milliseconds: 500),
|
||||||
overflow: TextOverflow.ellipsis,
|
child: AnimatedOpacity(
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
opacity: showExpandButton ? 1 : 0,
|
||||||
color:
|
duration: const Duration(milliseconds: 125),
|
||||||
Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.65),
|
child: Container(
|
||||||
),
|
color: Colors.black.withValues(alpha: 0.6),
|
||||||
|
child: FlatButton(
|
||||||
|
onTap: () async => openFullScreenPlayer(),
|
||||||
|
child: const Icon(Icons.keyboard_arrow_up_rounded),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (!progress.isNaN && constraints.maxWidth > 500)
|
),
|
||||||
Text(
|
),
|
||||||
"${playbackInfo.position.readAbleDuration} / ${playbackInfo.duration.readAbleDuration}"),
|
Expanded(
|
||||||
Padding(
|
child: InkWell(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
onTap: () => item?.navigateTo(context),
|
||||||
child: IconButton.filledTonal(
|
child: Column(
|
||||||
onPressed: () => ref.read(videoPlayerProvider).playOrPause(),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
icon: playbackInfo.playing
|
mainAxisSize: MainAxisSize.min,
|
||||||
? const Icon(Icons.pause_rounded)
|
children: [
|
||||||
: const Icon(Icons.play_arrow_rounded),
|
Flexible(
|
||||||
),
|
child: Text(
|
||||||
),
|
item?.title ?? "",
|
||||||
if (constraints.maxWidth > 500) ...[
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
IconButton(
|
maxLines: 1,
|
||||||
onPressed: () {
|
|
||||||
final volume = player.lastState?.volume == 0 ? 100.0 : 0.0;
|
|
||||||
player.setVolume(volume);
|
|
||||||
},
|
|
||||||
icon: Icon(
|
|
||||||
ref.watch(videoPlayerSettingsProvider.select((value) => value.volume)) <= 0
|
|
||||||
? IconsaxPlusBold.volume_cross
|
|
||||||
: IconsaxPlusBold.volume_high,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Tooltip(
|
if (item?.detailedName(context)?.isNotEmpty == true)
|
||||||
message: context.localized.stop,
|
Flexible(
|
||||||
waitDuration: const Duration(milliseconds: 500),
|
child: Text(
|
||||||
child: IconButton(
|
item?.detailedName(context) ?? "",
|
||||||
onPressed: () async => stopPlayer(),
|
overflow: TextOverflow.ellipsis,
|
||||||
icon: const Icon(IconsaxPlusBold.stop),
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.65),
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Expanded(
|
||||||
LinearProgressIndicator(
|
child: Row(
|
||||||
minHeight: 6,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
backgroundColor: Colors.black.withValues(alpha: 0.25),
|
children: [
|
||||||
color: Theme.of(context).colorScheme.primary,
|
if (constraints.maxWidth > 500)
|
||||||
value: progress.clamp(0, 1),
|
Flexible(
|
||||||
),
|
child: Text(
|
||||||
],
|
"${lastPosition.readAbleDuration} / ${playbackInfo.duration.readAbleDuration}"),
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
child: IconButton.filledTonal(
|
||||||
|
onPressed: () => ref.read(videoPlayerProvider).playOrPause(),
|
||||||
|
icon: playbackInfo.playing
|
||||||
|
? const Icon(Icons.pause_rounded)
|
||||||
|
: const Icon(Icons.play_arrow_rounded),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: OverflowView.flexible(
|
||||||
|
builder: (context, remainingItemCount) => PopupMenuButton(
|
||||||
|
iconColor: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.45),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
itemBuilder: (context) => itemActions
|
||||||
|
.sublist(itemActions.length - remainingItemCount)
|
||||||
|
.map(
|
||||||
|
(e) => e.toPopupMenuItem(useIcons: true),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
children: itemActions.map((e) => e.toButton()).toList(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer
|
||||||
],
|
? SizedBox(
|
||||||
);
|
height: 8,
|
||||||
}),
|
child: FladderSlider(
|
||||||
|
value: lastPosition.inMilliseconds.toDouble(),
|
||||||
|
min: 0.0,
|
||||||
|
max: playbackInfo.duration.inMilliseconds.toDouble(),
|
||||||
|
thumbWidth: 8,
|
||||||
|
onChangeStart: (value) {
|
||||||
|
setState(() {
|
||||||
|
changingSliderValue = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onChangeEnd: (value) async {
|
||||||
|
await player.seek(Duration(milliseconds: value ~/ 1));
|
||||||
|
await Future.delayed(const Duration(milliseconds: 250));
|
||||||
|
if (player.lastState?.playing == true) {
|
||||||
|
player.play();
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
lastPosition = Duration(milliseconds: value.toInt());
|
||||||
|
changingSliderValue = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
lastPosition = Duration(milliseconds: value.toInt());
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: LinearProgressIndicator(
|
||||||
|
minHeight: 8,
|
||||||
|
backgroundColor: Colors.black.withValues(alpha: 0.25),
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
value: (playbackInfo.position.inMilliseconds / playbackInfo.duration.inMilliseconds)
|
||||||
|
.clamp(0, 1),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ class NavigationButton extends ConsumerStatefulWidget {
|
||||||
final Function()? onPressed;
|
final Function()? onPressed;
|
||||||
final Function()? onLongPress;
|
final Function()? onLongPress;
|
||||||
final List<ItemAction> trailing;
|
final List<ItemAction> trailing;
|
||||||
|
final Widget? customIcon;
|
||||||
final bool selected;
|
final bool selected;
|
||||||
final Duration duration;
|
final Duration duration;
|
||||||
const NavigationButton({
|
const NavigationButton({
|
||||||
|
|
@ -24,6 +25,7 @@ class NavigationButton extends ConsumerStatefulWidget {
|
||||||
this.expanded = false,
|
this.expanded = false,
|
||||||
this.onPressed,
|
this.onPressed,
|
||||||
this.onLongPress,
|
this.onLongPress,
|
||||||
|
this.customIcon,
|
||||||
this.selected = false,
|
this.selected = false,
|
||||||
this.trailing = const [],
|
this.trailing = const [],
|
||||||
this.duration = const Duration(milliseconds: 125),
|
this.duration = const Duration(milliseconds: 125),
|
||||||
|
|
@ -64,9 +66,11 @@ class _NavigationButtonState extends ConsumerState<NavigationButton> {
|
||||||
onLongPress: widget.onLongPress,
|
onLongPress: widget.onLongPress,
|
||||||
child: widget.horizontal
|
child: widget.horizontal
|
||||||
? Padding(
|
? Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
padding: widget.customIcon != null
|
||||||
|
? EdgeInsetsGeometry.zero
|
||||||
|
: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 35,
|
height: widget.customIcon != null ? 60 : 35,
|
||||||
child: Row(
|
child: Row(
|
||||||
spacing: 4,
|
spacing: 4,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
|
@ -85,10 +89,11 @@ class _NavigationButtonState extends ConsumerState<NavigationButton> {
|
||||||
.withValues(alpha: widget.selected && !widget.expanded ? 1 : 0),
|
.withValues(alpha: widget.selected && !widget.expanded ? 1 : 0),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
AnimatedSwitcher(
|
widget.customIcon ??
|
||||||
duration: widget.duration,
|
AnimatedSwitcher(
|
||||||
child: widget.selected ? widget.selectedIcon : widget.icon,
|
duration: widget.duration,
|
||||||
),
|
child: widget.selected ? widget.selectedIcon : widget.icon,
|
||||||
|
),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
if (widget.horizontal && widget.expanded) ...[
|
if (widget.horizontal && widget.expanded) ...[
|
||||||
if (widget.label != null)
|
if (widget.label != null)
|
||||||
|
|
@ -119,17 +124,18 @@ class _NavigationButtonState extends ConsumerState<NavigationButton> {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Padding(
|
: Padding(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: widget.customIcon != null ? EdgeInsetsGeometry.zero : const EdgeInsets.all(8),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
children: [
|
children: [
|
||||||
AnimatedSwitcher(
|
widget.customIcon ??
|
||||||
duration: widget.duration,
|
AnimatedSwitcher(
|
||||||
child: widget.selected ? widget.selectedIcon : widget.icon,
|
duration: widget.duration,
|
||||||
),
|
child: widget.selected ? widget.selectedIcon : widget.icon,
|
||||||
|
),
|
||||||
if (widget.label != null && widget.horizontal && widget.expanded)
|
if (widget.label != null && widget.horizontal && widget.expanded)
|
||||||
Flexible(child: Text(widget.label!))
|
Flexible(child: Text(widget.label!))
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,13 @@ import 'package:iconsax_plus/iconsax_plus.dart';
|
||||||
|
|
||||||
import 'package:fladder/models/collection_types.dart';
|
import 'package:fladder/models/collection_types.dart';
|
||||||
import 'package:fladder/models/view_model.dart';
|
import 'package:fladder/models/view_model.dart';
|
||||||
|
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||||
import 'package:fladder/routes/auto_router.gr.dart';
|
import 'package:fladder/routes/auto_router.gr.dart';
|
||||||
import 'package:fladder/screens/metadata/refresh_metadata.dart';
|
import 'package:fladder/screens/metadata/refresh_metadata.dart';
|
||||||
import 'package:fladder/screens/shared/animated_fade_size.dart';
|
import 'package:fladder/screens/shared/animated_fade_size.dart';
|
||||||
|
import 'package:fladder/theme.dart';
|
||||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||||
|
import 'package:fladder/util/fladder_image.dart';
|
||||||
import 'package:fladder/util/localization_helper.dart';
|
import 'package:fladder/util/localization_helper.dart';
|
||||||
import 'package:fladder/widgets/navigation_scaffold/components/adaptive_fab.dart';
|
import 'package:fladder/widgets/navigation_scaffold/components/adaptive_fab.dart';
|
||||||
import 'package:fladder/widgets/navigation_scaffold/components/destination_model.dart';
|
import 'package:fladder/widgets/navigation_scaffold/components/destination_model.dart';
|
||||||
|
|
@ -36,6 +39,7 @@ class NestedNavigationDrawer extends ConsumerWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final useLibraryPosters = ref.watch(clientSettingsProvider.select((value) => value.usePosterForLibrary));
|
||||||
return NavigationDrawer(
|
return NavigationDrawer(
|
||||||
key: const Key('navigation_drawer'),
|
key: const Key('navigation_drawer'),
|
||||||
backgroundColor: isExpanded ? Colors.transparent : null,
|
backgroundColor: isExpanded ? Colors.transparent : null,
|
||||||
|
|
@ -91,22 +95,41 @@ class NestedNavigationDrawer extends ConsumerWidget {
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
...views.map((library) => DrawerListButton(
|
...views.map((library) {
|
||||||
label: library.name,
|
var selected = context.router.currentUrl.contains(library.id);
|
||||||
selected: context.router.currentUrl.contains(library.id),
|
final Widget? posterIcon = useLibraryPosters
|
||||||
actions: [
|
? ClipRRect(
|
||||||
ItemActionButton(
|
borderRadius: FladderTheme.smallShape.borderRadius,
|
||||||
label: Text(context.localized.scanLibrary),
|
child: AspectRatio(
|
||||||
icon: const Icon(IconsaxPlusLinear.refresh),
|
aspectRatio: 1.0,
|
||||||
action: () => showRefreshPopup(context, library.id, library.name),
|
child: FladderImage(
|
||||||
),
|
image: library.imageData?.primary,
|
||||||
],
|
placeHolder: Card(
|
||||||
onPressed: () {
|
child: Icon(
|
||||||
context.router.push(LibrarySearchRoute(viewModelId: library.id));
|
selected ? library.collectionType.icon : library.collectionType.iconOutlined,
|
||||||
Scaffold.of(context).closeDrawer();
|
),
|
||||||
},
|
),
|
||||||
selectedIcon: Icon(library.collectionType.icon),
|
),
|
||||||
icon: Icon(library.collectionType.iconOutlined))),
|
),
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
return DrawerListButton(
|
||||||
|
label: library.name,
|
||||||
|
selected: selected,
|
||||||
|
actions: [
|
||||||
|
ItemActionButton(
|
||||||
|
label: Text(context.localized.scanLibrary),
|
||||||
|
icon: const Icon(IconsaxPlusLinear.refresh),
|
||||||
|
action: () => showRefreshPopup(context, library.id, library.name),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onPressed: () {
|
||||||
|
context.router.push(LibrarySearchRoute(viewModelId: library.id));
|
||||||
|
Scaffold.of(context).closeDrawer();
|
||||||
|
},
|
||||||
|
selectedIcon: posterIcon ?? Icon(library.collectionType.icon),
|
||||||
|
icon: posterIcon ?? Icon(library.collectionType.iconOutlined));
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
const Divider(indent: 28, endIndent: 28),
|
const Divider(indent: 28, endIndent: 28),
|
||||||
if (isExpanded)
|
if (isExpanded)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
|
@ -9,16 +7,20 @@ import 'package:iconsax_plus/iconsax_plus.dart';
|
||||||
import 'package:overflow_view/overflow_view.dart';
|
import 'package:overflow_view/overflow_view.dart';
|
||||||
|
|
||||||
import 'package:fladder/models/collection_types.dart';
|
import 'package:fladder/models/collection_types.dart';
|
||||||
|
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||||
import 'package:fladder/providers/views_provider.dart';
|
import 'package:fladder/providers/views_provider.dart';
|
||||||
import 'package:fladder/routes/auto_router.gr.dart';
|
import 'package:fladder/routes/auto_router.gr.dart';
|
||||||
import 'package:fladder/screens/metadata/refresh_metadata.dart';
|
import 'package:fladder/screens/metadata/refresh_metadata.dart';
|
||||||
import 'package:fladder/screens/shared/animated_fade_size.dart';
|
import 'package:fladder/screens/shared/animated_fade_size.dart';
|
||||||
|
import 'package:fladder/theme.dart';
|
||||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||||
|
import 'package:fladder/util/fladder_image.dart';
|
||||||
import 'package:fladder/util/localization_helper.dart';
|
import 'package:fladder/util/localization_helper.dart';
|
||||||
import 'package:fladder/widgets/navigation_scaffold/components/adaptive_fab.dart';
|
import 'package:fladder/widgets/navigation_scaffold/components/adaptive_fab.dart';
|
||||||
import 'package:fladder/widgets/navigation_scaffold/components/destination_model.dart';
|
import 'package:fladder/widgets/navigation_scaffold/components/destination_model.dart';
|
||||||
import 'package:fladder/widgets/navigation_scaffold/components/navigation_button.dart';
|
import 'package:fladder/widgets/navigation_scaffold/components/navigation_button.dart';
|
||||||
import 'package:fladder/widgets/navigation_scaffold/components/settings_user_icon.dart';
|
import 'package:fladder/widgets/navigation_scaffold/components/settings_user_icon.dart';
|
||||||
|
import 'package:fladder/widgets/shared/custom_tooltip.dart';
|
||||||
import 'package:fladder/widgets/shared/item_actions.dart';
|
import 'package:fladder/widgets/shared/item_actions.dart';
|
||||||
import 'package:fladder/widgets/shared/modal_bottom_sheet.dart';
|
import 'package:fladder/widgets/shared/modal_bottom_sheet.dart';
|
||||||
|
|
||||||
|
|
@ -43,39 +45,21 @@ class SideNavigationBar extends ConsumerStatefulWidget {
|
||||||
|
|
||||||
class _SideNavigationBarState extends ConsumerState<SideNavigationBar> {
|
class _SideNavigationBarState extends ConsumerState<SideNavigationBar> {
|
||||||
bool expandedSideBar = false;
|
bool expandedSideBar = false;
|
||||||
bool showOnHover = false;
|
|
||||||
Timer? timer;
|
|
||||||
double currentWidth = 80;
|
|
||||||
|
|
||||||
void startTimer() {
|
|
||||||
timer?.cancel();
|
|
||||||
timer = Timer(const Duration(milliseconds: 650), () {
|
|
||||||
setState(() {
|
|
||||||
showOnHover = true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void stopTimer() {
|
|
||||||
timer?.cancel();
|
|
||||||
timer = Timer(const Duration(milliseconds: 125), () {
|
|
||||||
setState(() {
|
|
||||||
showOnHover = false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final views = ref.watch(viewsProvider.select((value) => value.views));
|
final views = ref.watch(viewsProvider.select((value) => value.views));
|
||||||
|
final usePostersForLibrary = ref.watch(clientSettingsProvider.select((value) => value.usePosterForLibrary));
|
||||||
|
|
||||||
final expandedWidth = 250.0;
|
final expandedWidth = 250.0;
|
||||||
final padding = MediaQuery.paddingOf(context);
|
final padding = MediaQuery.paddingOf(context);
|
||||||
|
|
||||||
final collapsedWidth = 90.0 + padding.left;
|
final collapsedWidth = 90 + padding.left;
|
||||||
final largeBar = AdaptiveLayout.layoutModeOf(context) != LayoutMode.single;
|
final largeBar = AdaptiveLayout.layoutModeOf(context) != LayoutMode.single;
|
||||||
final fullyExpanded = largeBar ? expandedSideBar : false;
|
final fullyExpanded = largeBar ? expandedSideBar : false;
|
||||||
final shouldExpand = showOnHover || fullyExpanded;
|
final shouldExpand = fullyExpanded;
|
||||||
final isDesktop = AdaptiveLayout.of(context).isDesktop;
|
final isDesktop = AdaptiveLayout.of(context).isDesktop;
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
AdaptiveLayoutBuilder(
|
AdaptiveLayoutBuilder(
|
||||||
|
|
@ -88,151 +72,229 @@ class _SideNavigationBarState extends ConsumerState<SideNavigationBar> {
|
||||||
color: Theme.of(context).colorScheme.surface.withValues(alpha: shouldExpand ? 0.95 : 0.85),
|
color: Theme.of(context).colorScheme.surface.withValues(alpha: shouldExpand ? 0.95 : 0.85),
|
||||||
width: shouldExpand ? expandedWidth : collapsedWidth,
|
width: shouldExpand ? expandedWidth : collapsedWidth,
|
||||||
child: MouseRegion(
|
child: MouseRegion(
|
||||||
onEnter: (value) => startTimer(),
|
child: Padding(
|
||||||
onExit: (event) => stopTimer(),
|
key: const Key('navigation_rail'),
|
||||||
onHover: (value) => startTimer(),
|
padding: padding.copyWith(right: 0, top: isDesktop ? padding.top : null),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
spacing: 2,
|
||||||
if (isDesktop && AdaptiveLayout.of(context).platform != TargetPlatform.macOS) ...{
|
children: [
|
||||||
const SizedBox(height: 4),
|
Padding(
|
||||||
Text(
|
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||||
"Fladder",
|
child: Row(
|
||||||
style: Theme.of(context).textTheme.titleSmall,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
),
|
|
||||||
},
|
|
||||||
if (AdaptiveLayout.of(context).platform == TargetPlatform.macOS) SizedBox(height: padding.top),
|
|
||||||
Expanded(
|
|
||||||
child: Padding(
|
|
||||||
key: const Key('navigation_rail'),
|
|
||||||
padding: padding.copyWith(right: 0, top: isDesktop ? 8 : null),
|
|
||||||
child: Column(
|
|
||||||
spacing: 2,
|
|
||||||
children: [
|
children: [
|
||||||
Align(
|
if (expandedSideBar) ...[
|
||||||
alignment: largeBar && expandedSideBar ? Alignment.centerRight : Alignment.center,
|
Expanded(child: Text(context.localized.navigation)),
|
||||||
child: Opacity(
|
],
|
||||||
opacity: largeBar && expandedSideBar ? 0.65 : 1.0,
|
Opacity(
|
||||||
child: IconButton(
|
opacity: largeBar && expandedSideBar ? 0.65 : 1.0,
|
||||||
onPressed: !largeBar
|
child: IconButton(
|
||||||
? () => widget.scaffoldKey.currentState?.openDrawer()
|
onPressed: !largeBar
|
||||||
: () => setState(() {
|
? () => widget.scaffoldKey.currentState?.openDrawer()
|
||||||
expandedSideBar = !expandedSideBar;
|
: () => setState(() => expandedSideBar = !expandedSideBar),
|
||||||
if (!expandedSideBar) {
|
icon: Icon(
|
||||||
showOnHover = false;
|
largeBar && expandedSideBar ? IconsaxPlusLinear.sidebar_left : IconsaxPlusLinear.menu,
|
||||||
}
|
|
||||||
}),
|
|
||||||
icon: Icon(
|
|
||||||
largeBar && expandedSideBar ? IconsaxPlusLinear.sidebar_left : IconsaxPlusLinear.menu,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
const SizedBox(height: 8),
|
|
||||||
if (largeBar) ...[
|
|
||||||
AnimatedFadeSize(
|
|
||||||
duration: const Duration(milliseconds: 250),
|
|
||||||
child: shouldExpand ? actionButton(context).extended : actionButton(context).normal,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
spacing: 2,
|
|
||||||
mainAxisAlignment: !largeBar ? MainAxisAlignment.center : MainAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
...widget.destinations.mapIndexed(
|
|
||||||
(index, destination) =>
|
|
||||||
destination.toNavigationButton(widget.currentIndex == index, true, shouldExpand),
|
|
||||||
),
|
|
||||||
if (views.isNotEmpty && largeBar) ...[
|
|
||||||
const Divider(
|
|
||||||
indent: 32,
|
|
||||||
endIndent: 32,
|
|
||||||
),
|
|
||||||
Flexible(
|
|
||||||
child: OverflowView.flexible(
|
|
||||||
direction: Axis.vertical,
|
|
||||||
spacing: 4,
|
|
||||||
children: views.map(
|
|
||||||
(view) {
|
|
||||||
final actions = [
|
|
||||||
ItemActionButton(
|
|
||||||
label: Text(context.localized.scanLibrary),
|
|
||||||
icon: const Icon(IconsaxPlusLinear.refresh),
|
|
||||||
action: () => showRefreshPopup(context, view.id, view.name),
|
|
||||||
)
|
|
||||||
];
|
|
||||||
return view.toNavigationButton(
|
|
||||||
context.router.currentUrl.contains(view.id),
|
|
||||||
true,
|
|
||||||
shouldExpand,
|
|
||||||
() => context.pushRoute(LibrarySearchRoute(viewModelId: view.id)),
|
|
||||||
onLongPress: () => showBottomSheetPill(
|
|
||||||
context: context,
|
|
||||||
content: (context, scrollController) => ListView(
|
|
||||||
shrinkWrap: true,
|
|
||||||
controller: scrollController,
|
|
||||||
children: actions.listTileItems(context, useIcons: true),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
trailing: actions,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
).toList(),
|
|
||||||
builder: (context, remaining) {
|
|
||||||
return PopupMenuButton(
|
|
||||||
iconColor: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.45),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
icon: NavigationButton(
|
|
||||||
label: context.localized.other,
|
|
||||||
selectedIcon: const Icon(IconsaxPlusLinear.arrow_square_down),
|
|
||||||
icon: const Icon(IconsaxPlusLinear.arrow_square_down),
|
|
||||||
expanded: shouldExpand,
|
|
||||||
horizontal: true,
|
|
||||||
),
|
|
||||||
itemBuilder: (context) => views
|
|
||||||
.sublist(views.length - remaining)
|
|
||||||
.map(
|
|
||||||
(e) => PopupMenuItem(
|
|
||||||
onTap: () => context.pushRoute(LibrarySearchRoute(viewModelId: e.id)),
|
|
||||||
child: Row(
|
|
||||||
spacing: 8,
|
|
||||||
children: [
|
|
||||||
Icon(e.collectionType.iconOutlined),
|
|
||||||
Text(e.name),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
NavigationButton(
|
|
||||||
label: context.localized.settings,
|
|
||||||
selected: widget.currentLocation.contains(const SettingsRoute().routeName),
|
|
||||||
selectedIcon: const Icon(IconsaxPlusBold.setting_3),
|
|
||||||
horizontal: true,
|
|
||||||
expanded: shouldExpand,
|
|
||||||
icon: const SettingsUserIcon(),
|
|
||||||
onPressed: () {
|
|
||||||
if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single) {
|
|
||||||
context.router.push(const SettingsRoute());
|
|
||||||
} else {
|
|
||||||
context.router.push(const ClientSettingsRoute());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
if (largeBar) ...[
|
||||||
if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer) const SizedBox(height: 16),
|
AnimatedFadeSize(
|
||||||
],
|
duration: const Duration(milliseconds: 250),
|
||||||
|
child: shouldExpand ? actionButton(context).extended : actionButton(context).normal,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: !largeBar ? MainAxisAlignment.center : MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
...widget.destinations.mapIndexed(
|
||||||
|
(index, destination) => CustomTooltip(
|
||||||
|
tooltipContent: expandedSideBar
|
||||||
|
? null
|
||||||
|
: Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Text(
|
||||||
|
destination.label,
|
||||||
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
position: TooltipPosition.right,
|
||||||
|
child: destination.toNavigationButton(
|
||||||
|
widget.currentIndex == index,
|
||||||
|
true,
|
||||||
|
shouldExpand,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (views.isNotEmpty && largeBar) ...[
|
||||||
|
const Divider(
|
||||||
|
indent: 32,
|
||||||
|
endIndent: 32,
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: OverflowView.flexible(
|
||||||
|
direction: Axis.vertical,
|
||||||
|
spacing: 4,
|
||||||
|
children: views.map(
|
||||||
|
(view) {
|
||||||
|
final selected = context.router.currentUrl.contains(view.id);
|
||||||
|
final actions = [
|
||||||
|
ItemActionButton(
|
||||||
|
label: Text(context.localized.scanLibrary),
|
||||||
|
icon: const Icon(IconsaxPlusLinear.refresh),
|
||||||
|
action: () => showRefreshPopup(context, view.id, view.name),
|
||||||
|
)
|
||||||
|
];
|
||||||
|
return CustomTooltip(
|
||||||
|
tooltipContent: expandedSideBar
|
||||||
|
? null
|
||||||
|
: Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Text(
|
||||||
|
view.name,
|
||||||
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
position: TooltipPosition.right,
|
||||||
|
child: view.toNavigationButton(
|
||||||
|
selected,
|
||||||
|
true,
|
||||||
|
shouldExpand,
|
||||||
|
() => context.pushRoute(LibrarySearchRoute(viewModelId: view.id)),
|
||||||
|
onLongPress: () => showBottomSheetPill(
|
||||||
|
context: context,
|
||||||
|
content: (context, scrollController) => ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
|
controller: scrollController,
|
||||||
|
children: actions.listTileItems(context, useIcons: true),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
customIcon: usePostersForLibrary
|
||||||
|
? ClipRRect(
|
||||||
|
borderRadius: FladderTheme.smallShape.borderRadius,
|
||||||
|
child: SizedBox.square(
|
||||||
|
dimension: 50,
|
||||||
|
child: FladderImage(
|
||||||
|
image: view.imageData?.primary,
|
||||||
|
placeHolder: Card(
|
||||||
|
child: Icon(
|
||||||
|
selected
|
||||||
|
? view.collectionType.icon
|
||||||
|
: view.collectionType.iconOutlined,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
trailing: actions,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).toList(),
|
||||||
|
builder: (context, remaining) {
|
||||||
|
return CustomTooltip(
|
||||||
|
tooltipContent: expandedSideBar
|
||||||
|
? null
|
||||||
|
: Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Text(
|
||||||
|
context.localized.moreOptions,
|
||||||
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
position: TooltipPosition.right,
|
||||||
|
child: PopupMenuButton(
|
||||||
|
iconColor: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.45),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
tooltip: "",
|
||||||
|
icon: NavigationButton(
|
||||||
|
label: context.localized.other,
|
||||||
|
selectedIcon: const Icon(IconsaxPlusLinear.arrow_square_down),
|
||||||
|
icon: const Icon(IconsaxPlusLinear.arrow_square_down),
|
||||||
|
expanded: shouldExpand,
|
||||||
|
customIcon: usePostersForLibrary
|
||||||
|
? ClipRRect(
|
||||||
|
borderRadius: FladderTheme.smallShape.borderRadius,
|
||||||
|
child: const SizedBox.square(
|
||||||
|
dimension: 50,
|
||||||
|
child: Card(
|
||||||
|
child: Icon(IconsaxPlusLinear.arrow_square_down),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
horizontal: true,
|
||||||
|
),
|
||||||
|
itemBuilder: (context) => views
|
||||||
|
.sublist(views.length - remaining)
|
||||||
|
.map(
|
||||||
|
(e) => PopupMenuItem(
|
||||||
|
onTap: () => context.pushRoute(LibrarySearchRoute(viewModelId: e.id)),
|
||||||
|
child: Row(
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
usePostersForLibrary
|
||||||
|
? Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: FladderTheme.smallShape.borderRadius,
|
||||||
|
child: SizedBox.square(
|
||||||
|
dimension: 45,
|
||||||
|
child: FladderImage(
|
||||||
|
image: e.imageData?.primary,
|
||||||
|
placeHolder: Card(
|
||||||
|
child: Icon(
|
||||||
|
e.collectionType.iconOutlined,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Icon(e.collectionType.iconOutlined),
|
||||||
|
Text(e.name),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
NavigationButton(
|
||||||
|
label: context.localized.settings,
|
||||||
|
selected: widget.currentLocation.contains(const SettingsRoute().routeName),
|
||||||
|
selectedIcon: const Icon(IconsaxPlusBold.setting_3),
|
||||||
|
horizontal: true,
|
||||||
|
expanded: shouldExpand,
|
||||||
|
icon: const SettingsUserIcon(),
|
||||||
|
onPressed: () {
|
||||||
|
if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single) {
|
||||||
|
context.router.push(const SettingsRoute());
|
||||||
|
} else {
|
||||||
|
context.router.push(const ClientSettingsRoute());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer) const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import 'package:fladder/routes/auto_router.dart';
|
||||||
import 'package:fladder/screens/shared/animated_fade_size.dart';
|
import 'package:fladder/screens/shared/animated_fade_size.dart';
|
||||||
import 'package:fladder/screens/shared/nested_bottom_appbar.dart';
|
import 'package:fladder/screens/shared/nested_bottom_appbar.dart';
|
||||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||||
import 'package:fladder/util/theme_extensions.dart';
|
|
||||||
import 'package:fladder/widgets/navigation_scaffold/components/destination_model.dart';
|
import 'package:fladder/widgets/navigation_scaffold/components/destination_model.dart';
|
||||||
import 'package:fladder/widgets/navigation_scaffold/components/fladder_app_bar.dart';
|
import 'package:fladder/widgets/navigation_scaffold/components/fladder_app_bar.dart';
|
||||||
import 'package:fladder/widgets/navigation_scaffold/components/floating_player_bar.dart';
|
import 'package:fladder/widgets/navigation_scaffold/components/floating_player_bar.dart';
|
||||||
|
|
@ -58,9 +57,15 @@ class _NavigationScaffoldState extends ConsumerState<NavigationScaffold> {
|
||||||
|
|
||||||
final isDesktop = AdaptiveLayout.of(context).isDesktop;
|
final isDesktop = AdaptiveLayout.of(context).isDesktop;
|
||||||
|
|
||||||
final bottomPadding = isDesktop || kIsWeb ? 0.0 : MediaQuery.paddingOf(context).bottom;
|
final mediaQuery = MediaQuery.of(context);
|
||||||
|
|
||||||
|
final paddingOf = mediaQuery.padding;
|
||||||
|
final viewPaddingOf = mediaQuery.viewPadding;
|
||||||
|
|
||||||
|
final bottomPadding = isDesktop || kIsWeb ? 0.0 : paddingOf.bottom;
|
||||||
|
final bottomViewPadding = isDesktop || kIsWeb ? 0.0 : viewPaddingOf.bottom;
|
||||||
final isHomeScreen = currentIndex != -1;
|
final isHomeScreen = currentIndex != -1;
|
||||||
|
|
||||||
return PopScope(
|
return PopScope(
|
||||||
canPop: currentIndex == 0,
|
canPop: currentIndex == 0,
|
||||||
onPopInvokedWithResult: (didPop, result) {
|
onPopInvokedWithResult: (didPop, result) {
|
||||||
|
|
@ -72,58 +77,66 @@ class _NavigationScaffoldState extends ConsumerState<NavigationScaffold> {
|
||||||
alignment: Alignment.bottomCenter,
|
alignment: Alignment.bottomCenter,
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Padding(
|
child: MediaQuery(
|
||||||
padding: EdgeInsets.only(bottom: showPlayerBar ? floatingPlayerHeight - 12 + bottomPadding : 0),
|
data: mediaQuery.copyWith(
|
||||||
child: Scaffold(
|
padding: paddingOf.copyWith(
|
||||||
key: _key,
|
bottom: showPlayerBar ? floatingPlayerHeight(context) + 12 + bottomPadding : bottomPadding),
|
||||||
appBar: const FladderAppBar(),
|
viewPadding: viewPaddingOf.copyWith(
|
||||||
extendBodyBehindAppBar: true,
|
bottom: showPlayerBar ? floatingPlayerHeight(context) + bottomViewPadding : bottomViewPadding),
|
||||||
resizeToAvoidBottomInset: false,
|
),
|
||||||
extendBody: true,
|
//Builder to correctly apply new padding
|
||||||
floatingActionButton: AdaptiveLayout.layoutModeOf(context) == LayoutMode.single && isHomeScreen
|
child: Builder(builder: (context) {
|
||||||
? widget.destinations.elementAtOrNull(currentIndex)?.floatingActionButton?.normal
|
return Scaffold(
|
||||||
: null,
|
key: _key,
|
||||||
drawer: homeRoutes.any((element) => element.name.contains(currentLocation))
|
appBar: const FladderAppBar(),
|
||||||
? NestedNavigationDrawer(
|
extendBodyBehindAppBar: true,
|
||||||
actionButton: null,
|
resizeToAvoidBottomInset: false,
|
||||||
toggleExpanded: (value) => _key.currentState?.closeDrawer(),
|
extendBody: true,
|
||||||
views: views,
|
floatingActionButton: AdaptiveLayout.layoutModeOf(context) == LayoutMode.single && isHomeScreen
|
||||||
destinations: widget.destinations,
|
? widget.destinations.elementAtOrNull(currentIndex)?.floatingActionButton?.normal
|
||||||
currentLocation: currentLocation,
|
: null,
|
||||||
)
|
drawer: homeRoutes.any((element) => element.name.contains(currentLocation))
|
||||||
: null,
|
? NestedNavigationDrawer(
|
||||||
bottomNavigationBar: isHomeScreen && AdaptiveLayout.viewSizeOf(context) == ViewSize.phone
|
actionButton: null,
|
||||||
? HideOnScroll(
|
toggleExpanded: (value) => _key.currentState?.closeDrawer(),
|
||||||
controller: AdaptiveLayout.scrollOf(context),
|
views: views,
|
||||||
forceHide: !homeRoutes.any((element) => element.name.contains(currentLocation)),
|
destinations: widget.destinations,
|
||||||
child: NestedBottomAppBar(
|
currentLocation: currentLocation,
|
||||||
child: SizedBox(
|
)
|
||||||
height: 65,
|
: null,
|
||||||
child: Row(
|
bottomNavigationBar: isHomeScreen && AdaptiveLayout.viewSizeOf(context) == ViewSize.phone
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
? HideOnScroll(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
controller: AdaptiveLayout.scrollOf(context),
|
||||||
children: widget.destinations
|
forceHide: !homeRoutes.any((element) => element.name.contains(currentLocation)),
|
||||||
.map(
|
child: NestedBottomAppBar(
|
||||||
(destination) => destination.toNavigationButton(
|
child: SizedBox(
|
||||||
widget.currentRouteName == destination.route?.routeName, false, false),
|
height: 65,
|
||||||
)
|
child: Row(
|
||||||
.toList(),
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: widget.destinations
|
||||||
|
.map(
|
||||||
|
(destination) => destination.toNavigationButton(
|
||||||
|
widget.currentRouteName == destination.route?.routeName, false, false),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
)
|
: null,
|
||||||
: null,
|
body: widget.nestedChild != null
|
||||||
body: widget.nestedChild != null
|
? NavigationBody(
|
||||||
? NavigationBody(
|
child: widget.nestedChild!,
|
||||||
child: widget.nestedChild!,
|
parentContext: context,
|
||||||
parentContext: context,
|
currentIndex: currentIndex,
|
||||||
currentIndex: currentIndex,
|
destinations: widget.destinations,
|
||||||
destinations: widget.destinations,
|
currentLocation: currentLocation,
|
||||||
currentLocation: currentLocation,
|
drawerKey: _key,
|
||||||
drawerKey: _key,
|
)
|
||||||
)
|
: null,
|
||||||
: null,
|
);
|
||||||
),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Material(
|
Material(
|
||||||
|
|
@ -131,19 +144,7 @@ class _NavigationScaffoldState extends ConsumerState<NavigationScaffold> {
|
||||||
child: AnimatedFadeSize(
|
child: AnimatedFadeSize(
|
||||||
child: Container(
|
child: Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
decoration: BoxDecoration(
|
child: showPlayerBar ? const FloatingPlayerBar() : const SizedBox.shrink(),
|
||||||
color: context.colors.surface,
|
|
||||||
borderRadius: const BorderRadius.only(
|
|
||||||
topLeft: Radius.circular(12),
|
|
||||||
topRight: Radius.circular(12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: showPlayerBar
|
|
||||||
? Padding(
|
|
||||||
padding: EdgeInsets.only(bottom: bottomPadding),
|
|
||||||
child: const FloatingPlayerBar(),
|
|
||||||
)
|
|
||||||
: const SizedBox.shrink(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
130
lib/widgets/shared/custom_tooltip.dart
Normal file
130
lib/widgets/shared/custom_tooltip.dart
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class CustomTooltip extends StatefulWidget {
|
||||||
|
final Widget child;
|
||||||
|
final Widget? tooltipContent;
|
||||||
|
final double offset;
|
||||||
|
final TooltipPosition position;
|
||||||
|
final Duration showDelay;
|
||||||
|
|
||||||
|
const CustomTooltip({
|
||||||
|
required this.child,
|
||||||
|
required this.tooltipContent,
|
||||||
|
this.offset = 12,
|
||||||
|
this.position = TooltipPosition.top,
|
||||||
|
this.showDelay = const Duration(milliseconds: 125),
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
CustomTooltipState createState() => CustomTooltipState();
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TooltipPosition { top, bottom, left, right }
|
||||||
|
|
||||||
|
class CustomTooltipState extends State<CustomTooltip> {
|
||||||
|
OverlayEntry? _overlayEntry;
|
||||||
|
Timer? _tooltipTimer;
|
||||||
|
final GlobalKey _tooltipKey = GlobalKey();
|
||||||
|
|
||||||
|
void _showTooltip() {
|
||||||
|
_tooltipTimer?.cancel();
|
||||||
|
|
||||||
|
_tooltipTimer = Timer(widget.showDelay, () {
|
||||||
|
if (_overlayEntry == null) {
|
||||||
|
_overlayEntry = _createOverlayEntry();
|
||||||
|
Overlay.of(context).insert(_overlayEntry!);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _hideTooltip() {
|
||||||
|
_tooltipTimer?.cancel();
|
||||||
|
_overlayEntry?.remove();
|
||||||
|
_overlayEntry = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
OverlayEntry _createOverlayEntry() {
|
||||||
|
RenderBox renderBox = context.findRenderObject() as RenderBox;
|
||||||
|
Offset targetPosition = renderBox.localToGlobal(Offset.zero);
|
||||||
|
Size targetSize = renderBox.size;
|
||||||
|
|
||||||
|
return OverlayEntry(
|
||||||
|
builder: (context) {
|
||||||
|
final tooltipRenderBox = _tooltipKey.currentContext?.findRenderObject() as RenderBox?;
|
||||||
|
if (tooltipRenderBox != null) {
|
||||||
|
Size tooltipSize = tooltipRenderBox.size;
|
||||||
|
|
||||||
|
Offset tooltipPosition;
|
||||||
|
switch (widget.position) {
|
||||||
|
case TooltipPosition.top:
|
||||||
|
tooltipPosition = Offset(
|
||||||
|
targetPosition.dx + (targetSize.width - tooltipSize.width) / 2,
|
||||||
|
targetPosition.dy - tooltipSize.height - widget.offset,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case TooltipPosition.bottom:
|
||||||
|
tooltipPosition = Offset(
|
||||||
|
targetPosition.dx + (targetSize.width - tooltipSize.width) / 2,
|
||||||
|
targetPosition.dy + targetSize.height + widget.offset,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case TooltipPosition.left:
|
||||||
|
tooltipPosition = Offset(
|
||||||
|
targetPosition.dx - tooltipSize.width - widget.offset,
|
||||||
|
targetPosition.dy + (targetSize.height - tooltipSize.height) / 2,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case TooltipPosition.right:
|
||||||
|
tooltipPosition = Offset(
|
||||||
|
targetPosition.dx + targetSize.width + widget.offset,
|
||||||
|
targetPosition.dy + (targetSize.height - tooltipSize.height) / 2,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Positioned(
|
||||||
|
left: tooltipPosition.dx,
|
||||||
|
top: tooltipPosition.dy,
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: widget.tooltipContent,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (widget.tooltipContent == null) return widget.child;
|
||||||
|
return MouseRegion(
|
||||||
|
onEnter: (_) => _showTooltip(),
|
||||||
|
onExit: (_) => _hideTooltip(),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
widget.child,
|
||||||
|
Positioned(
|
||||||
|
left: -1000,
|
||||||
|
top: -1000,
|
||||||
|
child: Container(
|
||||||
|
key: _tooltipKey,
|
||||||
|
child: widget.tooltipContent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_tooltipTimer?.cancel();
|
||||||
|
_hideTooltip(); // Ensure the tooltip is hidden on dispose
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,7 @@ abstract class ItemAction {
|
||||||
PopupMenuEntry toPopupMenuItem({bool useIcons = false});
|
PopupMenuEntry toPopupMenuItem({bool useIcons = false});
|
||||||
Widget toLabel();
|
Widget toLabel();
|
||||||
Widget toListItem(BuildContext context, {bool useIcons = false, bool shouldPop = true});
|
Widget toListItem(BuildContext context, {bool useIcons = false, bool shouldPop = true});
|
||||||
|
Widget toButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
class ItemActionDivider extends ItemAction {
|
class ItemActionDivider extends ItemAction {
|
||||||
|
|
@ -26,6 +27,9 @@ class ItemActionDivider extends ItemAction {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget toListItem(BuildContext context, {bool useIcons = false, bool shouldPop = true}) => const Divider();
|
Widget toListItem(BuildContext context, {bool useIcons = false, bool shouldPop = true}) => const Divider();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget toButton() => Container();
|
||||||
}
|
}
|
||||||
|
|
||||||
class ItemActionButton extends ItemAction {
|
class ItemActionButton extends ItemAction {
|
||||||
|
|
@ -51,9 +55,10 @@ class ItemActionButton extends ItemAction {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MenuItemButton toMenuItemButton() {
|
MenuItemButton toMenuItemButton() => MenuItemButton(leadingIcon: icon, onPressed: action, child: label);
|
||||||
return MenuItemButton(leadingIcon: icon, onPressed: action, child: label);
|
|
||||||
}
|
@override
|
||||||
|
Widget toButton() => IconButton(onPressed: action, icon: icon ?? const SizedBox.shrink());
|
||||||
|
|
||||||
@override
|
@override
|
||||||
PopupMenuItem toPopupMenuItem({bool useIcons = false}) {
|
PopupMenuItem toPopupMenuItem({bool useIcons = false}) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue