import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/arguments_provider.dart'; import 'package:fladder/providers/settings/home_settings_provider.dart'; import 'package:fladder/screens/home_screen.dart'; import 'package:fladder/util/adaptive_layout/adaptive_layout_model.dart'; import 'package:fladder/util/debug_banner.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/poster_defaults.dart'; import 'package:fladder/util/resolution_checker.dart'; enum InputDevice { touch, pointer, dPad, } enum ViewSize { phone, tablet, desktop, television; const ViewSize(); String label(BuildContext context) => switch (this) { ViewSize.phone => context.localized.phone, ViewSize.tablet => context.localized.tablet, ViewSize.desktop => context.localized.desktop, ViewSize.television => context.localized.television, }; bool operator >(ViewSize other) => index > other.index; bool operator >=(ViewSize other) => index >= other.index; bool operator <(ViewSize other) => index < other.index; bool operator <=(ViewSize other) => index <= other.index; } enum LayoutMode { single, dual; const LayoutMode(); String label(BuildContext context) => switch (this) { LayoutMode.single => context.localized.layoutModeSingle, LayoutMode.dual => context.localized.layoutModeDual, }; bool operator >(ViewSize other) => index > other.index; bool operator >=(ViewSize other) => index >= other.index; bool operator <(ViewSize other) => index < other.index; bool operator <=(ViewSize other) => index <= other.index; } class AdaptiveLayout extends InheritedWidget { final AdaptiveLayoutModel data; const AdaptiveLayout({ super.key, required this.data, required super.child, }); static AdaptiveLayoutModel of(BuildContext context) { final inherited = context.dependOnInheritedWidgetOfExactType(); assert(inherited != null, 'No AdaptiveLayout found in context'); return inherited!.data; } static AdaptiveLayout? maybeOf(BuildContext context) { return context.dependOnInheritedWidgetOfExactType(); } static ViewSize layoutOf(BuildContext context) { final AdaptiveLayout? result = maybeOf(context); return result!.data.viewSize; } static PosterDefaults poster(BuildContext context) { final AdaptiveLayout? result = maybeOf(context); return result!.data.posterDefaults; } static ScrollController scrollOf(BuildContext context, HomeTabs tab) { final AdaptiveLayout? result = maybeOf(context); return result?.data.controller[tab] ?? ScrollController(); } static EdgeInsets adaptivePadding(BuildContext context, {double horizontalPadding = 16}) { final viewPadding = MediaQuery.paddingOf(context); final padding = viewPadding.copyWith( left: AdaptiveLayout.of(context).sideBarWidth + horizontalPadding + viewPadding.left, top: 0, bottom: 0, right: viewPadding.right + horizontalPadding, ); return padding; } static LayoutMode layoutModeOf(BuildContext context) => maybeOf(context)!.data.layoutMode; static ViewSize viewSizeOf(BuildContext context) => maybeOf(context)!.data.viewSize; static InputDevice inputDeviceOf(BuildContext context) => maybeOf(context)!.data.inputDevice; @override bool updateShouldNotify(AdaptiveLayout oldWidget) => data != oldWidget.data; } const defaultTitleBarHeight = 35.0; class AdaptiveLayoutBuilder extends ConsumerStatefulWidget { final AdaptiveLayoutModel? adaptiveLayout; final Widget Function(BuildContext context) child; const AdaptiveLayoutBuilder({ this.adaptiveLayout, required this.child, super.key, }); @override ConsumerState createState() => _AdaptiveLayoutBuilderState(); } class _AdaptiveLayoutBuilderState extends ConsumerState { List layoutPoints = [ LayoutPoints(start: 0, end: 599, type: ViewSize.phone), LayoutPoints(start: 600, end: 1919, type: ViewSize.tablet), LayoutPoints(start: 1920, end: 3180, type: ViewSize.desktop), ]; late ViewSize viewSize = ViewSize.tablet; late LayoutMode layoutMode = LayoutMode.single; late TargetPlatform currentPlatform = defaultTargetPlatform; final Map scrollControllers = { for (var item in HomeTabs.values) item: ScrollController(), }; @override void didChangeDependencies() { calculateLayout(); calculateSize(); super.didChangeDependencies(); } bool get isDesktop { if (kIsWeb) return false; return [ TargetPlatform.macOS, TargetPlatform.windows, TargetPlatform.linux, ].contains(currentPlatform); } void calculateLayout() { ViewSize? newType; for (var element in layoutPoints) { if (MediaQuery.of(context).size.width > element.start && MediaQuery.of(context).size.width < element.end) { newType = element.type; } } viewSize = newType ?? ViewSize.tablet; } void calculateSize() { LayoutMode newSize; if (MediaQuery.of(context).size.width > 0 && MediaQuery.of(context).size.width < 960) { newSize = LayoutMode.single; } else { newSize = LayoutMode.dual; } layoutMode = newSize; } @override Widget build(BuildContext context) { final arguments = ref.watch(argumentsStateProvider); final htpcMode = arguments.htpcMode; final acceptedLayouts = htpcMode ? {LayoutMode.dual} : ref.watch(homeSettingsProvider.select((value) => value.screenLayouts)); final acceptedViewSizes = htpcMode ? {ViewSize.television} : ref.watch(homeSettingsProvider.select((value) => value.layoutStates)); final selectedViewSize = selectAvailableOrSmaller(viewSize, acceptedViewSizes, ViewSize.values); final selectedLayoutMode = selectAvailableOrSmaller(layoutMode, acceptedLayouts, LayoutMode.values); final input = htpcMode ? InputDevice.dPad : (isDesktop || kIsWeb) ? InputDevice.pointer : InputDevice.touch; final posterDefaults = const PosterDefaults(size: 350, ratio: 0.55); final currentLayout = widget.adaptiveLayout ?? AdaptiveLayoutModel( viewSize: selectedViewSize, layoutMode: selectedLayoutMode, inputDevice: input, platform: currentPlatform, isDesktop: isDesktop, sideBarWidth: 0, controller: scrollControllers, posterDefaults: posterDefaults, ); final mediaQuery = MediaQuery.of(context); return MediaQuery( data: mediaQuery.copyWith( padding: isDesktop || kIsWeb ? const EdgeInsets.only(top: defaultTitleBarHeight, bottom: 16) : null, viewPadding: isDesktop || kIsWeb ? const EdgeInsets.only(top: defaultTitleBarHeight, bottom: 16) : null, ), child: AdaptiveLayout( data: currentLayout.copyWith( viewSize: selectedViewSize, layoutMode: selectedLayoutMode, inputDevice: input, platform: currentPlatform, isDesktop: isDesktop, controller: scrollControllers, posterDefaults: posterDefaults, ), child: Builder( builder: (context) => isDesktop ? ResolutionChecker( child: widget.adaptiveLayout == null ? DebugBanner(child: widget.child(context)) : widget.child(context), ) : widget.adaptiveLayout == null ? DebugBanner(child: widget.child(context)) : widget.child(context), ), ), ); } } double? get topPadding { return switch (defaultTargetPlatform) { TargetPlatform.linux || TargetPlatform.windows || TargetPlatform.macOS => 35, _ => null }; }