import 'dart:async';
import 'dart:collection';
import 'dart:io';
import 'dart:ui';

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' hide ReorderableListView, ReorderCallback, SliverReorderableList, ReorderableDragStartListener, ReorderableDelayedDragStartListener, Tooltip;
import 'package:flutter/rendering.dart' as fr;
import 'package:flutter/services.dart';

import 'package:basic_audio_handler/basic_audio_handler.dart';
import 'package:checkmark/checkmark.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_scrollbar_modified/flutter_scrollbar_modified.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:flutter_tilt/flutter_tilt.dart';
import 'package:history_manager/history_manager.dart';
import 'package:like_button/like_button.dart';
import 'package:photo_view/photo_view.dart';
import 'package:playlist_manager/playlist_manager.dart';
import 'package:selectable_autolink_text/selectable_autolink_text.dart';
import 'package:sleek_circular_slider/sleek_circular_slider.dart';
import 'package:super_sliver_list/super_sliver_list.dart';

import 'package:namida/base/audio_handler.dart';
import 'package:namida/base/pull_to_refresh.dart';
import 'package:namida/class/faudiomodel.dart';
import 'package:namida/class/route.dart';
import 'package:namida/class/shortcut_data.dart';
import 'package:namida/class/track.dart';
import 'package:namida/class/version_wrapper.dart';
import 'package:namida/controller/connectivity.dart';
import 'package:namida/controller/current_color.dart';
import 'package:namida/controller/miniplayer_controller.dart';
import 'package:namida/controller/navigator_controller.dart';
import 'package:namida/controller/platform/namida_channel/namida_channel.dart';
import 'package:namida/controller/platform/shortcuts_manager/shortcuts_manager.dart';
import 'package:namida/controller/player_controller.dart';
import 'package:namida/controller/playlist_controller.dart';
import 'package:namida/controller/settings_controller.dart';
import 'package:namida/controller/shortcuts_controller.dart';
import 'package:namida/controller/time_ago_controller.dart';
import 'package:namida/controller/version_controller.dart';
import 'package:namida/controller/video_controller.dart';
import 'package:namida/controller/waveform_controller.dart';
import 'package:namida/controller/window_controller.dart';
import 'package:namida/core/constants.dart';
import 'package:namida/core/dimensions.dart';
import 'package:namida/core/enums.dart';
import 'package:namida/core/extensions.dart';
import 'package:namida/core/functions.dart';
import 'package:namida/core/icon_fonts/broken_icons.dart';
import 'package:namida/core/namida_converter_ext.dart';
import 'package:namida/core/translations/language.dart';
import 'package:namida/core/utils.dart';
import 'package:namida/packages/scroll_physics_modified.dart';
import 'package:namida/packages/smooth_scroll_controller.dart';
import 'package:namida/ui/dialogs/edit_tags_dialog.dart';
import 'package:namida/ui/pages/about_page.dart';
import 'package:namida/ui/pages/settings_page.dart';
import 'package:namida/ui/widgets/animated_widgets.dart';
import 'package:namida/ui/widgets/custom_tooltip.dart';
import 'package:namida/ui/widgets/library/track_tile.dart';
import 'package:namida/ui/widgets/popup_wrapper.dart';
import 'package:namida/ui/widgets/settings/extra_settings.dart';
import 'package:namida/youtube/class/youtube_id.dart';
import 'package:namida/youtube/controller/youtube_info_controller.dart';

import 'custom_reorderable_list.dart';

export 'popup_wrapper.dart';

part 'smooth_scroll.dart';

class NamidaReordererableListener extends StatelessWidget {
  final int index;
  final int durationMs;
  final Widget child;

  const NamidaReordererableListener({
    super.key,
    required this.index,
    this.durationMs = 50,
    required this.child,
  });

  @override
  Widget build(BuildContext context) {
    return ReorderableDelayedDragStartListener(
      index: index,
      delay: isDesktop ? Duration.zero : Duration(milliseconds: durationMs),
      child: child,
    );
  }
}

class CustomSwitch extends StatelessWidget {
  final bool active;
  final double height;
  final double width;
  final Color? circleColor;
  final Color? bgColor;
  final Color? shadowColor;
  final int durationInMillisecond;
  final Color? passedColor;

  const CustomSwitch({
    super.key,
    required this.active,
    this.height = 21.0,
    this.width = 40.0,
    this.circleColor,
    this.durationInMillisecond = 300,
    this.bgColor,
    this.shadowColor,
    this.passedColor,
  });

  @override
  Widget build(BuildContext context) {
    final finalColor = passedColor ?? CurrentColor.inst.color;
    final theme = context.theme;
    final duration = Duration(milliseconds: durationInMillisecond);
    final curve = Curves.fastEaseInToSlowEaseOut;
    return SizedBox(
      width: width,
      height: height,
      child: AnimatedDecoration(
        duration: duration,
        curve: curve,
        decoration: BoxDecoration(
          color: (active
              ? bgColor ?? Color.alphaBlend(finalColor.withAlpha(180), theme.colorScheme.surface).withAlpha(140)
              // : theme.scaffoldBackgroundColor.withAlpha(34)
              : Color.alphaBlend(theme.scaffoldBackgroundColor.withAlpha(60), theme.disabledColor)),
          borderRadius: BorderRadius.circular(30.0.multipliedRadius),
          boxShadow: [
            BoxShadow(
              offset: const Offset(0, 2),
              blurRadius: active ? 8 : 2,
              spreadRadius: 0,
              color: (shadowColor ?? Color.alphaBlend(finalColor.withAlpha(180), theme.colorScheme.surface)).withOpacityExt(active ? 0.8 : 0.3),
            ),
          ],
        ),
        child: Padding(
          padding: EdgeInsets.symmetric(horizontal: width / 10),
          child: AnimatedAlign(
            duration: duration,
            curve: curve,
            alignment: active ? Alignment.centerRight : Alignment.centerLeft,
            child: SizedBox(
              width: width / 3,
              height: height / 1.5,
              child: DecoratedBox(
                decoration: BoxDecoration(
                  color: circleColor ?? Colors.white.withAlpha(222),
                  borderRadius: BorderRadius.circular(30.0.multipliedRadius),
                  // boxShadow: [
                  //   BoxShadow(color: Colors.black.withAlpha(100), spreadRadius: 1, blurRadius: 4, offset: Offset(0, 2)),
                  // ],
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class CustomSwitchListTile extends StatelessWidget {
  final bool value;
  final void Function(bool isTrue) onChanged;
  final String title;
  final String? subtitle;
  final Widget? leading;
  final IconData? icon;
  final Color? passedColor;
  final int? rotateIcon;
  final bool enabled;
  final bool largeTitle;
  final int maxSubtitleLines;
  final VisualDensity? visualDensity;
  final Color? bgColor;
  final bool dense;
  final bool extraDense;

  const CustomSwitchListTile({
    super.key,
    required this.value,
    required this.onChanged,
    required this.title,
    this.subtitle,
    this.leading,
    this.icon,
    this.passedColor,
    this.rotateIcon,
    this.enabled = true,
    this.largeTitle = false,
    this.maxSubtitleLines = 8,
    this.visualDensity,
    this.bgColor,
    this.dense = true,
    this.extraDense = false,
  });

  @override
  Widget build(BuildContext context) {
    return CustomListTile(
      bgColor: bgColor,
      title: title,
      subtitle: subtitle,
      enabled: enabled,
      icon: icon,
      leading: leading,
      largeTitle: largeTitle,
      maxSubtitleLines: maxSubtitleLines,
      passedColor: passedColor,
      rotateIcon: rotateIcon,
      onTap: () => onChanged(value),
      dense: dense,
      extraDense: extraDense,
      trailing: IgnorePointer(
        child: FittedBox(
          child: Row(
            children: [
              const SizedBox(
                width: 12.0,
              ),
              CustomSwitch(active: value, passedColor: passedColor),
            ],
          ),
        ),
      ),
    );
  }
}

class CustomListTile extends StatelessWidget {
  final void Function()? onTap;
  final String title;
  final String? subtitle;
  final Widget? trailing;
  final Widget? trailingRaw;
  final String? trailingText;
  final IconData? icon;
  final Widget? leading;
  final Color? passedColor;
  final int? rotateIcon;
  final bool enabled;
  final bool largeTitle;
  final int? maxTitleLines;
  final int maxSubtitleLines;
  final TextStyle? titleStyle;
  final double borderR;
  final Color? bgColor;
  final bool dense;
  final bool extraDense;

  const CustomListTile({
    super.key,
    required this.title,
    this.subtitle,
    this.trailing,
    this.trailingRaw,
    this.trailingText,
    this.onTap,
    this.leading,
    this.icon,
    this.passedColor,
    this.rotateIcon,
    this.enabled = true,
    this.largeTitle = false,
    this.maxTitleLines,
    this.maxSubtitleLines = 8,
    this.titleStyle,
    this.borderR = 18.0,
    this.bgColor,
    this.dense = true,
    this.extraDense = false,
  });

  @override
  Widget build(BuildContext context) {
    final theme = context.theme;
    final textTheme = theme.textTheme;
    final iconColor = context.defaultIconColor(passedColor);

    final baseDensity = VisualDensity.compact.baseSizeAdjustment;
    var minTileHeight =
        6.0 +
        baseDensity.dy +
        switch ((subtitle?.isNotEmpty == true)) {
          true => dense ? 60.0 : 62.0, // 2 lines
          false => dense ? 50.0 : 56.0, // 1 line
        };
    var verticalPadding = 8.0;
    var borderR = this.borderR;
    var iconSize = 24.0;

    if (this.extraDense) {
      minTileHeight *= 0.85;
      verticalPadding = 1.0;
      borderR *= 0.8;
    }

    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 1.0),
      child: AnimatedOpacity(
        duration: const Duration(milliseconds: 400),
        opacity: enabled ? 1.0 : 0.5,
        child: ConstrainedBox(
          constraints: BoxConstraints(minHeight: minTileHeight),
          child: NamidaInkWell(
            onTap: enabled ? onTap : null,
            decoration: BoxDecoration(
              color: bgColor,
              borderRadius: BorderRadius.circular(borderR.multipliedRadius),
            ),
            padding: const EdgeInsets.symmetric(horizontal: 16.0),
            child: NamidaMouseRegion(
              enabled: enabled && onTap != null,
              child: Row(
                children: [
                  if (icon != null) ...[
                    rotateIcon != null
                        ? RotatedBox(
                            quarterTurns: rotateIcon!,
                            child: Icon(
                              icon,
                              color: iconColor,
                              size: iconSize,
                            ),
                          )
                        : Icon(
                            icon,
                            color: iconColor,
                            size: iconSize,
                          ),
                    const SizedBox(width: 12.0),
                  ] else if (leading != null) ...[
                    leading!,
                    const SizedBox(width: 12.0),
                  ],

                  Expanded(
                    child: Padding(
                      padding: EdgeInsets.symmetric(vertical: verticalPadding),
                      child: Column(
                        mainAxisSize: MainAxisSize.min,
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text(
                            title,
                            style: titleStyle ?? (largeTitle ? theme.textTheme.displayLarge : theme.textTheme.displayMedium),
                            maxLines: maxTitleLines ?? (subtitle != null ? 4 : 5),
                            overflow: TextOverflow.ellipsis,
                          ),
                          if (subtitle?.isNotEmpty == true) ...[
                            const SizedBox(height: 2.0),
                            Text(
                              subtitle!,
                              style: theme.textTheme.displaySmall,
                              maxLines: maxSubtitleLines,
                              overflow: TextOverflow.ellipsis,
                            ),
                          ],
                        ],
                      ),
                    ),
                  ),

                  const SizedBox(width: 6.0),

                  const SizedBox(width: 12.0),

                  if (trailingRaw != null) ...[
                    trailingRaw!,
                  ] else if (trailing != null || trailingText != null) ...[
                    ConstrainedBox(
                      constraints: BoxConstraints(
                        minWidth: 0,
                        maxWidth: Dimensions.inst.availableAppContentWidth * 0.3,
                        maxHeight: minTileHeight,
                      ),
                      child: FittedBox(
                        fit: BoxFit.scaleDown,
                        child: trailingText != null
                            ? Text(
                                trailingText!,
                                style: textTheme.displayMedium?.copyWith(color: theme.colorScheme.onSurface.withAlpha(200)),
                                maxLines: 1,
                                overflow: TextOverflow.ellipsis,
                                textAlign: TextAlign.center,
                              )
                            : trailing!,
                      ),
                    ),
                  ],
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

/// Blurs a child, effective for performance
class NamidaBlur extends StatelessWidget {
  final double blur;
  final bool enabled;
  final TileMode? tileMode;
  final Widget child;

  const NamidaBlur({
    super.key,
    required this.blur,
    this.enabled = true,
    bool fixArtifacts = false,
    required this.child,
  }) : tileMode = fixArtifacts ? TileMode.decal : NamidaBlur.kDefaultTileMode;

  static const kDefaultTileMode = TileMode.clamp;

  @override
  Widget build(BuildContext context) {
    return ImageFiltered(
      enabled: enabled && blur > 0,
      imageFilter: ImageFilter.blur(sigmaX: blur, sigmaY: blur, tileMode: tileMode),
      child: child,
    );
  }
}

/// Blurs a background behind a child, demanding/expensive for performance.
class NamidaBgBlur extends StatelessWidget {
  final double blur;
  final bool enabled;
  final bool disableIfBlur0;
  final Widget child;

  const NamidaBgBlur({
    super.key,
    required this.blur,
    this.enabled = true,
    this.disableIfBlur0 = true,
    required this.child,
  });

  static final _groupKey = BackdropKey();

  @override
  Widget build(BuildContext context) {
    if (!enabled || (disableIfBlur0 && blur == 0)) return child;
    Widget blurredWidget = BackdropFilter(
      backdropGroupKey: _groupKey,
      filter: ImageFilter.blur(sigmaX: blur, sigmaY: blur, tileMode: NamidaBlur.kDefaultTileMode),
      child: child,
    );
    final topArea = WindowController.instance?.windowTitleBarHeightIfActive;
    if (topArea != null) {
      // -- ensure window title bar is not blurred
      blurredWidget = ClipRect(
        child: blurredWidget,
      );
    }
    return blurredWidget;
  }
}

/// Same as [NamidaBgBlur] but with more configurations like clipping & decoration
class NamidaBgBlurClipped extends StatelessWidget {
  final double blur;
  final bool enabled;
  final Clip clipBehavior;
  final BoxShape shape;
  final BoxDecoration? decoration;
  final BorderRadiusGeometry? borderRadius;
  final Widget child;

  const NamidaBgBlurClipped({
    super.key,
    required this.blur,
    this.enabled = true,
    this.shape = BoxShape.rectangle,
    this.clipBehavior = Clip.antiAlias,
    this.decoration,
    this.borderRadius,
    required this.child,
  });

  @override
  Widget build(BuildContext context) {
    final decoration = this.decoration;

    if (!enabled || blur == 0) {
      return decoration != null
          ? DecoratedBox(
              decoration: decoration,
              child: child,
            )
          : borderRadius != null && shape != BoxShape.rectangle
          ? DecoratedBox(
              decoration: BoxDecoration(
                borderRadius: borderRadius,
                shape: decoration?.shape ?? BoxShape.rectangle,
              ),
            )
          : child;
    }

    return ClipPath(
      clipBehavior: clipBehavior,
      clipper: DecorationClipper(
        decoration: BoxDecoration(
          borderRadius: decoration?.borderRadius ?? borderRadius,
          shape: decoration?.shape ?? shape,
        ),
      ),
      child: NamidaBgBlur(
        blur: blur,
        enabled: enabled,
        child: decoration != null
            ? DecoratedBox(
                decoration: BoxDecoration(
                  borderRadius: decoration.borderRadius,
                  shape: decoration.shape,
                  backgroundBlendMode: decoration.backgroundBlendMode,
                  border: decoration.border,
                  boxShadow: decoration.boxShadow,
                  color: decoration.color,
                  gradient: decoration.gradient,
                  image: decoration.image,
                ),
                child: child,
              )
            : child,
      ),
    );
  }
}

class DropShadow extends StatelessWidget {
  final Widget child;
  final Widget? bottomChild;
  final double blurRadius;
  final double bgSizePercentage;
  final double sizePercentage;
  final Offset offset;

  const DropShadow({
    required this.child,
    this.bottomChild,
    this.blurRadius = 10.0,
    this.offset = const Offset(0, 4),
    this.bgSizePercentage = defaultBgSizePercentage,
    this.sizePercentage = defaultSizePercentage,
    super.key,
  });

  static const defaultBgSizePercentage = 0.925;
  static const defaultSizePercentage = 0.95;

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        Transform.scale(
          scale: bgSizePercentage,
          child: Transform.translate(
            offset: offset,
            child: ImageFiltered(
              imageFilter: ImageFilter.blur(
                sigmaX: blurRadius,
                sigmaY: blurRadius,
                tileMode: TileMode.decal,
              ),
              child: bottomChild ?? child,
            ),
          ),
        ),
        sizePercentage == 1.0
            ? child
            : Transform.scale(
                scale: sizePercentage,
                child: child,
              ),
      ],
    );
  }
}

class CustomBlurryDialog extends StatelessWidget {
  final IconData? icon;
  final String? title;
  final Widget? titleWidget;
  final Widget? titleWidgetInPadding;
  final List<Widget>? trailingWidgets;
  final Widget? child;
  final List<Widget>? actions;
  final Widget? leftAction;
  final bool normalTitleStyle;
  final String? bodyText;
  final bool isWarning;
  final bool scrollable;
  final double horizontalInset;
  final double verticalInset;
  final EdgeInsetsGeometry contentPadding;
  final ThemeData? theme;

  const CustomBlurryDialog({
    super.key,
    this.child,
    this.trailingWidgets,
    this.title,
    this.titleWidget,
    this.titleWidgetInPadding,
    this.actions,
    this.icon,
    this.normalTitleStyle = false,
    this.bodyText,
    this.isWarning = false,
    this.horizontalInset = 50.0,
    this.verticalInset = 32.0,
    this.scrollable = true,
    this.contentPadding = const EdgeInsets.all(14.0),
    this.leftAction,
    this.theme,
  });

  @override
  Widget build(BuildContext context) {
    final ctxth = theme ?? context.theme;
    final vInsets = verticalInset;
    final double horizontalMargin = Dimensions.calculateDialogHorizontalMargin(context, horizontalInset);
    return Center(
      child: SmoothSingleChildScrollView(
        child: Dialog(
          backgroundColor: ctxth.dialogTheme.backgroundColor,
          surfaceTintColor: Colors.transparent,
          insetPadding: EdgeInsets.symmetric(horizontal: horizontalMargin, vertical: vInsets),
          clipBehavior: Clip.antiAlias,
          child: ConstrainedBox(
            constraints: BoxConstraints(maxWidth: kDialogMaxWidth),
            child: TapDetector(
              onTap: () {},
              child: Container(
                color: Colors.transparent,
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    /// Title.
                    ?titleWidget,
                    if (titleWidgetInPadding != null)
                      Padding(
                        padding: const EdgeInsets.only(top: 28.0, left: 28.0, right: 24.0),
                        child: Align(
                          alignment: Alignment.centerLeft,
                          child: titleWidgetInPadding,
                        ),
                      ),
                    if (titleWidget == null && titleWidgetInPadding == null)
                      normalTitleStyle
                          ? Padding(
                              padding: const EdgeInsets.only(top: 28.0, left: 28.0, right: 24.0),
                              child: Row(
                                children: [
                                  if (icon != null || isWarning) ...[
                                    Icon(
                                      isWarning ? Broken.warning_2 : icon,
                                    ),
                                    const SizedBox(
                                      width: 10.0,
                                    ),
                                  ],
                                  Expanded(
                                    child: Text(
                                      isWarning ? lang.warning : title ?? '',
                                      style: ctxth.textTheme.displayLarge,
                                    ),
                                  ),
                                  ...?trailingWidgets,
                                ],
                              ),
                            )
                          : Container(
                              color: Color.alphaBlend(ctxth.colorScheme.primary.withOpacityExt(0.02), ctxth.cardTheme.color!),
                              padding: const EdgeInsets.all(16),
                              child: Row(
                                mainAxisAlignment: MainAxisAlignment.center,
                                children: [
                                  if (icon != null) ...[
                                    Icon(
                                      icon,
                                    ),
                                    const SizedBox(
                                      width: 10.0,
                                    ),
                                  ],
                                  Expanded(
                                    child: Text(
                                      title ?? '',
                                      style: ctxth.textTheme.displayMedium,
                                      textAlign: TextAlign.center,
                                    ),
                                  ),
                                ],
                              ),
                            ),

                    /// Body.
                    Padding(
                      padding: contentPadding,
                      child: SizedBox(
                        width: context.width,
                        child: bodyText != null
                            ? Padding(
                                padding: const EdgeInsets.all(12.0),
                                child: Text(
                                  bodyText!,
                                  style: ctxth.textTheme.displayMedium,
                                ),
                              )
                            : child,
                      ),
                    ),

                    /// Actions.
                    if (actions != null)
                      Padding(
                        padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
                        child: LayoutWidthProvider(
                          builder: (context, maxWidth) {
                            final leftActionMaxWidth = leftAction == null ? 0.0 : maxWidth * 0.35;
                            final actionsMaxWidth = (maxWidth - leftActionMaxWidth);
                            return SizedBox(
                              width: maxWidth,
                              child: Row(
                                mainAxisSize: .max,
                                children: [
                                  if (leftAction != null) ...[
                                    ConstrainedBox(
                                      constraints: BoxConstraints(maxWidth: leftActionMaxWidth),
                                      child: FittedBox(
                                        alignment: AlignmentDirectional.centerStart,
                                        fit: BoxFit.scaleDown,
                                        child: leftAction!,
                                      ),
                                    ),
                                    const SizedBox(width: 6.0),
                                  ],

                                  Expanded(
                                    child: ConstrainedBox(
                                      constraints: BoxConstraints(maxWidth: actionsMaxWidth),
                                      child: FittedBox(
                                        alignment: AlignmentDirectional.centerEnd,
                                        fit: BoxFit.scaleDown,
                                        child: Row(
                                          mainAxisSize: .max,
                                          mainAxisAlignment: .end,
                                          children: [
                                            ...actions!.addSeparators(separator: const SizedBox(width: 6.0), skipFirst: 1),
                                          ],
                                        ),
                                      ),
                                    ),
                                  ),
                                ],
                              ),
                            );
                          },
                        ),
                      ),
                  ],
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class NamidaButtonText extends Text {
  const NamidaButtonText(
    super.data, {
    super.key,
    TextStyle? style,
    super.softWrap,
    super.overflow,
  }) : super(style: style ?? const TextStyle(fontSize: 15.0));
}

class NamidaFABButton extends StatelessWidget {
  final IconData icon;
  final String? text;
  final String Function()? tooltip;
  final void Function() onTap;
  final void Function()? onLongPress;
  final bool? enabled;
  final bool big;
  final bool dim;

  const NamidaFABButton({
    super.key,
    required this.icon,
    this.text,
    this.tooltip,
    required this.onTap,
    this.onLongPress,
    this.enabled,
    this.big = false,
    this.dim = false,
  });

  @override
  Widget build(BuildContext context) {
    final size = big ? kFABSize * 1.25 : kFABSize;
    final iconSize = big ? 28.0 : 22.0;
    final fontSizeMultiplier = big ? 1.15 : 1.0;
    return NamidaButton(
      tooltip: tooltip,
      minHeight: size,
      minWidth: size,
      fontSizeMultiplier: fontSizeMultiplier,
      icon: icon,
      text: text,
      enabled: enabled,
      opaqueBG: true,
      isMinimumSquared: text == null,
      colors: dim ? NamidaButtonColors.dimmed : NamidaButtonColors.fab,
      iconSize: iconSize,
      onTap: onTap,
      onLongPress: onLongPress,
      tooltipPreferBelow: false,
    );
  }
}

class NamidaTextButton extends StatelessWidget {
  final String text;
  final double? minHeight;
  final bool enabled;
  final double fontSizeMultiplier;
  final void Function() onTap;

  const NamidaTextButton({
    super.key,
    required this.text,
    this.minHeight,
    this.enabled = true,
    this.fontSizeMultiplier = 1.0,
    required this.onTap,
  });

  static const double kDefaultMinHeight = NamidaButton.kDefaultMinHeight;

  @override
  Widget build(BuildContext context) {
    return NamidaButton(
      onTap: onTap,
      enabled: enabled,
      text: text,
      fontSizeMultiplier: fontSizeMultiplier,
      minHeight: minHeight,
      dense: true,
      colors: NamidaButtonColors.dimmed,
    );
  }
}

enum NamidaButtonColors {
  selected,
  fab,
  saturated,
  normal,
  mid,
  dimmed,
  ;

  static const defaultValue = normal;
}

class NamidaButton extends StatelessWidget {
  final IconData? icon;
  final IconData? secondaryIcon;
  final double? iconSize;
  final String? text;
  final double fontSizeMultiplier;
  final Color? colorScheme;
  final double? minHeight;
  final double? minWidth;
  final double? borderRadius;
  final String Function()? tooltip;
  final void Function() onTap;
  final void Function()? onLongPress;
  final bool? enabled;
  final bool? isLoading;
  final bool dense;
  final NamidaButtonColors colors;
  final List<BoxShadow>? boxShadow;
  final bool opaqueBG;
  final bool animatedDecoration;
  final bool isCircle;
  final bool isMinimumSquared;
  final bool? tooltipPreferBelow;
  final bool isCentered;

  const NamidaButton({
    super.key,
    this.icon,
    this.secondaryIcon,
    this.iconSize = 20.0,
    this.text,
    this.fontSizeMultiplier = 1.0,
    this.colorScheme,
    this.minHeight,
    this.minWidth,
    this.borderRadius,
    this.tooltip,
    required this.onTap,
    this.onLongPress,
    this.enabled,
    this.isLoading,
    this.dense = false,
    this.colors = NamidaButtonColors.normal,
    this.boxShadow,
    this.opaqueBG = false,
    this.animatedDecoration = false,
    this.isCircle = false,
    this.isMinimumSquared = false,
    this.tooltipPreferBelow,
    this.isCentered = true,
  });

  static const double kDefaultMinHeight = 36.0;

  @override
  Widget build(BuildContext context) {
    final (double colorSchemeOpacity, double bgOpacity, double borderOpacity, double foregroundOpacity) = switch (colors) {
      NamidaButtonColors.selected => (0.8, 0.8, 0.8, 0.8),
      NamidaButtonColors.fab => (0.5, 0.2, 0.6, 0.85),
      NamidaButtonColors.saturated => (0.4, 0.2, 0.6, 0.85),
      NamidaButtonColors.normal => (0.3, 0.2, 0.6, 0.85),
      NamidaButtonColors.mid => (0.2, 0.2, 0.6, 0.85),
      NamidaButtonColors.dimmed => (0.1, 0.2, 0.6, 0.85),
    };

    final colorScheme = (this.colorScheme ?? context.theme.colorScheme.primary).withOpacityExt(colorSchemeOpacity);

    bool whiteForeground = switch (colors) {
      .selected => true,
      _ => false,
    };

    var bgColor = colorScheme.withOpacityExt(colorScheme.a * bgOpacity);
    if (opaqueBG) bgColor = Color.alphaBlend(bgColor, context.theme.scaffoldBackgroundColor);
    final borderColor = colorScheme.withOpacityExt(colorScheme.a * borderOpacity);
    final foregroundColor = (whiteForeground ? Colors.white : context.defaultIconColor(colorScheme)).withOpacityExt(foregroundOpacity);

    final text = this.text;
    final icon = this.icon;
    final iconSize = this.iconSize;
    final secondaryIcon = this.secondaryIcon;
    final enabled = this.enabled;
    final isLoading = this.isLoading;

    final iconChild = (icon == null
        ? null
        : secondaryIcon != null
        ? StackedIcon(
            disableColor: true,
            baseIcon: icon,
            iconSize: iconSize,
            baseIconColor: foregroundColor,
            secondaryIconColor: foregroundColor,
            secondaryIcon: secondaryIcon,
            secondaryIconSize: 13.0,
          )
        : Icon(
            icon,
            size: iconSize,
            color: foregroundColor,
          ));

    final textStyle = context.textTheme.displayMedium?.copyWith(
      color: foregroundColor,
      fontSize: fontSizeMultiplier * (dense ? 15.0 : 15.5),
    );

    final textChild = (text == null || text.isEmpty
        ? null
        : Text(
            text,
            style: textStyle,
            softWrap: false,
            overflow: TextOverflow.ellipsis,
          ));

    EdgeInsets padding;
    double minHeight = this.minHeight ?? kDefaultMinHeight;

    if (isCircle || isMinimumSquared) {
      padding = EdgeInsets.symmetric(
        horizontal: 12.0,
        vertical: 12.0,
      );
    } else if (dense) {
      padding = EdgeInsets.symmetric(
        horizontal: textChild != null
            ? iconChild != null
                  ? 10.0
                  : 12.0
            : iconChild != null
            ? 10.0
            : 8.0,
        vertical: 8.0,
      );
    } else {
      padding = EdgeInsets.symmetric(
        horizontal: textChild != null
            ? iconChild != null
                  ? 18.0
                  : 24.0
            : iconChild != null
            ? 16.0
            : 12.0,
        vertical: 8.0,
      );
    }

    if (!dense && textChild != null && iconChild == null) {
      padding *= 1.25;
    }

    final brRaw = borderRadius ?? (isCircle ? 99.0 : 20.0);
    Widget box = NamidaTooltip(
      message: tooltip,
      preferBelow: tooltipPreferBelow,
      child: ConstrainedBox(
        constraints: BoxConstraints(minHeight: minHeight, minWidth: isCircle || isMinimumSquared ? minHeight : 0.0),
        child: NamidaInkWell(
          animationDurationMS: animatedDecoration ? 300 : 0,
          borderRadius: brRaw,
          onTap: onTap,
          onLongPress: onLongPress,
          decoration: BoxDecoration(
            color: bgColor,
            border: Border.all(
              width: 0.5,
              color: borderColor,
            ),
            boxShadow: boxShadow,
          ),
          padding: padding,
          child: Row(
            mainAxisAlignment: isCentered ? MainAxisAlignment.center : MainAxisAlignment.start,
            mainAxisSize: .min,
            children: [
              ?iconChild,
              if (iconChild != null && textChild != null) const SizedBox(width: 8.0),
              if (textChild != null)
                Flexible(
                  child: textChild,
                ),
            ],
          ),
        ),
      ),
    );

    if (isLoading != null) {
      box = AnimatedRotatingBorder(
        isLoading: isLoading,
        duration: const Duration(milliseconds: 1200),
        borderRadius: brRaw.multipliedRadius,
        colors: [
          colorScheme,
        ],
        child: box,
      );
    }

    if (enabled != null) {
      box = IgnorePointer(
        ignoring: !enabled,
        child: AnimatedOpacity(
          opacity: enabled ? 1.0 : 0.6,
          duration: const Duration(milliseconds: 250),
          child: box,
        ),
      );

      if (opaqueBG) {
        box = IgnorePointer(
          ignoring: !enabled,
          child: Stack(
            children: [
              Positioned.fill(
                child: IgnorePointer(
                  child: DecoratedBox(
                    decoration: BoxDecoration(
                      borderRadius: BorderRadius.circular(brRaw.multipliedRadius),
                      color: bgColor,
                    ),
                  ),
                ),
              ),
              box,
            ],
          ),
        );
      }
    }

    return box;
  }
}

class StatsContainer extends StatelessWidget {
  final Widget? child;
  final Widget? leading;
  final IconData? icon;
  final String? title;
  final String? value;
  final String? total;

  const StatsContainer({
    super.key,
    this.child,
    this.leading,
    this.icon,
    this.title,
    this.value,
    this.total,
  });

  @override
  Widget build(BuildContext context) {
    final theme = context.theme;
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
      decoration: BoxDecoration(
        color: theme.cardTheme.color?.withAlpha(200),
        borderRadius: BorderRadius.circular(18.0.multipliedRadius),
      ),
      margin: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 5.0),
      child:
          child ??
          Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              leading ??
                  Icon(
                    icon,
                    color: context.defaultIconColor(),
                  ),
              const SizedBox(width: 8.0),
              Text(title ?? ''),
              const SizedBox(width: 8.0),
              Text(value ?? ''),
              if (total != null) Text(" ${lang.ofLabel} $total"),
            ],
          ),
    );
  }
}

class SmallListTile extends StatelessWidget {
  final String title;
  final String? subtitle;
  final Widget? trailing;
  final IconData? icon;
  final IconData? trailingIcon;
  final bool active;
  final bool displayAnimatedCheck;
  final bool compact;
  final Color? color;
  final double? iconSize;
  final void Function()? onTap;
  final void Function()? onLongPress;
  final EdgeInsetsGeometry? padding;
  final double? titleGap;
  final double borderRadius;
  final Widget? leading;
  final VisualDensity? visualDensity;

  const SmallListTile({
    super.key,
    required this.title,
    this.onTap,
    this.onLongPress,
    this.trailing,
    this.active = false,
    this.icon,
    this.trailingIcon,
    this.displayAnimatedCheck = false,
    this.compact = true,
    this.subtitle,
    this.color,
    this.iconSize,
    this.padding,
    this.titleGap,
    this.borderRadius = 0.0,
    this.leading,
    this.visualDensity,
  });

  @override
  Widget build(BuildContext context) {
    final theme = context.theme;
    final textTheme = theme.textTheme;
    final iconColor = color != null ? context.defaultIconColor(color, textTheme.displayMedium?.color) : null;
    final padding = this.padding ?? (icon != null ? const EdgeInsets.only(left: 16.0, right: 12.0) : const EdgeInsets.only(left: 12.0, right: 12.0));
    return ListTile(
      contentPadding: padding,
      horizontalTitleGap: titleGap,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(borderRadius.multipliedRadius),
      ),
      dense: false,
      leading:
          leading ??
          SizedBox(
            height: double.infinity,
            child: icon != null
                ? Icon(
                    icon,
                    color: iconColor,
                    size: iconSize,
                  )
                : Stack(
                    children: [
                      AnimatedShow(
                        show: active,
                        isHorizontal: true,
                        child: const Icon(
                          Broken.arrow_circle_right,
                          size: 20.0,
                        ),
                      ),
                      AnimatedShow(
                        show: !active,
                        isHorizontal: true,
                        child: const Icon(
                          Broken.arrow_right_3,
                          size: 18.0,
                        ),
                      ),
                    ],
                  ),
          ),
      visualDensity: visualDensity ?? (compact ? const VisualDensity(horizontal: -2.2, vertical: -2.2) : const VisualDensity(horizontal: -1.2, vertical: -1.2)),
      title: Text(
        title,
        style: textTheme.displayMedium?.copyWith(
          color: color != null
              ? Color.alphaBlend(
                  color!.withAlpha(40),
                  textTheme.displayMedium!.color!,
                )
              : null,
        ),
      ),
      subtitle: subtitle != null
          ? Text(
              subtitle!,
              style: textTheme.displaySmall?.copyWith(
                color: color != null
                    ? Color.alphaBlend(
                        color!.withAlpha(40),
                        textTheme.displayMedium!.color!,
                      )
                    : null,
              ),
            )
          : null,
      trailing: displayAnimatedCheck
          ? NamidaCheckMark(
              size: 18.0,
              activeColor: color,
              inactiveColor: color,
              active: settings.artistSortReversed.value,
            )
          : trailingIcon != null
          ? Icon(
              trailingIcon,
              color: color,
              size: 18.0,
            )
          : trailing,
      onTap: onTap,
      onLongPress: onLongPress,
    );
  }
}

class ListTileWithCheckMark extends StatelessWidget {
  final bool active;
  final RxBase<bool>? activeRx;
  final void Function()? onTap;
  final String? title;
  final String subtitle;
  final IconData? icon;
  final Color? tileColor;
  final Widget? titleWidget;
  final Widget? leading;
  final double? iconSize;
  final bool dense;
  final bool expanded;
  final double borderRadius;

  const ListTileWithCheckMark({
    super.key,
    this.active = false,
    this.activeRx,
    this.onTap,
    this.title,
    this.subtitle = '',
    this.icon = Broken.arrange_circle,
    this.tileColor,
    this.titleWidget,
    this.leading,
    this.iconSize,
    this.dense = false,
    this.expanded = true,
    this.borderRadius = 14.0,
  });

  @override
  Widget build(BuildContext context) {
    final theme = context.theme;
    final textTheme = theme.textTheme;
    final tileAlpha = context.isDarkMode ? 5 : 20;
    final br = BorderRadius.circular(borderRadius.multipliedRadius);
    final titleWidgetFinal = Padding(
      padding: EdgeInsets.symmetric(horizontal: dense ? 10.0 : 14.0),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          titleWidget ??
              Text(
                title ?? lang.reverseOrder,
                style: textTheme.displayMedium,
              ),
          if (subtitle != '')
            Text(
              subtitle,
              style: textTheme.displaySmall,
            ),
        ],
      ),
    );
    return Material(
      borderRadius: br,
      color: tileColor ?? Color.alphaBlend(theme.colorScheme.onSurface.withAlpha(tileAlpha), theme.cardTheme.color!),
      child: InkWell(
        borderRadius: br,
        onTap: onTap,
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
          child: Row(
            mainAxisSize: expanded ? MainAxisSize.max : MainAxisSize.min,
            children: [
              if (leading != null)
                leading!
              else if (icon != null)
                Icon(
                  icon,
                  size: iconSize,
                ),
              expanded
                  ? Expanded(
                      child: titleWidgetFinal,
                    )
                  : Flexible(
                      child: titleWidgetFinal,
                    ),
              activeRx != null
                  ? ObxO(
                      rx: activeRx!,
                      builder: (context, active) => NamidaCheckMark(
                        size: 18.0,
                        active: active,
                      ),
                    )
                  : NamidaCheckMark(
                      size: 18.0,
                      active: active,
                    ),
            ],
          ),
        ),
      ),
    );
  }
}

class NamidaCheckMark extends StatelessWidget {
  final double size;
  final bool active;
  final Color? activeColor;
  final Color? inactiveColor;

  const NamidaCheckMark({
    super.key,
    required this.size,
    required this.active,
    this.activeColor,
    this.inactiveColor,
  });

  @override
  Widget build(BuildContext context) {
    final theme = context.theme;
    return SizedBox(
      width: size,
      height: size,
      child: CheckMark(
        duration: const Duration(milliseconds: 800),
        curve: Curves.fastLinearToSlowEaseIn,
        strokeWidth: 2,
        activeColor: activeColor ?? theme.colorScheme.secondary,
        inactiveColor: inactiveColor ?? theme.colorScheme.secondary,
        active: active,
      ),
    );
  }
}

class NamidaExpansionTile extends StatefulWidget {
  final IconData? icon;
  final Color? iconColor;
  final Widget? leading;
  final double trailingIconSize;
  final String titleText;
  final Widget? subtitle;
  final String? subtitleText;
  final Color? textColor;
  final Color? textColorScheme;
  final List<Widget> children;
  final EdgeInsetsGeometry? childrenPadding;
  final bool initiallyExpanded;
  final Widget Function(Widget iconWidget)? trailingBuilder;
  final ValueChanged<bool>? onExpansionChanged;
  final bool normalRightPadding;
  final Color? bgColor;
  final bool bigahh;
  final bool compact;
  final bool borderless;

  const NamidaExpansionTile({
    super.key,
    this.icon,
    this.iconColor,
    this.leading,
    this.trailingIconSize = 20.0,
    required this.titleText,
    this.subtitle,
    this.subtitleText,
    this.textColor,
    this.textColorScheme,
    this.children = const <Widget>[],
    this.childrenPadding = EdgeInsets.zero,
    this.initiallyExpanded = false,
    this.trailingBuilder,
    this.onExpansionChanged,
    this.normalRightPadding = false,
    this.bgColor,
    this.bigahh = false,
    this.compact = true,
    this.borderless = false,
  });

  @override
  State<NamidaExpansionTile> createState() => _NamidaExpansionTileState();
}

class _NamidaExpansionTileState extends State<NamidaExpansionTile> {
  final _rotationTurns = 0.0.obs;
  void _rotateTrailingIcon([_]) {
    _rotationTurns.value += 0.5;
  }

  void _onExpansionChanged(bool expanding) {
    _rotateTrailingIcon();
    widget.onExpansionChanged?.call(expanding);
  }

  @override
  void initState() {
    super.initState();
    if (widget.initiallyExpanded) _rotateTrailingIcon();
  }

  @override
  void dispose() {
    _rotationTurns.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final textTheme = context.textTheme;
    final trailingIconWidget = IgnorePointer(
      child: ObxO(
        rx: _rotationTurns,
        builder: (context, turns) => AnimatedRotation(
          turns: turns,
          duration: const Duration(milliseconds: 300),
          curve: Curves.fastEaseInToSlowEaseOut,
          child: IconButton(
            onPressed: () {},
            icon: Icon(
              Broken.arrow_down_2,
              size: widget.trailingIconSize,
            ),
          ),
        ),
      ),
    );
    return ListTileTheme(
      dense: !widget.bigahh,
      child: ExpansionTile(
        collapsedShape: widget.borderless ? const Border() : null,
        shape: widget.borderless ? const Border() : null,
        visualDensity: widget.compact ? VisualDensity.compact : VisualDensity.comfortable,
        controlAffinity: ListTileControlAffinity.trailing,
        collapsedBackgroundColor: widget.bgColor,
        backgroundColor: widget.bgColor,
        initiallyExpanded: widget.initiallyExpanded,
        onExpansionChanged: widget.onExpansionChanged == null ? _rotateTrailingIcon : _onExpansionChanged,
        expandedAlignment: Alignment.centerLeft,
        tilePadding: EdgeInsets.only(left: 16.0, right: widget.normalRightPadding ? 16.0 : 12.0),
        leading:
            widget.leading ??
            Icon(
              widget.icon,
              color: widget.iconColor,
            ),
        trailing: widget.trailingBuilder?.call(trailingIconWidget) ?? trailingIconWidget,
        title: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              widget.titleText,
              style: textTheme.displayMedium?.copyWith(
                color:
                    widget.textColor ??
                    (widget.textColorScheme == null
                        ? null
                        : Color.alphaBlend(
                            widget.textColorScheme!.withAlpha(40),
                            textTheme.displayMedium!.color!,
                          )),
              ),
            ),
            if (widget.subtitle != null)
              widget.subtitle!
            else if (widget.subtitleText != null)
              Text(
                widget.subtitleText!,
                style: textTheme.displaySmall,
              ),
          ],
        ),
        childrenPadding: widget.childrenPadding,
        children: widget.children,
      ),
    );
  }
}

class CreatePlaylistButton extends StatelessWidget {
  const CreatePlaylistButton({super.key});

  Future<void> promptCreate() async {
    final controller = TextEditingController();
    final exportAsM3uRx = false.obs;
    final formKey = GlobalKey<FormState>();

    await NamidaNavigator.inst.navigateDialog(
      onDisposing: () {
        controller.dispose();
        exportAsM3uRx.close();
      },
      dialog: Form(
        key: formKey,
        child: CustomBlurryDialog(
          title: lang.createNewPlaylist,
          actions: [
            const CancelButton(),
            NamidaButton(
              text: lang.save,
              onTap: () async {
                if (formKey.currentState!.validate()) {
                  final name = controller.text;
                  final m3uPath = exportAsM3uRx.value ? PlaylistController.getUnusedM3uFilePathInStorage(name) : null;

                  final pl = await PlaylistController.inst.addNewPlaylist(name, m3uPath: m3uPath);
                  if (m3uPath != null) {
                    await PlaylistController.inst.exportPlaylistToM3UFile(pl, m3uPath);
                    snackyy(
                      message: "${lang.savedIn}: $m3uPath",
                      leftBarIndicatorColor: CurrentColor.inst.color,
                      altDesign: true,
                      top: false,
                    );
                  }

                  NamidaNavigator.inst.closeDialog();
                }
              },
            ),
          ],
          child: Column(
            mainAxisSize: .min,
            children: [
              ObxO(
                rx: exportAsM3uRx,
                builder: (context, exportAsM3u) => CustomSwitchListTile(
                  icon: Broken.direct_send,
                  title: lang.convertToM3UPlaylist,
                  subtitle: AppDirs.M3UPlaylists,
                  value: exportAsM3u,
                  onChanged: (_) => exportAsM3uRx.toggle(),
                ),
              ),
              const SizedBox(height: 12.0),
              CustomTagTextField(
                controller: controller,
                hintText: lang.name,
                labelText: '',
                validator: (value) => PlaylistController.inst.validatePlaylistName(value),
              ),
            ],
          ),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return NamidaButton(
      icon: Broken.add,
      text: lang.create,
      onTap: promptCreate,
    );
  }
}

class MoreIcon extends StatelessWidget {
  final void Function()? onPressed;
  final void Function()? onLongPress;
  final bool rotated;
  final double padding;
  final Color? iconColor;
  final double iconSize;
  final bool enableSecondaryTap;

  const MoreIcon({
    super.key,
    this.onPressed,
    this.rotated = true,
    this.padding = 1.0,
    this.iconColor,
    this.iconSize = 18.0,
    this.onLongPress,
    this.enableSecondaryTap = true,
  });

  @override
  Widget build(BuildContext context) {
    return RotatedBox(
      quarterTurns: rotated ? 1 : 0,
      child: NamidaInkWell(
        borderRadius: 4.0,
        onTap: onPressed,
        onLongPress: onLongPress,
        enableSecondaryTap: enableSecondaryTap,
        padding: EdgeInsets.all(padding),
        child: Icon(
          Broken.more,
          size: iconSize,
          color: iconColor,
        ),
      ),
    );
  }
}

class StackedIcon extends StatelessWidget {
  final IconData baseIcon;
  final IconData? secondaryIcon;
  final String? secondaryText;
  final Color? baseIconColor;
  final Color? secondaryIconColor;
  final Color? shadowColor;
  final double? iconSize;
  final double? secondaryIconSize;
  final double blurRadius;
  final Widget? smallChild;
  final bool disableColor;
  final bool delightenColors;
  final double margin;

  const StackedIcon({
    super.key,
    required this.baseIcon,
    this.secondaryIcon,
    this.baseIconColor,
    this.secondaryIconColor,
    this.shadowColor,
    this.secondaryText,
    this.iconSize,
    this.secondaryIconSize = 14.0,
    this.blurRadius = 3.0,
    this.smallChild,
    this.disableColor = false,
    this.delightenColors = false,
    this.margin = -2.0,
  });

  Color? _getColory(BuildContext context, Color? c) {
    return disableColor
        ? null
        : delightenColors && c != null
        ? context.defaultIconColor(c)
        : c ?? context.defaultIconColor();
  }

  @override
  Widget build(BuildContext context) {
    final theme = context.theme;
    final textTheme = theme.textTheme;
    return Stack(
      clipBehavior: Clip.none,
      children: [
        Icon(
          baseIcon,
          color: _getColory(context, baseIconColor),
          size: iconSize,
        ),
        Positioned(
          bottom: margin,
          right: margin,
          child: DecoratedBox(
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(6),
              boxShadow: [
                BoxShadow(
                  color: shadowColor ?? theme.scaffoldBackgroundColor,
                  spreadRadius: 0,
                  blurRadius: blurRadius,
                ),
              ],
            ),
            child:
                smallChild ??
                (secondaryText != null
                    ? Text(
                        secondaryText!,
                        style: textTheme.displaySmall?.copyWith(color: _getColory(context, secondaryIconColor)),
                      )
                    : Icon(
                        secondaryIcon,
                        size: secondaryIconSize,
                        color: _getColory(context, secondaryIconColor),
                      )),
          ),
        ),
      ],
    );
  }
}

class SmallIconButton extends StatelessWidget {
  final IconData icon;
  final void Function()? onTap;

  const SmallIconButton({super.key, required this.icon, this.onTap});

  @override
  Widget build(BuildContext context) {
    return NamidaIconButton(
      icon: icon,
      onPressed: onTap,
      iconSize: 20.0,
      horizontalPadding: 0,
    );
  }
}

class CancelButtonDisabled extends StatelessWidget {
  final Rx<bool> disabledRx;
  final bool addMargin;
  final void Function()? onPressed;

  const CancelButtonDisabled({
    super.key,
    required this.disabledRx,
    this.addMargin = true,
    this.onPressed,
  });

  @override
  Widget build(BuildContext context) {
    return ObxO(
      rx: disabledRx,
      builder: (context, disabled) => CancelButtonDisabledRaw(
        disabled: disabled,
        addMargin: addMargin,
        onPressed: onPressed,
      ),
    );
  }
}

class CancelButtonDisabledRaw extends StatelessWidget {
  final bool disabled;
  final bool addMargin;
  final void Function()? onPressed;

  const CancelButtonDisabledRaw({
    super.key,
    required this.disabled,
    this.addMargin = true,
    this.onPressed,
  });

  @override
  Widget build(BuildContext context) {
    return CancelButton(
      enabled: !disabled,
      addMargin: addMargin,
      onPressed: onPressed,
    );
  }
}

class CancelButton extends StatelessWidget {
  final bool addMargin;
  final bool enabled;
  final void Function()? onPressed;

  const CancelButton({
    super.key,
    this.addMargin = true,
    this.enabled = true,
    this.onPressed,
  });

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: addMargin ? const EdgeInsetsGeometry.symmetric(horizontal: 4.0) : EdgeInsetsGeometry.zero,
      child: NamidaTextButton(
        onTap: onPressed ?? NamidaNavigator.inst.closeDialog,
        enabled: enabled,
        text: lang.cancel,
      ),
    );
  }
}

class DoneButton extends StatelessWidget {
  final bool enabled;
  final void Function()? additional;
  const DoneButton({super.key, this.enabled = true, this.additional});

  @override
  Widget build(BuildContext context) {
    return NamidaButton(
      enabled: enabled,
      text: lang.done,
      onTap: () {
        NamidaNavigator.inst.closeDialog();
        if (additional != null) additional!();
      },
    );
  }
}

class CollapsedSettingTileWidget extends StatelessWidget {
  final Color? bgColor;
  const CollapsedSettingTileWidget({super.key, this.bgColor});

  @override
  Widget build(BuildContext context) {
    Localizations.localeOf(context);
    return ObxO(
      rx: settings.useSettingCollapsedTiles,
      builder: (context, useSettingCollapsedTiles) => CustomSwitchListTile(
        bgColor: bgColor,
        icon: Broken.archive,
        title: lang.useCollapsedSettingTiles,
        value: useSettingCollapsedTiles,
        onChanged: (isTrue) async {
          settings.save(useSettingCollapsedTiles: !isTrue);
          await NamidaNavigator.inst.popPage();
          const SettingsPage().navigate();
        },
      ),
    );
  }
}

class AboutPageTileWidget extends StatelessWidget {
  const AboutPageTileWidget({super.key});

  @override
  Widget build(BuildContext context) {
    Localizations.localeOf(context);
    return CustomCollapsedListTile(
      title: () => lang.about,
      subtitle: null,
      icon: Broken.info_circle,
      page: null,
      rawPage: () => const AboutPage(),
    );
  }
}

class NamidaBlurryContainer extends StatelessWidget {
  final Widget child;
  final void Function()? onTap;
  final BorderRadius? borderRadius;
  final double? width;
  final double? height;
  final EdgeInsetsGeometry padding;

  const NamidaBlurryContainer({
    super.key,
    required this.child,
    this.onTap,
    this.borderRadius,
    this.width,
    this.height,
    this.padding = const EdgeInsets.symmetric(horizontal: 6.0, vertical: 2.0),
  });

  @override
  Widget build(BuildContext context) {
    final theme = context.theme;
    final blurredAlphaLight = context.isDarkMode ? 60 : 140;
    final blurEnabled = settings.enableBlurEffect.value;

    Widget? finalChild = ColoredBox(
      color: theme.cardColor.withAlpha(blurEnabled ? blurredAlphaLight : 220),
      child: SizedBox(
        width: width,
        height: height,
        child: Padding(
          padding: padding,
          child: child,
        ),
      ),
    );

    if (blurEnabled) {
      finalChild = NamidaBgBlur(
        blur: 5.0,
        child: finalChild,
      );
    }

    final brr = borderRadius ?? BorderRadius.only(bottomLeft: Radius.circular(8.0.multipliedRadius));

    if (blurEnabled || brr != BorderRadius.zero) {
      finalChild = ClipPath(
        clipper: DecorationClipper(
          decoration: BoxDecoration(
            borderRadius: brr,
          ),
        ),
        child: finalChild,
      );
    }

    if (onTap != null) {
      finalChild = InkWell(
        onTap: onTap,
        child: finalChild,
      );
    }
    return finalChild;
  }
}

class ContainerWithBorder extends StatelessWidget {
  final Widget? child;
  final Color? borderColor;
  const ContainerWithBorder({super.key, this.child, this.borderColor});

  @override
  Widget build(BuildContext context) {
    final theme = context.theme;
    return DecoratedBox(
      decoration: BoxDecoration(
        shape: BoxShape.circle,
        color: borderColor ?? theme.cardColor.withAlpha(160),
        boxShadow: [
          BoxShadow(
            color: theme.shadowColor.withAlpha(60),
            blurRadius: 4,
            offset: const Offset(0, 2.0),
          ),
        ],
      ),
      child: Padding(
        padding: EdgeInsetsGeometry.all(3.0),
        child: child,
      ),
    );
  }
}

class NamidaWheelSlider extends StatefulWidget {
  final double width;
  final double perspective;
  final int max;
  final int min;
  final int stepper;
  final int multiplier;
  final int initValue;
  final bool extraValue;
  final double itemSize;
  final double squeeze;
  final String? text;
  final String? topText;
  final double? textPadding;
  final double? topTextPadding;
  final void Function(int val) onValueChanged;

  const NamidaWheelSlider({
    super.key,
    this.width = 80,
    this.perspective = 0.01,
    required int initValue,
    this.extraValue = false,
    this.min = 0,
    required this.max,
    this.stepper = 1,
    this.multiplier = 1,
    required this.onValueChanged,
    this.text,
    this.topText,
    this.textPadding = 2.0,
    this.topTextPadding = 12.0,
  }) : itemSize = 8,
       squeeze = 1.8,
       initValue = initValue < min ? max + 1 : initValue - min,
       assert(min < max, 'min should be less than max');

  @override
  State<NamidaWheelSlider> createState() => _NamidaWheelSliderState();
}

class _NamidaWheelSliderState extends State<NamidaWheelSlider> {
  late final _controller = FixedExtentScrollController(initialItem: (widget.initValue / widget.stepper / widget.multiplier).round());

  static bool _isMultipleOfFive(int n) => n % 5 == 0;

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final theme = context.theme;
    final textTheme = theme.textTheme;
    final totalCount = ((widget.max - widget.min) / widget.stepper).round() + (widget.extraValue ? 1 : 0);

    return SizedBox(
      width: widget.width,
      child: Column(
        children: [
          if (widget.topText != null) ...[
            Text(
              widget.topText!,
              style: textTheme.displaySmall?.copyWith(fontWeight: FontWeight.w600),
              maxLines: 1,
              overflow: TextOverflow.ellipsis,
            ),
            SizedBox(
              height: widget.topTextPadding,
            ),
          ],
          Stack(
            alignment: Alignment.center,
            children: [
              SizedBox(
                height: 38.0,
                child: RotatedBox(
                  quarterTurns: 3,
                  child: ListWheelScrollView.useDelegate(
                    controller: _controller,
                    childDelegate: ListWheelChildBuilderDelegate(
                      childCount: totalCount + 1,
                      builder: (context, index) {
                        final multipleOfFive = _isMultipleOfFive(index);
                        return Align(
                          alignment: Alignment.center,
                          child: SizedBox(
                            width: multipleOfFive ? 35.0 : 20.0,
                            height: 1.5,
                            child: SizedBox(
                              width: multipleOfFive ? 35.0 : 20.0,
                              height: 1.5,
                              child: ColoredBox(
                                color: theme.iconTheme.color!,
                              ),
                            ),
                          ),
                        );
                      },
                    ),
                    onSelectedItemChanged: (val) {
                      int finalValue = (val * widget.stepper * widget.multiplier + widget.min);
                      if ((widget.extraValue && finalValue > widget.max)) finalValue = -1;
                      widget.onValueChanged(finalValue);
                      HapticFeedback.lightImpact();
                    },
                    perspective: widget.perspective,
                    squeeze: widget.squeeze,
                    useMagnifier: true,
                    itemExtent: widget.itemSize,
                  ),
                ),
              ),
              IgnorePointer(
                ignoring: true,
                child: SizedBox(
                  height: 38.0,
                  width: 2.5,
                  child: DecoratedBox(
                    decoration: BoxDecoration(
                      borderRadius: BorderRadius.circular(4.0.multipliedRadius),
                      color: theme.listTileTheme.textColor!,
                    ),
                  ),
                ),
              ),
            ],
          ),
          if (widget.text != null) ...[
            SizedBox(
              height: widget.textPadding,
            ),
            FittedBox(
              child: Text(
                widget.text!,
                style: TextStyle(
                  color: textTheme.displaySmall?.color,
                ),
              ),
            ),
          ],
        ],
      ),
    );
  }
}

class NamidaLoadingControllerProvider extends StatefulWidget {
  final Widget Function(NamidaLoadingController loadingController) builder;
  const NamidaLoadingControllerProvider({super.key, required this.builder});

  @override
  State<NamidaLoadingControllerProvider> createState() => _NamidaLoadingControllerProviderState();
}

class _NamidaLoadingControllerProviderState extends State<NamidaLoadingControllerProvider> {
  final _loadingController = NamidaLoadingController();

  @override
  void dispose() {
    _loadingController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return widget.builder(_loadingController);
  }
}

class NamidaLoadingController {
  NamidaLoadingController();

  void startLoading() => _startLoadingFn?.call();
  void stopLoading() => _stopLoadingFn?.call();
  bool get isLoading => _isLoading;

  void Function()? _startLoadingFn;
  void Function()? _stopLoadingFn;
  bool _isLoading = false;

  void dispose() {
    _startLoadingFn = null;
    _stopLoadingFn = null;
  }
}

class NamidaLoadingSwitcher extends StatefulWidget {
  final NamidaLoadingController? controller;
  final Widget Function(NamidaLoadingController loadingController) builder;
  final double? size;
  final bool showLoading;

  const NamidaLoadingSwitcher({
    super.key,
    this.controller,
    required this.builder,
    this.size,
    this.showLoading = true,
  });

  @override
  State<NamidaLoadingSwitcher> createState() => _NamidaLoadingSwitcherState();
}

class _NamidaLoadingSwitcherState extends State<NamidaLoadingSwitcher> {
  late final NamidaLoadingController loadingController;

  @override
  void initState() {
    loadingController = widget.controller ?? NamidaLoadingController();
    loadingController._startLoadingFn = _startLoading;
    loadingController._stopLoadingFn = _stopLoading;
    loadingController._isLoading = false;
    super.initState();
  }

  void _startLoading() {
    if (mounted) setState(() => loadingController._isLoading = true);
  }

  void _stopLoading() {
    if (mounted) setState(() => loadingController._isLoading = false);
  }

  @override
  void dispose() {
    if (widget.controller == null) loadingController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final child = widget.builder(loadingController);
    final isLoading = loadingController.isLoading;
    return Stack(
      fit: StackFit.loose,
      alignment: Alignment.center,
      children: [
        AnimatedOpacity(
          opacity: isLoading ? 0.5 : 1.0,
          duration: const Duration(milliseconds: 200),
          child: child,
        ),
        if (isLoading && widget.showLoading)
          IgnorePointer(
            child: AnimatedOpacity(
              opacity: isLoading ? 0.8 : 0.0,
              duration: const Duration(milliseconds: 200),
              child: SizedBox(
                width: widget.size,
                height: widget.size,
                child: widget.size == null ? const CircularProgressIndicator(strokeWidth: 4.0) : const CircularProgressIndicator(strokeWidth: 2.0),
              ),
            ),
          ),
      ],
    );
  }
}

class NamidaRawLikeButton extends StatelessWidget {
  final double size;
  final Color? enabledColor;
  final Color? disabledColor;
  final bool? isLiked;
  final EdgeInsetsGeometry padding;
  final String? removeConfirmationAction;
  final Future<bool> Function(bool isLiked)? onTap;
  final IconData likedIcon;
  final IconData normalIcon;
  final bool enableGradient;

  const NamidaRawLikeButton({
    super.key,
    this.size = 24.0,
    this.enabledColor,
    this.disabledColor,
    required this.isLiked,
    required this.removeConfirmationAction,
    required this.onTap,
    this.padding = EdgeInsets.zero,
    this.likedIcon = Broken.heart_filled,
    this.normalIcon = Broken.heart,
    this.enableGradient = false,
  });

  Future<bool> _confirmRemoval(String action) async {
    bool confirmed = false;
    await NamidaNavigator.inst.navigateDialog(
      dialog: CustomBlurryDialog(
        isWarning: true,
        normalTitleStyle: true,
        bodyText: '$action?',
        actions: [
          const CancelButton(),
          NamidaButton(
            colorScheme: Colors.red,
            text: lang.remove.toUpperCase(),
            onTap: () async {
              confirmed = true;
              NamidaNavigator.inst.closeDialog();
            },
          ),
        ],
      ),
    );
    return confirmed;
  }

  @override
  Widget build(BuildContext context) {
    final theme = context.theme;

    late final enabledIconWidget = Icon(
      key: const ValueKey('like_switcher_on'),
      likedIcon,
      color: enabledColor ?? theme.colorScheme.primary,
      size: size,
    );
    late final normalIconWidget = Icon(
      key: const ValueKey('like_switcher_off'),
      normalIcon,
      color: disabledColor ?? theme.colorScheme.secondary,
      size: size,
    );
    return NamidaMouseRegion(
      enabled: onTap != null,
      child: LikeButton(
        size: size,
        padding: padding,
        likeCountPadding: EdgeInsets.zero,
        bubblesColor: BubblesColor(
          dotPrimaryColor: theme.colorScheme.primary,
          dotSecondaryColor: theme.colorScheme.primaryContainer,
        ),
        circleColor: CircleColor(
          start: theme.colorScheme.secondary,
          end: theme.colorScheme.secondaryContainer,
        ),
        isLiked: isLiked,
        onTap: (isLiked) async {
          if (isLiked && removeConfirmationAction != null) {
            final confirmed = await _confirmRemoval(removeConfirmationAction!);
            if (!confirmed) return isLiked;
          }
          return await onTap?.call(isLiked);
        },
        likeBuilder: (value) {
          if (enableGradient) {
            return CustomAnimatedSwitcher(
              duration: const Duration(milliseconds: 300),
              reverseDuration: const Duration(milliseconds: 300),
              child: value
                  ? ShaderMask(
                      key: const ValueKey('like_switcher_on'),
                      blendMode: BlendMode.srcIn,
                      shaderCallback: (Rect bounds) => LinearGradient(
                        begin: Alignment.topLeft,
                        end: Alignment.bottomRight,
                        stops: [
                          0.2,
                          0.9,
                        ],
                        colors: [
                          theme.colorScheme.primary.withOpacityExt(0.75),
                          Color.alphaBlend(theme.colorScheme.primary.withOpacityExt(0.25), CurrentColor.inst.color).withOpacityExt(0.75),
                        ],
                      ).createShader(bounds),
                      child: enabledIconWidget,
                    )
                  : normalIconWidget,
            );
          } else {
            return CustomAnimatedSwitcher(
              duration: const Duration(milliseconds: 300),
              reverseDuration: const Duration(milliseconds: 300),
              child: value ? enabledIconWidget : normalIconWidget,
            );
          }
        },
      ),
    );
  }
}

class NamidaLocalLikeButton extends StatelessWidget {
  final Track track;
  final double size;
  final Color? color;

  const NamidaLocalLikeButton({
    super.key,
    required this.track,
    this.size = 30.0,
    this.color,
  });

  @override
  Widget build(BuildContext context) {
    return ObxOClass(
      rx: PlaylistController.inst.favouritesPlaylist,
      builder: (context, favouritesPlaylist) => NamidaRawLikeButton(
        size: size,
        enabledColor: color,
        disabledColor: color,
        isLiked: favouritesPlaylist.isSubItemFavourite(track),
        removeConfirmationAction: lang.removeFromFavourites,
        onTap: (isLiked) async => PlaylistController.inst.favouriteButtonOnPressed(track),
      ),
    );
  }
}

class NamidaIconButton extends StatefulWidget {
  final EdgeInsetsGeometry? padding;
  final double horizontalPadding;
  final double verticalPadding;
  final double? iconSize;
  final IconData? icon;
  final Color? iconColor;
  final void Function()? onPressed;
  final void Function(LongPressStartDetails details)? onLongPressStart;
  final void Function()? onLongPressFinish;
  final void Function()? onLongPress;
  final String Function()? tooltip;
  final bool disableColor;
  final Widget? child;

  const NamidaIconButton({
    super.key,
    this.padding,
    this.horizontalPadding = 8.0,
    this.verticalPadding = 0.0,
    required this.icon,
    this.onPressed,
    this.onLongPressStart,
    this.onLongPressFinish,
    this.onLongPress,
    this.iconSize,
    this.iconColor,
    this.tooltip,
    this.disableColor = false,
    this.child,
  });

  @override
  State<NamidaIconButton> createState() => _NamidaIconButtonState();
}

class _NamidaIconButtonState extends State<NamidaIconButton> {
  bool isPressed = false;

  @override
  Widget build(BuildContext context) {
    return NamidaTooltip(
      message: widget.tooltip,
      child: NamidaMouseRegion(
        enabled: widget.onPressed != null || widget.onLongPress != null,
        child: GestureDetector(
          behavior: HitTestBehavior.translucent,
          onTapDown: (value) => setState(() => isPressed = true),
          onTapUp: (value) => setState(() => isPressed = false),
          onTapCancel: () => setState(() => isPressed = false),
          onTap: widget.onPressed,
          onLongPressStart: widget.onLongPressStart,
          onLongPressEnd: widget.onLongPressFinish == null ? null : (details) => widget.onLongPressFinish!(),
          onLongPressCancel: widget.onLongPressFinish,
          onLongPress: widget.onLongPress,
          onLongPressUp: widget.onLongPressFinish,
          child: AnimatedOpacity(
            duration: const Duration(milliseconds: 200),
            opacity: isPressed ? 0.5 : 1.0,
            child: Padding(
              padding: widget.padding ?? EdgeInsets.symmetric(horizontal: widget.horizontalPadding, vertical: widget.verticalPadding),
              child:
                  widget.child ??
                  Icon(
                    widget.icon,
                    size: widget.iconSize,
                    color: widget.disableColor ? null : (widget.iconColor ?? context.theme.colorScheme.secondary),
                  ),
            ),
          ),
        ),
      ),
    );
  }
}

class NamidaAppBarIcon extends StatelessWidget {
  final IconData icon;
  final Widget? child;
  final void Function()? onPressed;
  final String Function()? tooltip;

  const NamidaAppBarIcon({
    super.key,
    required this.icon,
    this.child,
    this.onPressed,
    this.tooltip,
  });

  @override
  Widget build(BuildContext context) {
    return NamidaIconButton(
      verticalPadding: 8.0,
      horizontalPadding: 6.0,
      icon: icon,
      onPressed: onPressed,
      tooltip: tooltip,
      child: child,
    );
  }
}

class NamidaPartyContainer extends StatelessWidget {
  final double spreadRadiusMultiplier;
  final double? width;
  final double? height;
  const NamidaPartyContainer({
    super.key,
    this.spreadRadiusMultiplier = 1.0,
    this.width,
    this.height,
  });

  @override
  Widget build(BuildContext context) {
    if (!settings.enablePartyModeColorSwap.value) {
      return ObxO(
        rx: Player.inst.nowPlayingPosition,
        builder: (context, nowPlayingPosition) {
          final finalScale = WaveformController.inst.getCurrentAnimatingScale(nowPlayingPosition);
          return AnimatedSizedBox(
            duration: const Duration(milliseconds: 400),
            height: height ?? context.height,
            width: width ?? context.width,
            decoration: BoxDecoration(
              boxShadow: [
                BoxShadow(
                  color: CurrentColor.inst.miniplayerColor.withAlpha(150),
                  spreadRadius: 150 * finalScale * spreadRadiusMultiplier,
                  blurRadius: 10 + (200 * finalScale),
                ),
              ],
            ),
          );
        },
      );
    } else {
      return ObxO(
        rx: Player.inst.nowPlayingPosition,
        builder: (context, nowPlayingPosition) {
          final finalScale = WaveformController.inst.getCurrentAnimatingScale(nowPlayingPosition);
          return height != null
              ? ObxO(
                  rx: CurrentColor.inst.paletteFirstHalf,
                  builder: (context, firstHalf) => Row(
                    children: [
                      ...firstHalf.map(
                        (e) => AnimatedSizedBox(
                          duration: const Duration(milliseconds: 400),
                          height: height,
                          width: width ?? Dimensions.inst.miniplayerMaxWidth / firstHalf.length,
                          decoration: BoxDecoration(
                            boxShadow: [
                              BoxShadow(
                                color: e.withAlpha(150),
                                spreadRadius: 150 * finalScale * spreadRadiusMultiplier,
                                blurRadius: 10 + (200 * finalScale),
                              ),
                            ],
                          ),
                        ),
                      ),
                    ],
                  ),
                )
              : ObxO(
                  rx: CurrentColor.inst.paletteSecondHalf,
                  builder: (context, secondHalf) => Column(
                    children: [
                      ...secondHalf.map(
                        (e) => AnimatedSizedBox(
                          duration: const Duration(milliseconds: 400),
                          height: height ?? context.height / secondHalf.length,
                          width: width,
                          decoration: BoxDecoration(
                            boxShadow: [
                              BoxShadow(
                                color: e.withAlpha(150),
                                spreadRadius: 140 * finalScale * spreadRadiusMultiplier,
                                blurRadius: 10 + (200 * finalScale),
                              ),
                            ],
                          ),
                        ),
                      ),
                    ],
                  ),
                );
        },
      );
    }
  }
}

class SubpageInfoContainer extends StatelessWidget {
  final double maxWidth;
  final String title;
  final String subtitle;
  final String thirdLineText;
  final double? height;
  final double topPadding;
  final double bottomPadding;
  final Widget Function(double size) imageBuilder;
  final Iterable<Selectable> Function() tracksFn;
  final QueueSource source;
  final String heroTag;

  const SubpageInfoContainer({
    super.key,
    required this.maxWidth,
    required this.title,
    required this.subtitle,
    this.thirdLineText = '',
    this.height,
    required this.imageBuilder,
    required this.tracksFn,
    this.topPadding = 16.0,
    this.bottomPadding = 16.0,
    required this.source,
    required this.heroTag,
  });

  void _openAdvancedShuffleDialog() {
    return openAdvancedShuffleDialog(tracksFn, source);
  }

  void _openAdvancedPlayDialog() {
    return openAdvancedPlayDialog(tracksFn, source);
  }

  static void openAdvancedShuffleDialog(Iterable<Selectable> Function() tracksFn, QueueSource source) {
    final tracks = tracksFn();
    final tracksCount = tracks.length;
    final countRx = tracksCount.obs;

    Iterable<Playable> getTracksWithLimit() {
      var all = tracksFn();
      final requiredCount = countRx.value;
      if (requiredCount > 0 && requiredCount < all.length) {
        all = all.getRandomSample(requiredCount);
      }
      return all;
    }

    Track? playAfterTrack;
    if (Player.inst.currentItem.value is Selectable && Player.inst.latestInsertedIndex > Player.inst.currentIndex.value) {
      playAfterTrack = (Player.inst.currentQueue.value[Player.inst.latestInsertedIndex] as Selectable).track;
    }

    NamidaNavigator.inst.navigateDialog(
      onDisposing: () {
        countRx.close();
      },
      dialog: CustomBlurryDialog(
        title: lang.shuffle,
        actions: const [
          DoneButton(),
        ],
        child: Column(
          children: [
            const SizedBox(height: 32.0),
            Obx(
              (context) {
                final count = countRx.valueR;
                return NamidaWheelSlider(
                  initValue: countRx.value,
                  min: 0,
                  max: (tracks.length - 1).withMinimum(1),
                  onValueChanged: (val) => countRx.value = (val + 1).withMaximum(tracksCount),
                  text: count == tracksCount ? lang.all : count.displayTrackKeyword,
                  topText: lang.tracks,
                  textPadding: 8.0,
                );
              },
            ),
            const SizedBox(height: 12.0),
            CustomListTile(
              icon: Broken.shuffle,
              title: lang.shuffle,
              onTap: () {
                Player.inst.playOrPause(0, getTracksWithLimit(), source, shuffle: true);
                NamidaNavigator.inst.closeDialog();
              },
            ),
            CustomListTile(
              icon: Broken.next,
              title: "${lang.playNext} (${lang.shuffle})",
              onTap: () {
                Player.inst.addToQueue(getTracksWithLimit(), insertNext: true);
                NamidaNavigator.inst.closeDialog();
              },
            ),
            if (playAfterTrack != null)
              CustomListTile(
                icon: Broken.hierarchy_square,
                title: "${lang.playAfter} (${lang.shuffle})",
                subtitle: [playAfterTrack.artistsList.firstOrNull, playAfterTrack.title].joinText(separator: ' - '),
                onTap: () {
                  Player.inst.addToQueue(getTracksWithLimit(), insertAfterLatest: true);
                  NamidaNavigator.inst.closeDialog();
                },
              ),
            CustomListTile(
              icon: Broken.play_cricle,
              title: "${lang.playLast} (${lang.shuffle})",
              onTap: () {
                Player.inst.addToQueue(getTracksWithLimit());
                NamidaNavigator.inst.closeDialog();
              },
            ),
          ],
        ),
      ),
    );
  }

  static void openAdvancedPlayDialog(Iterable<Selectable> Function() tracksFn, QueueSource source) {
    Iterable<Playable> getTracksWithLimit() {
      return tracksFn();
    }

    Track? playAfterTrack;
    if (Player.inst.currentItem.value is Selectable && Player.inst.latestInsertedIndex > Player.inst.currentIndex.value) {
      playAfterTrack = (Player.inst.currentQueue.value[Player.inst.latestInsertedIndex] as Selectable).track;
    }

    NamidaNavigator.inst.navigateDialog(
      dialog: CustomBlurryDialog(
        title: lang.play,
        actions: const [
          DoneButton(),
        ],
        child: Column(
          children: [
            const SizedBox(height: 12.0),
            CustomListTile(
              icon: Broken.play,
              title: lang.play,
              onTap: () {
                Player.inst.playOrPause(0, getTracksWithLimit(), source);
                NamidaNavigator.inst.closeDialog();
              },
            ),
            CustomListTile(
              icon: Broken.next,
              title: lang.playNext,
              onTap: () {
                Player.inst.addToQueue(getTracksWithLimit(), insertNext: true);
                NamidaNavigator.inst.closeDialog();
              },
            ),
            if (playAfterTrack != null)
              CustomListTile(
                icon: Broken.hierarchy_square,
                title: lang.playAfter,
                subtitle: [playAfterTrack.artistsList.firstOrNull, playAfterTrack.title].joinText(separator: ' - '),
                onTap: () {
                  Player.inst.addToQueue(getTracksWithLimit(), insertAfterLatest: true);
                  NamidaNavigator.inst.closeDialog();
                },
              ),
            CustomListTile(
              icon: Broken.play_cricle,
              title: lang.playLast,
              onTap: () {
                Player.inst.addToQueue(getTracksWithLimit());
                NamidaNavigator.inst.closeDialog();
              },
            ),
          ],
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    final textTheme = context.textTheme;
    const textHeroEnabled = false;
    const pauseHero = 'kururing';
    final showSubpageInfoAtSide = Dimensions.inst.showSubpageInfoAtSideContext(context);

    return Container(
      alignment: Alignment.topCenter,
      padding: const EdgeInsets.only(top: 12.0, left: 12.0, right: 12.0, bottom: 4.0),
      margin: EdgeInsets.only(top: topPadding, bottom: bottomPadding),
      height: height,
      child: LayoutWidthHeightProvider(
        builder: (context, maxWidth, maxHeight) {
          maxWidth = maxWidth.withMaximum(this.maxWidth);

          double imageMaxWidth;
          double infoMaxWidth;

          if (showSubpageInfoAtSide) {
            imageMaxWidth = maxWidth;
            infoMaxWidth = maxWidth;
          } else {
            imageMaxWidth = (maxWidth * 0.4).withMaximum(maxHeight * 0.3);
            infoMaxWidth = maxWidth - imageMaxWidth;
          }

          final imageWidget = imageBuilder(imageMaxWidth);

          double getFontSize(double p, double min, double max) => ((infoMaxWidth * 0.2).withMaximum(maxHeight * 0.1) * p).clampDouble(min, max);

          final textAndButtonsWidget = Column(
            mainAxisAlignment: MainAxisAlignment.start,
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisSize: MainAxisSize.min,
            children: [
              const SizedBox(height: 18.0),
              Padding(
                padding: const EdgeInsets.only(left: 14.0),
                child: NamidaHero(
                  enabled: textHeroEnabled,
                  tag: '${pauseHero}line1_$heroTag',
                  child: showSubpageInfoAtSide
                      ? Text(
                          title,
                          style: textTheme.displayLarge?.copyWith(fontSize: getFontSize(0.5, 10.0, 32.0)),
                          softWrap: true,
                        )
                      : Text(
                          title,
                          style: textTheme.displayLarge?.copyWith(fontSize: getFontSize(0.4, 10.0, 32.0)),
                          maxLines: 1,
                          softWrap: false,
                          overflow: TextOverflow.ellipsis,
                        ),
                ),
              ),
              if (subtitle.isNotEmpty) ...[
                const SizedBox(height: 2.0),
                Padding(
                  padding: const EdgeInsets.only(left: 14.0),
                  child: NamidaHero(
                    enabled: textHeroEnabled,
                    tag: '${pauseHero}line2_$heroTag',
                    child: Text(
                      subtitle,
                      overflow: TextOverflow.ellipsis,
                      maxLines: 2,
                      style: textTheme.displayMedium?.copyWith(fontSize: getFontSize(0.28, 10.0, 24.0)),
                    ),
                  ),
                ),
              ],
              if (thirdLineText.isNotEmpty) ...[
                const SizedBox(height: 2.0),
                Padding(
                  padding: const EdgeInsets.only(left: 14.0),
                  child: NamidaHero(
                    enabled: textHeroEnabled,
                    tag: '${pauseHero}line3_$heroTag',
                    child: Text(
                      thirdLineText,
                      overflow: TextOverflow.ellipsis,
                      maxLines: 1,
                      style: textTheme.displaySmall?.copyWith(fontSize: getFontSize(0.25, 10.0, 22.0)),
                    ),
                  ),
                ),
              ],
              const SizedBox(height: 18.0),
              ConstrainedBox(
                constraints: BoxConstraints(maxWidth: infoMaxWidth * 0.85),
                child: FittedBox(
                  alignment: Alignment.topLeft,
                  fit: BoxFit.scaleDown,
                  child: Row(
                    mainAxisSize: MainAxisSize.min,
                    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                    children: [
                      const SizedBox(width: 6.0),
                      NamidaButton(
                        icon: Broken.shuffle,
                        onTap: () => Player.inst.playOrPause(
                          0,
                          tracksFn(),
                          source,
                          shuffle: true,
                        ),
                        onLongPress: _openAdvancedShuffleDialog,
                      ),
                      const SizedBox(width: 6.0),
                      NamidaButton(
                        onTap: () => Player.inst.addToQueue(tracksFn()),
                        icon: Broken.play_cricle,
                        text: lang.playLast,
                        onLongPress: _openAdvancedPlayDialog,
                      ),
                      const SizedBox(width: 6.0),
                    ],
                  ),
                ),
              ),
            ],
          );

          return showSubpageInfoAtSide
              ? Column(
                  mainAxisSize: MainAxisSize.min,
                  crossAxisAlignment: CrossAxisAlignment.start,
                  mainAxisAlignment: MainAxisAlignment.start,
                  children: [
                    Flexible(
                      child: FittedBox(
                        alignment: Alignment.topLeft,
                        fit: BoxFit.scaleDown,
                        child: imageWidget,
                      ),
                    ),
                    Padding(
                      padding: const EdgeInsets.only(left: 4.0),
                      child: textAndButtonsWidget,
                    ),
                  ],
                )
              : Row(
                  mainAxisAlignment: MainAxisAlignment.start,
                  children: [
                    ConstrainedBox(
                      constraints: BoxConstraints(maxWidth: imageMaxWidth),
                      child: FittedBox(
                        alignment: Alignment.topLeft,
                        fit: BoxFit.scaleDown,
                        child: imageWidget,
                      ),
                    ),
                    ConstrainedBox(
                      constraints: BoxConstraints(maxWidth: infoMaxWidth),
                      child: textAndButtonsWidget,
                    ),
                  ],
                );
        },
      ),
    );
  }
}

class AnimatingTile extends StatelessWidget {
  final int position;
  final Widget child;
  final bool shouldAnimate;
  final bool allowTilting;
  final Duration duration;

  const AnimatingTile({
    super.key,
    required this.position,
    required this.child,
    this.duration = const Duration(milliseconds: 400),
    this.shouldAnimate = true,
    this.allowTilting = false,
  });

  @override
  Widget build(BuildContext context) {
    Widget child = this.child;
    if (shouldAnimate) {
      child = AnimationConfiguration.staggeredList(
        position: position,
        duration: duration,
        delay: const Duration(milliseconds: 50),
        child: SlideAnimation(
          verticalOffset: 25.0,
          child: FadeInAnimation(
            duration: duration,
            child: child,
          ),
        ),
      );
    }

    if (NamidaFeaturesVisibility.tiltingCardsEffect) {
      if (allowTilting && settings.extra.tiltingCardsEffect == true) {
        child = _EncapsulateWithTilt(
          isInOpenSpace: false,
          child: child,
        );
      }
    }

    return child;
  }
}

class AnimatingGrid extends StatelessWidget {
  final int position;
  final int countPerRowResolved;
  final int columnCount;
  final Widget child;
  final bool shouldAnimate;
  final bool isInOpenSpace;

  const AnimatingGrid({
    super.key,
    required this.position,
    required this.countPerRowResolved,
    required this.columnCount,
    required this.child,
    this.shouldAnimate = true,
    this.isInOpenSpace = false,
  });

  @override
  Widget build(BuildContext context) {
    Widget child = this.child;
    if (shouldAnimate) {
      final duration = Duration(
        milliseconds: (400 / countPerRowResolved).clamp(100, 400).round(),
      );
      child = AnimationConfiguration.staggeredGrid(
        columnCount: columnCount,
        position: position,
        duration: duration,
        child: SlideAnimation(
          verticalOffset: 25.0,
          child: FadeInAnimation(
            duration: duration,
            child: child,
          ),
        ),
      );
    }

    if (NamidaFeaturesVisibility.tiltingCardsEffect) {
      if (settings.extra.tiltingCardsEffect == true) {
        child = _EncapsulateWithTilt(
          isInOpenSpace: isInOpenSpace,
          child: child,
        );
      }
    }

    return child;
  }
}

class _EncapsulateWithTilt extends StatelessWidget {
  final Widget child;

  /// when parent doesn't have limited size, cuz we force expand child by default.
  final bool isInOpenSpace;

  const _EncapsulateWithTilt({
    required this.child,
    required this.isInOpenSpace,
  });

  @override
  Widget build(BuildContext context) {
    return Tilt.base(
      tiltConfig: const TiltConfig(
        angle: 2.0,
        sensorFactor: 0.5,
        sensorRevertFactor: 0.1,
        enableOutsideAreaMove: false,
        enableReverse: false,
        enableGestureTouch: false,
        enableGestureHover: true,
        enableGestureSensors: false,
        controllerMoveDuration: Duration(milliseconds: 200),
        leaveDuration: Duration(milliseconds: 400),
        moveDuration: Duration(milliseconds: 100),
        sensorMoveDuration: Duration(milliseconds: 50),
        enterDuration: Duration(milliseconds: 800),
        controllerLeaveDuration: Duration(milliseconds: 200),
      ),
      fps: 30,
      clipBehavior: Clip.none,
      lightConfig: const LightConfig(
        enableReverse: true,
        maxIntensity: 0.2,
        // projectorScale: 0.8,
        spreadFactor: 2.0,
        color: Color(0xCCFFFFFF),
      ),
      shadowConfig: const ShadowBaseConfig(
        disable: true,
        enableReverse: true,
        color: Colors.transparent,
      ),
      childLayout: ChildLayout(
        inner: [
          Stack(
            clipBehavior: Clip.none,
            fit: isInOpenSpace ? StackFit.loose : StackFit.expand, // otherwise inner stacks collapse
            children: [
              child,
            ],
          ),
        ],
      ),
      child: const SizedBox(),
    );
  }
}

class NamidaDrawerListTile extends StatelessWidget {
  final void Function()? onTap;
  final bool enabled;
  final String title;
  final IconData? icon;
  final double? width;
  final double? height;
  final EdgeInsetsGeometry margin;
  final EdgeInsetsGeometry padding;
  final bool isCentered;
  final double iconSize;

  const NamidaDrawerListTile({
    super.key,
    this.onTap,
    required this.enabled,
    required this.title,
    required this.icon,
    this.width,
    this.height,
    this.margin = const EdgeInsets.symmetric(horizontal: 12.0, vertical: 3.0),
    this.padding = const EdgeInsets.symmetric(horizontal: 10.0, vertical: 11.0),
    this.isCentered = false,
    this.iconSize = 20.0,
  });

  @override
  Widget build(BuildContext context) {
    final colorScheme = CurrentColor.inst.color;
    final theme = context.theme;
    final textTheme = theme.textTheme;
    return NamidaInkWell(
      animationDurationMS: 200,
      alignment: Alignment.center,
      width: width,
      height: height,
      decoration: BoxDecoration(
        color: enabled ? colorScheme : theme.cardColor,
        borderRadius: BorderRadius.circular(8.0.multipliedRadius),
        boxShadow: enabled
            ? [
                BoxShadow(
                  color: colorScheme.withAlpha(100),
                  spreadRadius: 0.2,
                  blurRadius: 8.0,
                  offset: const Offset(0.0, 3.0),
                ),
              ]
            : null,
      ),
      margin: margin,
      padding: padding,
      onTap: onTap,
      borderRadius: 8.0,
      child: Row(
        mainAxisAlignment: isCentered ? MainAxisAlignment.center : MainAxisAlignment.start,
        children: [
          Icon(
            icon,
            color: enabled ? Colors.white.withAlpha(200) : null,
            size: iconSize,
          ),
          if (title != '') const SizedBox(width: 10.0),
          if (title != '')
            Expanded(
              child: Text(
                title,
                style: textTheme.displayMedium?.copyWith(
                  color: enabled ? Colors.white.withAlpha(200) : null,
                  fontSize: 15.0,
                ),
                overflow: TextOverflow.fade,
                softWrap: false,
              ),
            ),
        ],
      ),
    );
  }
}

class SearchPageTitleRow extends StatelessWidget {
  final String title;
  final String subtitle;
  final IconData icon;
  final Widget? trailing;
  final Widget? subtitleWidget;
  final Widget? leading;

  const SearchPageTitleRow({
    super.key,
    required this.title,
    this.subtitle = '',
    required this.icon,
    this.trailing,
    this.subtitleWidget,
    this.leading,
  });

  @override
  Widget build(BuildContext context) {
    final textTheme = context.textTheme;
    return Row(
      children: [
        const SizedBox(width: 16.0),
        Row(
          children: [
            leading ?? Icon(icon),
            const SizedBox(width: 8.0),
            Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  title,
                  style: textTheme.displayLarge?.copyWith(fontSize: 15.5),
                ),
                ?subtitleWidget,
                if (subtitle != '')
                  Text(
                    subtitle,
                    style: textTheme.displaySmall,
                  ),
              ],
            ),
          ],
        ),
        const Spacer(),
        ?trailing,
        const SizedBox(width: 8.0),
      ],
    );
  }
}

class NamidaLogoContainer extends StatelessWidget {
  final double? width, height;
  final double iconSize;
  final bool displayText;
  final bool lighterShadow;
  final EdgeInsetsGeometry? padding;
  final EdgeInsetsGeometry? margin;
  final VoidCallback? afterTap;

  const NamidaLogoContainer({
    super.key,
    this.height = 54.0,
    this.width,
    this.iconSize = 40.0,
    this.displayText = true,
    this.lighterShadow = false,
    this.padding,
    this.margin,
    this.afterTap,
  });

  @override
  Widget build(BuildContext context) {
    final theme = context.theme;
    final textTheme = theme.textTheme;
    final isDarkMode = context.isDarkMode;
    final colorScheme = CurrentColor.inst.color;
    final scaffoldBgColor = Color.alphaBlend(theme.scaffoldBackgroundColor.withOpacityExt(0.5), isDarkMode ? Colors.black : Colors.white);
    const foregroundColorOpacity = 0.8;
    final foregroundColor = Color.alphaBlend(colorScheme.withOpacityExt(0.1), theme.colorScheme.onSurface).withOpacityExt(foregroundColorOpacity);
    return NamidaInkWell(
      onTap: () {
        if (NamidaNavigator.inst.currentRoute?.route != RouteType.PAGE_about) {
          const AboutPage().navigate();
        }
        afterTap?.call();
      },
      animationDurationMS: 300,
      alignment: Alignment.center,
      height: height,
      width: width,
      margin: margin ?? const EdgeInsets.symmetric(horizontal: 12.0).add(const EdgeInsets.only(top: 16.0, bottom: 8.0)),
      padding: padding ?? const EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0),
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(12.0.multipliedRadius),
        border: Border.all(
          color: colorScheme.withOpacityExt(0.5),
        ),
        gradient: LinearGradient(
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
          colors: [
            Color.alphaBlend(scaffoldBgColor.withOpacityExt(0.90), colorScheme).withOpacityExt(1.0),
            Color.alphaBlend(scaffoldBgColor.withOpacityExt(0.65), colorScheme).withOpacityExt(1.0),
          ],
        ),
        boxShadow: [
          lighterShadow
              ? BoxShadow(
                  color: colorScheme.withAlpha(isDarkMode ? 20 : 60),
                  spreadRadius: 0.1,
                  blurRadius: 6.0,
                  offset: const Offset(0.0, 2.0),
                )
              : BoxShadow(
                  color: colorScheme.withAlpha(isDarkMode ? 30 : 80),
                  spreadRadius: 0.2,
                  blurRadius: 8.0,
                  offset: const Offset(0.0, 4.0),
                ),
        ],
      ),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        mainAxisSize: MainAxisSize.min,
        children: [
          if (displayText) const SizedBox(width: 4.0),
          Image.asset(
            NamidaChannel.defaultLayerIconForPlatform,
            width: iconSize,
            height: iconSize,
            cacheHeight: 240,
            cacheWidth: 240,
            alignment: Alignment.center,
            color: foregroundColor,
          ),
          if (displayText) ...[
            const SizedBox(width: 10.0),
            Expanded(
              child: Text(
                'Namida',
                style: textTheme.displayLarge?.copyWith(
                  color: foregroundColor.withOpacityExt(foregroundColorOpacity * 0.9),
                  fontSize: 17.5,
                ),
                overflow: TextOverflow.fade,
                softWrap: false,
              ),
            ),
          ],
        ],
      ),
    );
  }
}

class NamidaContainerDivider extends StatelessWidget {
  final double? width;
  final double height;
  final Color? color;
  final Color? colorForce;
  final EdgeInsetsGeometry? margin;

  const NamidaContainerDivider({
    super.key,
    this.width,
    this.height = 2.0,
    this.color,
    this.margin,
    this.colorForce,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      height: height,
      width: width,
      margin: margin,
      decoration: BoxDecoration(
        color: colorForce ?? (color ?? context.theme.dividerColor).withAlpha(namida.isDarkMode ? 100 : 20),
        borderRadius: BorderRadius.circular(18.0.multipliedRadius),
      ),
    );
  }
}

class FadeDismissible extends StatefulWidget {
  final Widget child;
  final void Function(DismissDirection direction) onDismissed;
  final void Function(DragStartDetails details)? onDismissStart;
  final void Function(DragEndDetails details)? onDismissEnd;
  final void Function(DragEndDetails details)? onDismissCancel;
  final DismissDirection direction;
  final Duration dismissDuration;
  final Duration settleDuration;
  final double dismissThreshold;
  final double dismissRangeStart;
  final double dismissRangeEnd;
  final Curve dismissCurve;
  final Curve settleCurve;
  final bool Function()? draggable;
  final RxBase<bool>? draggableRx;
  final Widget? onTopWidget;
  final bool removeOnDismiss;
  final Widget Function()? leftWidget;
  final Widget Function()? rightWidget;

  /// value multiplied by the animation.
  /// 0.0 means top friction, 1.0 means normal friction & >1 means more lose friction
  final double friction;

  const FadeDismissible({
    required super.key,
    required this.child,
    required this.onDismissed,
    this.onDismissStart,
    this.onDismissEnd,
    this.onDismissCancel,
    this.direction = DismissDirection.horizontal,
    this.dismissDuration = const Duration(milliseconds: 300),
    this.settleDuration = const Duration(milliseconds: 300),
    this.dismissThreshold = 0.8,
    this.dismissRangeStart = 0.1,
    this.dismissRangeEnd = 0.9,
    this.dismissCurve = Curves.fastOutSlowIn,
    this.settleCurve = Curves.easeOutQuart,
    this.draggable,
    this.draggableRx,
    this.onTopWidget,
    this.removeOnDismiss = true,
    this.leftWidget,
    this.rightWidget,
    this.friction = 1.0,
  });

  static bool isDismissing = false;

  @override
  State<FadeDismissible> createState() => _FadeDismissibleState();
}

class _FadeDismissibleState extends State<FadeDismissible> with SingleTickerProviderStateMixin {
  double get progress => _animation.value.abs();

  late final _animation = AnimationController(
    vsync: this,
    lowerBound: -1,
    upperBound: 1,
    value: 0,
  );

  @override
  void dispose() {
    _animation.dispose();
    super.dispose();
  }

  double _dragged = 0;

  bool _draggable = true;
  bool _inDismissRange = true;
  bool? _canSwipeInternal; // calculate for direction once to allow swiping back.

  void calculateInDismissRange(double positionDx, double maxWidth) {
    final percentage = positionDx / maxWidth;
    _inDismissRange = percentage >= widget.dismissRangeStart && percentage <= widget.dismissRangeEnd;
  }

  Future<void> _animateDismiss(double to, {required bool faster}) async {
    await _animation.animateTo(to, duration: faster ? widget.dismissDuration * 0.5 : widget.dismissDuration, curve: faster ? Curves.linear : widget.dismissCurve);
  }

  Future<void> _dismissToRight(DragEndDetails d, {bool faster = false}) async {
    if (widget.removeOnDismiss) {
      await _animateDismiss(1, faster: faster);
      widget.onDismissed(DismissDirection.startToEnd);
      if (widget.onDismissEnd != null) widget.onDismissEnd!(d);
      _animation.animateTo(0, duration: Duration.zero); // fixes rendering issue
    } else {
      widget.onDismissed(DismissDirection.startToEnd);
      await _resetToMiddle(d);
    }
  }

  Future<void> _dismissToLeft(DragEndDetails d, {bool faster = false}) async {
    if (widget.removeOnDismiss) {
      await _animateDismiss(-1, faster: faster);
      widget.onDismissed(DismissDirection.endToStart);
      if (widget.onDismissEnd != null) widget.onDismissEnd!(d);
      _animation.animateTo(0, duration: Duration.zero); // fixes rendering issue
    } else {
      widget.onDismissed(DismissDirection.endToStart);
      await _resetToMiddle(d);
    }
  }

  Future<void> _resetToMiddle(DragEndDetails d) async {
    if (widget.onDismissCancel != null) widget.onDismissCancel!(d);
    await _animation.animateTo(0, duration: widget.settleDuration, curve: widget.settleCurve);
  }

  Widget buildChild(bool draggable, Widget child, double maxWidth, Animation<double> fadeAnimation) {
    return HorizontalDragDetector(
      onStart: !draggable
          ? null
          : (d) {
              if (PullToRefreshMixin.isPulling) return;
              FadeDismissible.isDismissing = true;
              if (widget.onDismissStart != null) widget.onDismissStart!(d);
              calculateInDismissRange(d.localPosition.dx, maxWidth);
              if (widget.draggable != null) _draggable = widget.draggable!();
            },
      onUpdate: !draggable
          ? null
          : (d) {
              if (!_draggable) return;
              if (!_inDismissRange) return;
              if (PullToRefreshMixin.isPulling) return;
              if (_canSwipeInternal == null) {
                bool canSwipe = true;
                if (d.delta.dx.isNegative) {
                  if (widget.direction == DismissDirection.startToEnd) canSwipe = false;
                } else {
                  if (widget.direction == DismissDirection.endToStart) canSwipe = false;
                }
                if (canSwipe != _canSwipeInternal) _canSwipeInternal = canSwipe;
              }
              if (_canSwipeInternal == false) return;

              _dragged += d.delta.dx;
              _animation.animateTo(_dragged / maxWidth, duration: Duration.zero);
            },
      onEnd: !draggable
          ? null
          : (d) {
              if (PullToRefreshMixin.isPulling) return;
              FadeDismissible.isDismissing = false;
              _canSwipeInternal = null;

              final velocity = d.velocity.pixelsPerSecond.dx;
              if (velocity > 800) {
                _dismissToRight(d, faster: true);
              } else if (velocity < -800) {
                _dismissToLeft(d, faster: true);
              } else if (progress > widget.dismissThreshold) {
                if (_animation.value < 0) {
                  _dismissToLeft(d);
                } else {
                  _dismissToRight(d);
                }
              } else {
                _resetToMiddle(d);
              }
              _dragged = 0;
            },
      onCancel: () {
        FadeDismissible.isDismissing = false;
      },
      child: AnimatedBuilder(
        animation: _animation,
        child: FadeTransition(
          opacity: fadeAnimation,
          child: child,
        ),
        builder: (context, child) {
          final p = _animation.value;
          // if (p == 0) return child!; // causes unnecessary rebuilds
          return Transform.translate(
            offset: p == 0 ? Offset.zero : Offset(p * widget.friction * maxWidth, 0),
            child: child!,
          );
        },
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    final maxWidth = Dimensions.inst.availableAppContentWidth;
    final child = widget.onTopWidget != null
        ? Stack(
            children: [
              widget.child,
              widget.onTopWidget!,
            ],
          )
        : widget.child;

    final fadeAnimation = _animation.drive(
      widget.friction == 1.0
          ? Animatable.fromCallback((value) => 1 - value.abs())
          : Animatable.fromCallback(
              (value) => 1 - (value * widget.friction).abs().clampDouble(0, 1),
            ),
    );
    Widget dismissibleChild = widget.draggableRx != null
        ? ObxO(
            rx: widget.draggableRx!,
            builder: (context, value) => buildChild(value && widget.direction != DismissDirection.none, child, maxWidth, fadeAnimation),
          )
        : buildChild(_draggable && widget.direction != DismissDirection.none, child, maxWidth, fadeAnimation);

    if (widget.leftWidget != null || widget.rightWidget != null) {
      final reverseFadeAnimation = ReverseAnimation(fadeAnimation.drive(Animatable.fromCallback((value) => (value * 1.5 - 0.5).clampDouble(0, 1))));
      Widget? leftWidget;
      Widget? rightWidget;
      dismissibleChild = Stack(
        alignment: AlignmentGeometry.center,
        children: [
          if (widget.leftWidget != null)
            Positioned(
              left: 0.0,
              child: AnimatedBuilder(
                animation: _animation,
                builder: (context, _) {
                  final p = _animation.value;
                  if (p <= 0) return const SizedBox();
                  return leftWidget ??= FadeTransition(
                    opacity: reverseFadeAnimation,
                    child: widget.leftWidget!(),
                  );
                },
              ),
            ),
          if (widget.rightWidget != null)
            Positioned(
              right: 0.0,
              child: AnimatedBuilder(
                animation: _animation,
                builder: (context, _) {
                  final p = _animation.value;
                  if (p >= 0) return const SizedBox();
                  return rightWidget ??= FadeTransition(
                    opacity: reverseFadeAnimation,
                    child: widget.rightWidget!(),
                  );
                },
              ),
            ),
          dismissibleChild,
        ],
      );
    }
    return dismissibleChild;
  }
}

class FadeIgnoreTransition extends StatefulWidget {
  /// Wether to completely replace the [child] with a [SizedBox], instead of just using [IgnorePointer].
  final bool completelyKillWhenPossible;
  final Animation<double> opacity;
  final Widget child;

  const FadeIgnoreTransition({
    super.key,
    this.completelyKillWhenPossible = false,
    required this.opacity,
    required this.child,
  });

  @override
  State<FadeIgnoreTransition> createState() => _FadeIgnoreTransitionState();
}

class _FadeIgnoreTransitionState extends State<FadeIgnoreTransition> {
  bool _ignoring = false;

  @override
  void initState() {
    super.initState();
    widget.opacity.addListener(_checkOpacity);
    _checkOpacity();

    // -- just extra, for some rare cases
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (mounted) _checkOpacity();
    });
  }

  @override
  void didUpdateWidget(FadeIgnoreTransition oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.opacity != oldWidget.opacity) {
      oldWidget.opacity.removeListener(_checkOpacity);
      widget.opacity.addListener(_checkOpacity);
      _checkOpacity();
    }
  }

  @override
  void dispose() {
    widget.opacity.removeListener(_checkOpacity);
    super.dispose();
  }

  void _checkOpacity() {
    final shouldIgnore = widget.opacity.value <= 0.01;
    if (_ignoring != shouldIgnore) {
      setState(() => _ignoring = shouldIgnore);
    }
  }

  @override
  Widget build(BuildContext context) {
    if (widget.completelyKillWhenPossible && _ignoring) return const SizedBox();
    return IgnorePointer(
      ignoring: _ignoring,
      child: FadeTransition(
        opacity: widget.opacity,
        child: widget.child,
      ),
    );
  }
}

class NamidaSelectableAutoLinkText extends StatelessWidget {
  final String text;
  final double fontScale;
  final Color? colorScheme;
  const NamidaSelectableAutoLinkText({super.key, required this.text, this.fontScale = 1.0, this.colorScheme});

  @override
  Widget build(BuildContext context) {
    final theme = context.theme;
    final textTheme = theme.textTheme;
    return SelectableAutoLinkText(
      text,
      style: textTheme.displayMedium?.copyWith(
        fontSize: 13.5 * fontScale,
      ),
      linkStyle: textTheme.displayMedium?.copyWith(
        color: theme.colorScheme.primary.withAlpha(210),
        fontSize: 13.5 * fontScale,
      ),
      highlightedLinkStyle: TextStyle(
        color: theme.colorScheme.primary.withAlpha(220),
        backgroundColor: theme.colorScheme.onSurface.withAlpha(40),
        fontSize: 13.5 * fontScale,
      ),
      scrollPhysics: const NeverScrollableScrollPhysics(),
      onTap: (url) async => await NamidaLinkUtils.openLinkPreferNamida(url, theme: theme),
    );
  }
}

class DefaultPlaylistCard extends StatelessWidget {
  final Color colorScheme;
  final IconData icon;
  final String title;
  final String text;
  final double? width;
  final bool displayLoadingIndicator;
  final void Function()? onTap;

  const DefaultPlaylistCard({
    super.key,
    required this.colorScheme,
    required this.icon,
    required this.title,
    this.text = '',
    this.width,
    this.displayLoadingIndicator = false,
    this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    final theme = context.theme;
    final textTheme = theme.textTheme;
    return NamidaInkWell(
      borderRadius: 12.0,
      bgColor: Color.alphaBlend(colorScheme.withAlpha(10), theme.cardColor),
      onTap: onTap,
      padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 12.0),
      child: Row(
        children: [
          Icon(
            icon,
            color: colorScheme.withAlpha(200),
          ),
          const SizedBox(width: 10.0),
          Expanded(
            child: Text(
              title,
              style: textTheme.displayMedium?.copyWith(color: Color.alphaBlend(colorScheme.withAlpha(10), textTheme.displayMedium!.color!)),
              softWrap: false,
              overflow: TextOverflow.fade,
            ),
          ),
          const SizedBox(width: 6.0),
          displayLoadingIndicator
              ? const LoadingIndicator()
              : Text(
                  text,
                  style: textTheme.displayMedium?.copyWith(color: Color.alphaBlend(colorScheme.withAlpha(30), textTheme.displayMedium!.color!)),
                ),
          const SizedBox(width: 2.0),
        ],
      ),
    );
  }
}

class NamidaCircularPercentage extends StatelessWidget {
  final double size;
  final double percentage;
  final String heroTag;
  final bool hero;

  const NamidaCircularPercentage({
    super.key,
    this.size = 48.0,
    required this.hero,
    required this.percentage,
    required this.heroTag,
  });

  @override
  Widget build(BuildContext context) {
    final theme = context.theme;
    final textTheme = theme.textTheme;
    final circularSlider = SleekCircularSlider(
      appearance: CircularSliderAppearance(
        customWidths: CustomSliderWidths(
          trackWidth: size / 24,
          progressBarWidth: size / 12,
        ),
        customColors: CustomSliderColors(
          dotColor: Colors.transparent,
          trackColor: theme.cardTheme.color,
          dynamicGradient: true,
          progressBarColors: [
            theme.colorScheme.primary.withAlpha(100),
            Colors.transparent,
            theme.colorScheme.secondary.withAlpha(100),
            Colors.transparent,
            theme.colorScheme.primary.withAlpha(100),
          ],
          hideShadow: true,
        ),
        size: size,
        spinnerMode: true,
      ),
    );
    return Stack(
      alignment: Alignment.center,
      children: [
        hero
            ? Hero(
                tag: heroTag,
                child: circularSlider,
              )
            : circularSlider,
        if (percentage.isFinite)
          Text(
            "${((percentage).clampDouble(0.01, 1.0) * 100).toStringAsFixed(0)}%",
            style: textTheme.displaySmall?.copyWith(fontSize: size / 3.2),
          ),
      ],
    );
  }
}

class NamidaReorderableActiveListView<E> extends StatefulWidget {
  final List<E> enumValues;
  final RxBaseCore<List<E>> rxList;
  final String Function(E item) toText;
  final IconData Function(E item) toIcon;
  final IconData? Function(E item)? toSecondaryIcon;
  final void Function(List<E> activeItems) onSave;
  final void Function(int i, List<E> activeItems)? onItemRemoved;
  final int minimumItems;

  const NamidaReorderableActiveListView({
    super.key,
    required this.enumValues,
    required this.rxList,
    required this.toText,
    required this.toIcon,
    this.toSecondaryIcon,
    required this.onSave,
    this.onItemRemoved,
    this.minimumItems = 3,
  });

  @override
  State<NamidaReorderableActiveListView<E>> createState() => _NamidaReorderableActiveListViewState<E>();
}

class _NamidaReorderableActiveListViewState<E> extends State<NamidaReorderableActiveListView<E>> {
  late final RxList<({E item, bool active})> _itemsRx;

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

  void _initializeList() {
    final combined = LinkedHashSet<({E item, bool active})>(equals: (p0, p1) => p0.item == p1.item, hashCode: (p0) => p0.item.hashCode);
    for (final e in widget.rxList.value) {
      combined.add((item: e, active: true));
    }
    for (final e in widget.enumValues) {
      combined.add((item: e, active: false));
    }

    _itemsRx = combined.toList().obs;
  }

  @override
  void dispose() {
    _itemsRx.close();
    super.dispose();
  }

  List<E> getActiveItems() => _itemsRx.value.where((e) => e.active).map((e) => e.item).toList();

  void _onTap(int i) {
    final (:item, :active) = _itemsRx.value[i];
    if (active) {
      if (_itemsRx.value.where((e) => e.active).length <= widget.minimumItems) {
        showMinimumItemsSnack(widget.minimumItems);
        return;
      }
    }
    _itemsRx[i] = (item: item, active: !active);
    final newItems = getActiveItems();
    widget.onSave(newItems);

    if (!_itemsRx.value[i].active) {
      widget.onItemRemoved?.call(i, newItems);
    }
  }

  void _onReorder(int oldIndex, int newIndex) {
    if (newIndex > oldIndex) newIndex -= 1;
    final entry = _itemsRx.value.removeAt(oldIndex);
    _itemsRx.value.insert(newIndex, entry);
    _itemsRx.refresh();
    widget.onSave(getActiveItems());
  }

  @override
  Widget build(BuildContext context) {
    return ObxO(
      rx: _itemsRx,
      builder: (context, items) => NamidaListView(
        showScrollbarOnStart: true,
        itemExtent: null,
        listBottomPadding: 0,
        itemCount: items.length,
        onReorder: _onReorder,
        itemBuilder: (context, i) {
          final (:item, :active) = items[i];
          final mainIcon = widget.toIcon(item);
          final secondaryIcon = widget.toSecondaryIcon?.call(item);
          return Padding(
            key: ValueKey(item),
            padding: const EdgeInsets.all(3.0),
            child: ListTileWithCheckMark(
              title: "${i + 1}. ${widget.toText(item)}",
              icon: secondaryIcon == null ? mainIcon : null,
              leading: secondaryIcon == null
                  ? null
                  : StackedIcon(
                      baseIcon: mainIcon,
                      secondaryIcon: secondaryIcon,
                      disableColor: true,
                    ),
              active: active,
              onTap: () => _onTap(i),
            ),
          );
        },
      ),
    );
  }
}

class NamidaListView extends StatelessWidget {
  final Widget Function(Widget list)? listBuilder;
  final Widget Function(BuildContext context, int i) itemBuilder;
  final ReorderCallback? onReorder;
  final VoidCallback? onReorderCancel;
  final void Function(int index)? onReorderStart;
  final void Function(int index)? onReorderEnd;
  final Widget? header;
  final Widget? stickyHeader;
  final Widget Function(double maxWidth)? infoBox;
  final List<Widget>? widgetsInColumn;
  final double? listBottomPadding;
  final double? itemExtent;
  final fr.ItemExtentBuilder? itemExtentBuilder;
  final ScrollController? scrollController;
  final int itemCount;
  final ScrollPhysics? physics;
  final double scrollStep;
  final Map<String, int> scrollConfig;
  final bool reverse;
  final bool showScrollbarOnStart;

  const NamidaListView({
    super.key,
    this.listBuilder,
    this.header,
    this.stickyHeader,
    this.infoBox,
    this.widgetsInColumn,
    this.listBottomPadding,
    this.onReorder,
    this.onReorderCancel,
    required this.itemBuilder,
    required this.itemCount,
    required this.itemExtent,
    this.itemExtentBuilder,
    this.scrollController,
    this.onReorderStart,
    this.onReorderEnd,
    this.physics,
    this.scrollStep = 0,
    this.scrollConfig = const {},
    this.reverse = false,
    this.showScrollbarOnStart = false,
  });

  @override
  Widget build(BuildContext context) {
    final list = onReorder != null
        ? NamidaSliverReorderableList(
            itemExtent: itemExtent,
            itemExtentBuilder: itemExtentBuilder,
            itemBuilder: itemBuilder,
            itemCount: itemCount,
            onReorder: onReorder!,
            onReorderCancel: onReorderCancel,
            onReorderStart: onReorderStart,
            onReorderEnd: onReorderEnd,
          )
        : itemExtent != null
        ? SliverFixedExtentList.builder(
            itemExtent: itemExtent!,
            itemBuilder: itemBuilder,
            itemCount: itemCount,
          )
        : itemExtentBuilder != null
        ? SliverVariedExtentList.builder(
            itemExtentBuilder: itemExtentBuilder!,
            itemBuilder: itemBuilder,
            itemCount: itemCount,
          )
        : SuperSliverList.builder(
            itemBuilder: itemBuilder,
            itemCount: itemCount,
          );
    return NamidaListViewRaw(
      reverse: reverse,
      showScrollbarOnStart: showScrollbarOnStart,
      scrollController: scrollController,
      scrollConfig: scrollConfig,
      scrollStep: scrollStep,
      header: header,
      stickyHeader: stickyHeader,
      infoBox: infoBox,
      slivers: [list],
      builder:
          listBuilder ??
          (list) => widgetsInColumn != null
              ? Column(
                  children: [
                    ...widgetsInColumn!,
                    Expanded(child: list),
                  ],
                )
              : list,
      listBottomPadding: listBottomPadding,
      physics: physics,
    );
  }
}

class NamidaListViewRaw extends StatefulWidget {
  final List<Widget> slivers;
  final Widget Function(Widget list)? builder;
  final Widget Function(double maxWidth)? infoBox;
  final Widget? header;
  final Widget? stickyHeader;
  final Widget? footer;

  /// defaults to [Dimensions.globalBottomPaddingTotal]
  final double? listBottomPadding;
  final ScrollController? scrollController;
  final ScrollPhysics? physics;
  final Map<String, int> scrollConfig;
  final double scrollStep;
  final Axis scrollDirection;
  final bool reverse;
  final bool showScrollbarOnStart;

  const NamidaListViewRaw({
    super.key,
    required this.slivers,
    this.builder,
    this.infoBox,
    this.header,
    this.stickyHeader,
    this.footer,
    this.listBottomPadding,
    this.scrollController,
    this.physics,
    this.scrollConfig = const {},
    this.scrollStep = 0,
    this.scrollDirection = Axis.vertical,
    this.reverse = false,
    this.showScrollbarOnStart = false,
  });

  @override
  State<NamidaListViewRaw> createState() => _NamidaListViewRawState();
}

class _NamidaListViewRawState extends State<NamidaListViewRaw> {
  ScrollController? _scrollController;

  @override
  void initState() {
    _scrollController = widget.scrollController ?? NamidaScrollController.create();
    super.initState();
  }

  @override
  void dispose() {
    if (widget.scrollController == null) _scrollController?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    double? start = widget.header == null ? null : 0.0;
    double? end = widget.footer == null ? null : 0.0;
    if (widget.reverse) {
      (start, end) = (end, start);
    }

    final padding = EdgeInsets.only(bottom: this.widget.listBottomPadding ?? Dimensions.globalBottomPaddingTotal);
    final EdgeInsets startPadding, endPadding, listPadding;
    (startPadding, endPadding, listPadding) = switch (widget.scrollDirection) {
      Axis.horizontal || Axis.vertical when (start ?? end) == null => (EdgeInsets.zero, EdgeInsets.zero, padding),
      Axis.horizontal => (padding.copyWith(left: 0), padding.copyWith(right: 0), padding.copyWith(left: start, right: end)),
      Axis.vertical => (padding.copyWith(top: 0), padding.copyWith(bottom: 0), padding.copyWith(top: start, bottom: end)),
    };
    final (EdgeInsets headerPadding, EdgeInsets footerPadding) = widget.reverse ? (startPadding, endPadding) : (endPadding, startPadding);

    final showSubpageInfoAtSide = Dimensions.inst.showSubpageInfoAtSideContext(context);
    final displayHeaderAtTop = widget.header != null;
    final displayStickyHeaderAtTop = widget.stickyHeader != null;
    final displayInfoBoxAtTop = widget.infoBox != null && !showSubpageInfoAtSide;
    final displayInfoBoxAtSide = widget.infoBox != null && showSubpageInfoAtSide;
    Widget listW = ClipRect(
      child: SmoothCustomScrollView(
        scrollDirection: widget.scrollDirection,
        controller: _scrollController,
        physics: widget.physics,
        reverse: widget.reverse,
        slivers: <Widget>[
          if (displayInfoBoxAtTop)
            SliverPadding(
              padding: headerPadding,
              sliver: SliverToBoxAdapter(
                child: widget.infoBox?.call(Dimensions.inst.availableAppContentWidth),
              ),
            ),
          if (displayHeaderAtTop)
            SliverPadding(
              padding: displayInfoBoxAtTop ? EdgeInsets.zero : headerPadding,
              sliver: SliverToBoxAdapter(
                child: widget.header,
              ),
            ),
          if (displayStickyHeaderAtTop)
            SliverPadding(
              padding: displayInfoBoxAtTop ? EdgeInsets.zero : headerPadding,
              sliver: PinnedHeaderSliver(
                child: widget.stickyHeader,
              ),
            ),
          SliverPadding(
            padding: EdgeInsets.only(top: listPadding.top),
          ),
          ...widget.slivers,
          SliverPadding(
            padding: EdgeInsets.only(bottom: listPadding.bottom),
          ),
          if (widget.footer != null)
            SliverPadding(
              padding: footerPadding,
              sliver: SliverToBoxAdapter(child: widget.footer),
            ),
        ],
      ),
    );
    if (displayInfoBoxAtSide) {
      listW = Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          ConstrainedBox(
            constraints: BoxConstraints(maxWidth: Dimensions.inst.sideInfoMaxWidth),
            child: Align(
              alignment: Alignment.topCenter,
              child: SmoothSingleChildScrollView(
                child: widget.infoBox?.call(Dimensions.inst.sideInfoMaxWidth),
              ),
            ),
          ),
          Expanded(
            child: listW,
          ),
        ],
      );
    }
    return NamidaScrollbar(
      controller: _scrollController,
      scrollStep: widget.scrollStep,
      showOnStart: widget.showScrollbarOnStart,
      child: widget.builder?.call(listW) ?? listW,
    );
  }
}

class NamidaSliverReorderableList extends StatelessWidget {
  final Widget Function(BuildContext context, int i) itemBuilder;
  final ReorderCallback onReorder;
  final VoidCallback? onReorderCancel;
  final void Function(int index)? onReorderStart;
  final void Function(int index)? onReorderEnd;
  final double? itemExtent;
  final fr.ItemExtentBuilder? itemExtentBuilder;
  final int itemCount;

  const NamidaSliverReorderableList({
    super.key,
    required this.itemBuilder,
    required this.onReorder,
    this.onReorderCancel,
    this.onReorderStart,
    this.onReorderEnd,
    this.itemExtent,
    this.itemExtentBuilder,
    required this.itemCount,
  });

  Widget _reorderableItemBuilder(BuildContext context, int index) {
    final Widget item = itemBuilder(context, index);
    return ReorderableDelayedDragStartListener(
      delay: kLongPressTimeout,
      key: item.key!,
      index: index,
      child: item,
    );
  }

  @override
  Widget build(BuildContext context) {
    return SliverReorderableList(
      itemExtent: itemExtent,
      itemExtentBuilder: itemExtentBuilder,
      itemBuilder: _reorderableItemBuilder,
      itemCount: itemCount,
      onReorder: onReorder,
      onReorderCancel: onReorderCancel,
      proxyDecorator: (child, index, animation) => child,
      onReorderStart: onReorderStart,
      onReorderEnd: onReorderEnd,
      autoScrollerVelocityScalar: 500,
    );
  }
}

class NamidaTracksList extends StatelessWidget {
  final List<Selectable>? queue;
  final int queueLength;
  final Widget Function(BuildContext context, int i)? itemBuilder;
  final fr.ItemExtentBuilder? itemExtentBuilder;
  final Widget? header;
  final Widget? stickyHeader;
  final Widget Function(double maxWidth)? infoBox;
  final Widget? footer;
  final List<Widget>? widgetsInColumn;
  final ScrollController? scrollController;
  final double? listBottomPadding;
  final bool Function()? isTrackSelectable;
  final void Function()? onTap;
  final ScrollPhysics? physics;
  final QueueSourceBase queueSource;
  final bool displayTrackNumber;
  final bool shouldAnimate;
  final String Function(Selectable track)? thirdLineText;
  final Map<String, int> scrollConfig;

  const NamidaTracksList({
    super.key,
    this.queue,
    this.itemBuilder,
    this.itemExtentBuilder,
    this.header,
    this.stickyHeader,
    required this.infoBox,
    this.footer,
    this.widgetsInColumn,
    this.scrollController,
    this.listBottomPadding,
    required this.queueLength,
    this.isTrackSelectable,
    this.onTap,
    this.physics,
    required this.queueSource,
    this.displayTrackNumber = false,
    this.shouldAnimate = true,
    this.thirdLineText,
    this.scrollConfig = const {},
  });

  @override
  Widget build(BuildContext context) {
    if (itemBuilder != null) {
      return AnimationLimiter(
        child: NamidaListView(
          infoBox: infoBox,
          header: header,
          stickyHeader: stickyHeader,
          widgetsInColumn: widgetsInColumn,
          scrollController: scrollController,
          itemCount: queueLength,
          itemExtent: Dimensions.inst.trackTileItemExtent,
          itemExtentBuilder: itemExtentBuilder,
          listBottomPadding: listBottomPadding,
          physics: physics,
          scrollConfig: scrollConfig,
          itemBuilder: itemBuilder!,
        ),
      );
    } else if (queue != null) {
      final queue = this.queue!;
      final thirdLineText = this.thirdLineText;
      return AnimationLimiter(
        child: TrackTilePropertiesProvider(
          configs: TrackTilePropertiesConfigs(
            queueSource: queueSource,
            selectable: isTrackSelectable,
            displayTrackNumber: displayTrackNumber,
          ),
          builder: (properties) {
            return NamidaListView(
              infoBox: infoBox,
              header: header,
              stickyHeader: stickyHeader,
              widgetsInColumn: widgetsInColumn,
              scrollController: scrollController,
              itemCount: queueLength,
              itemExtent: Dimensions.inst.trackTileItemExtent,
              listBottomPadding: listBottomPadding,
              physics: physics,
              scrollConfig: scrollConfig,
              itemBuilder: (context, i) {
                final track = queue[i];
                return AnimatingTile(
                  key: ValueKey(i),
                  position: i,
                  shouldAnimate: shouldAnimate,
                  child: TrackTile(
                    properties: properties,
                    index: i,
                    trackOrTwd: track,
                    tracks: queue,
                    onTap: onTap,
                    thirdLineText: thirdLineText == null ? null : thirdLineText(track),
                  ),
                );
              },
            );
          },
        ),
      );
    } else {
      return const Text('PASS A QUEUE OR USE ITEM BUILDER');
    }
  }
}

class NamidaSupportButton extends StatelessWidget {
  final String? title;
  final bool closeDialog;
  const NamidaSupportButton({super.key, this.title, this.closeDialog = true});

  @override
  Widget build(BuildContext context) {
    return NamidaButton(
      icon: Broken.heart,
      text: title ?? lang.support,
      onTap: () {
        closeDialog.closeDialog();
        NamidaLinkUtils.openLink(AppSocial.DONATE_BUY_ME_A_COFFEE);
      },
    );
  }
}

class BackgroundWrapper extends StatelessWidget {
  final Widget child;
  const BackgroundWrapper({super.key, required this.child});

  @override
  Widget build(BuildContext context) {
    final theme = context.theme;
    return Material(
      color: theme.scaffoldBackgroundColor,
      child: child,
    );
  }
}

class NamidaInkWell extends StatelessWidget {
  final Color? bgColor;
  final VoidCallback? onTap;
  final VoidCallback? onLongPress;
  final bool enableSecondaryTap;
  final double borderRadius;
  final EdgeInsetsGeometry padding;
  final BoxDecoration decoration;
  final Widget? child;
  final int animationDurationMS;
  final EdgeInsetsGeometry? margin;
  final double? width;
  final double? height;
  final AlignmentGeometry? alignment;

  /// Setting this to [true] will force the [borderRadius] to be [0.0].
  final bool transparentHighlight;
  const NamidaInkWell({
    super.key,
    this.bgColor,
    this.onTap,
    this.onLongPress,
    this.enableSecondaryTap = false,
    this.borderRadius = 12.0,
    this.padding = EdgeInsets.zero,
    this.decoration = const BoxDecoration(),
    this.child,
    this.transparentHighlight = false,
    this.animationDurationMS = 0,
    this.margin,
    this.width,
    this.height,
    this.alignment,
  });

  @override
  Widget build(BuildContext context) {
    final theme = context.theme;
    final borderR =
        decoration.borderRadius?.resolve(Directionality.of(context)) ?? (transparentHighlight || borderRadius == 0 ? null : BorderRadius.circular(borderRadius.multipliedRadius));
    final highlightColor = transparentHighlight ? Colors.transparent : Color.alphaBlend(theme.scaffoldBackgroundColor.withAlpha(20), theme.highlightColor);
    final bgColor = this.bgColor ?? decoration.color;
    final decorationFinal = BoxDecoration(
      color: bgColor,
      borderRadius: borderR,
      backgroundBlendMode: decoration.backgroundBlendMode,
      boxShadow: decoration.boxShadow,
      gradient: decoration.gradient,
      shape: decoration.shape,
      image: decoration.image,
    );

    final foregroundDecorationFinal = BoxDecoration(
      border: decoration.border,
      borderRadius: borderR,
    );
    final childFinal = Material(
      clipBehavior: Clip.none,
      type: MaterialType.transparency,
      child: InkWell(
        borderRadius: borderR,
        hoverColor: highlightColor,
        highlightColor: highlightColor,
        onTap: onTap,
        onLongPress: onLongPress,
        onSecondaryTap: enableSecondaryTap ? onLongPress ?? onTap : null,
        child: SizedBox(
          height: height,
          width: width,
          child: Padding(
            padding: padding,
            child: child,
          ),
        ),
      ),
    );
    return animationDurationMS > 0
        ? AnimatedContainer(
            alignment: alignment,
            margin: margin,
            duration: Duration(milliseconds: animationDurationMS),
            decoration: decorationFinal,
            foregroundDecoration: foregroundDecorationFinal,
            clipBehavior: Clip.none,
            child: childFinal,
          )
        : Container(
            alignment: alignment,
            margin: margin,
            decoration: decorationFinal,
            foregroundDecoration: foregroundDecorationFinal,
            clipBehavior: Clip.none,
            child: childFinal,
          );
  }
}

class NamidaInkWellButton extends StatelessWidget {
  final Color? bgColor;
  final VoidCallback? onTap;
  final double borderRadius;
  final int animationDurationMS;
  final IconData? icon;
  final double iconSize;
  final String text;
  final bool enabled;
  final bool showLoadingWhenDisabled;
  final bool disableWhenLoading;
  final double sizeMultiplier;
  final double paddingMultiplier;
  final Widget? leading;
  final Widget? trailing;
  final BoxDecoration decoration;

  const NamidaInkWellButton({
    super.key,
    this.bgColor,
    this.onTap,
    this.borderRadius = 10.0,
    this.animationDurationMS = 250,
    required this.icon,
    this.iconSize = 18.0,
    required this.text,
    this.enabled = true,
    this.showLoadingWhenDisabled = true,
    this.disableWhenLoading = true,
    this.sizeMultiplier = 1.0,
    this.paddingMultiplier = 1.0,
    this.leading,
    this.trailing,
    this.decoration = const BoxDecoration(),
  });

  @override
  Widget build(BuildContext context) {
    final theme = context.theme;
    final textTheme = theme.textTheme;
    final itemsColor = theme.colorScheme.onSurface.withOpacityExt(0.8);
    final textGood = text.isNotEmpty;
    return IgnorePointer(
      ignoring: !enabled && disableWhenLoading,
      child: AnimatedOpacity(
        opacity: enabled ? 1.0 : 0.6,
        duration: Duration(milliseconds: animationDurationMS),
        child: NamidaInkWell(
          animationDurationMS: animationDurationMS,
          borderRadius: borderRadius * sizeMultiplier,
          padding: EdgeInsets.symmetric(horizontal: 12.0 * sizeMultiplier * paddingMultiplier, vertical: 6.0 * sizeMultiplier * paddingMultiplier),
          bgColor: bgColor ?? theme.colorScheme.secondaryContainer.withOpacityExt(0.5),
          decoration: decoration,
          onTap: onTap,
          enableSecondaryTap: false,
          child: Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              ?leading,
              if (!enabled && showLoadingWhenDisabled) ...[
                const LoadingIndicator(boxHeight: 18.0),
                SizedBox(width: 6.0 * sizeMultiplier),
              ] else if (icon != null) ...[
                Icon(
                  icon,
                  size: iconSize * sizeMultiplier,
                  color: itemsColor,
                ),
              ],
              if (textGood) ...[
                SizedBox(width: 6.0 * sizeMultiplier),
                Flexible(
                  child: Text(
                    text,
                    style: textTheme.displayMedium?.copyWith(
                      color: itemsColor,
                      fontSize: (15.0 * sizeMultiplier),
                    ),
                  ),
                ),
                if (textGood) const SizedBox(width: 4.0),
              ],
              ?trailing,
            ],
          ),
        ),
      ),
    );
  }
}

class HistoryJumpToDayIcon<T extends ItemWithDate, E> extends StatelessWidget {
  final HistoryManager<T, E> controller;
  final ({double itemExtent, double dayHeaderExtent}) Function() itemExtentAndDayHeaderExtent;
  final bool considerInfoBoxPadding;

  const HistoryJumpToDayIcon({
    super.key,
    required this.controller,
    required this.itemExtentAndDayHeaderExtent,
    required this.considerInfoBoxPadding,
  });

  double get topPadding => considerInfoBoxPadding && !Dimensions.inst.showSubpageInfoAtSide ? 64.0 : 0.0;

  DateTime? getCurrentDateFromScrollPosition() {
    final currentScrolledDay = getCurrentDayFromScrollPosition();
    return currentScrolledDay == null ? null : DateTime(1970).add(Duration(days: currentScrolledDay));
  }

  int? getCurrentDayFromScrollPosition() {
    final info = itemExtentAndDayHeaderExtent();
    final topPadding = this.topPadding;
    final currentScrolledDay = controller.currentScrollPositionToDay(info.itemExtent, info.dayHeaderExtent, topPadding: topPadding);
    return currentScrolledDay;
  }

  void scrollToDate(DateTime dateToScrollTo) {
    final dayToScrollTo = dateToScrollTo.toDaysSince1970();
    final days = controller.historyDays.toList();
    days.removeWhere((element) => element <= dayToScrollTo);
    double totalScrollOffset = controller.daysToSectionExtent(days);
    controller.scrollController.jumpTo(totalScrollOffset + topPadding - 48.0);
  }

  @override
  Widget build(BuildContext context) {
    return NamidaAppBarIcon(
      icon: Broken.calendar,
      tooltip: () => lang.jumpToDay,
      onPressed: () {
        final initialDate = getCurrentDateFromScrollPosition();
        showCalendarDialog(
          historyController: controller,
          title: lang.jumpToDay,
          buttonText: lang.jump,
          calendarType: NamidaCalendarDatePickerType.single,
          useHistoryDates: true,
          initialDate: initialDate,
          onGenerate: (dates) {
            NamidaNavigator.inst.closeDialog();
            final dateToScrollTo = dates.firstOrNull;
            if (dateToScrollTo != null) scrollToDate(dateToScrollTo);
          },
        );
      },
    );
  }
}

class BetweenDatesTextButton extends StatefulWidget {
  final bool useHistoryDates;
  final void Function(List<DateTime> dates) onConfirm;
  final bool maxToday;
  final int tracksLength;

  const BetweenDatesTextButton({
    super.key,
    required this.useHistoryDates,
    required this.onConfirm,
    this.maxToday = false,
    this.tracksLength = 0,
  });

  @override
  State<BetweenDatesTextButton> createState() => _BetweenDatesTextButtonState();
}

class _BetweenDatesTextButtonState extends State<BetweenDatesTextButton> {
  DateTime? oldestDate;
  DateTime? newestDate;

  @override
  Widget build(BuildContext context) {
    final textTheme = context.textTheme;
    final textWidget = Text(lang.betweenDates);

    return TextButton.icon(
      onPressed: () {
        showCalendarDialog(
          useHistoryDates: widget.useHistoryDates,
          lastDate: widget.maxToday ? DateTime.now() : null,
          title: lang.betweenDates,
          buttonText: lang.confirm,
          onGenerate: (dates) {
            oldestDate = dates.firstOrNull;
            newestDate = dates.lastOrNull;
            widget.onConfirm(dates);
            setState(() {});
          },
        );
      },
      icon: const Icon(Broken.calendar_1),
      label: oldestDate == null || newestDate == null
          ? textWidget
          : Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Row(
                  children: [
                    textWidget,
                    const SizedBox(width: 6.0),
                    if (widget.tracksLength != 0)
                      NamidaButtonText(
                        "(${widget.tracksLength.displayTrackKeyword})",
                        style: textTheme.displaySmall,
                      ),
                  ],
                ),
                NamidaButtonText(
                  "${oldestDate?.dateFormattedOriginal} → ${newestDate?.dateFormattedOriginal}",
                  style: textTheme.displaySmall,
                ),
              ],
            ),
    );
  }
}

/// Obx((context) => showIf.value ? child : const SizedBox(context));
class ObxShow extends StatelessWidget {
  final RxBase<bool> showIf;
  final Widget child;

  const ObxShow({
    super.key,
    required this.showIf,
    required this.child,
  });

  @override
  Widget build(BuildContext context) {
    return ObxO(
      rx: showIf,
      builder: (context, show) => show ? child : const SizedBox(),
    );
  }
}

class NamidaHero extends StatelessWidget {
  final Object? tag;
  final Widget child;
  final bool enabled;

  const NamidaHero({
    super.key,
    required this.tag,
    required this.child,
    this.enabled = true,
  });

  static final fadeAnimation2Convert = Animatable.fromCallback((value) => (value * (1 / 0.2)).clampDouble(0, 1));
  static final fadeAnimation1Convert = Animatable.fromCallback((value) => 1.0 - (value * 12 - 11).clampDouble(0, 1));

  static Widget _customHeroFlightShuttleBuilder(
    BuildContext flightContext,
    Animation<double> animation,
    HeroFlightDirection flightDirection,
    BuildContext fromHeroContext,
    BuildContext toHeroContext,
  ) {
    final Hero toHero = toHeroContext.widget as Hero;
    final Hero fromHero = fromHeroContext.widget as Hero;

    final (Hero hero1, Hero hero2) = switch (flightDirection) {
      HeroFlightDirection.push => (fromHero, toHero),
      HeroFlightDirection.pop => (toHero, fromHero),
    };

    final MediaQueryData? toMediaQueryData = MediaQuery.maybeOf(toHeroContext);
    final MediaQueryData? fromMediaQueryData = MediaQuery.maybeOf(fromHeroContext);

    if (toMediaQueryData == null || fromMediaQueryData == null) {
      return toHero.child;
    }

    final EdgeInsets fromHeroPadding = fromMediaQueryData.padding;
    final EdgeInsets toHeroPadding = toMediaQueryData.padding;

    final fadeAnimation2 = animation.drive(fadeAnimation2Convert);
    final fadeAnimation1 = animation.drive(fadeAnimation1Convert);

    final stackChild = Stack(
      alignment: Alignment.center,
      fit: StackFit.passthrough,
      children: [
        FadeTransition(
          opacity: fadeAnimation2,
          child: hero2.child,
        ),
        FadeTransition(
          opacity: fadeAnimation1,
          child: hero1.child,
        ),
      ],
    );

    return AnimatedBuilder(
      animation: animation,
      builder: (context, child) {
        return MediaQuery(
          data: toMediaQueryData.copyWith(
            padding: (flightDirection == HeroFlightDirection.push)
                ? EdgeInsetsTween(
                    begin: fromHeroPadding,
                    end: toHeroPadding,
                  ).evaluate(animation)
                : EdgeInsetsTween(
                    begin: toHeroPadding,
                    end: fromHeroPadding,
                  ).evaluate(animation),
          ),
          child: stackChild,
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return enabled && tag != null
        ? Hero(
            tag: tag!,
            // -- quite expensive to animate 2 fade transitions.
            flightShuttleBuilder: settings.performanceMode.value == PerformanceMode.highPerformance ? null : _customHeroFlightShuttleBuilder,
            child: child,
          )
        : child;
  }
}

class NamidaTooltip extends StatelessWidget {
  final String Function()? message;
  final bool? preferBelow;
  final TooltipTriggerMode? triggerMode;
  final Widget child;

  const NamidaTooltip({
    super.key,
    required this.message,
    this.preferBelow,
    this.triggerMode,
    required this.child,
  });

  @override
  Widget build(BuildContext context) {
    if (message == null) return child;
    return Tooltip(
      message: message,
      preferBelow: preferBelow,
      triggerMode: triggerMode,
      child: child,
    );
  }
}

class NamidaAnimatedSwitcher extends StatelessWidget {
  final Widget firstChild;
  final Widget secondChild;
  final bool showFirst;
  final int durationMS;
  final int? reverseDurationMS;
  final Curve? firstCurve;
  final Curve? secondCurve;
  final Curve? sizeCurve;
  final Curve? allCurves;

  const NamidaAnimatedSwitcher({
    super.key,
    required this.firstChild,
    required this.secondChild,
    required this.showFirst,
    this.durationMS = 400,
    this.reverseDurationMS,
    this.firstCurve,
    this.secondCurve,
    this.sizeCurve,
    this.allCurves,
  });

  @override
  Widget build(BuildContext context) {
    final allCurves = this.allCurves ?? Curves.fastLinearToSlowEaseIn;
    return AnimatedCrossFade(
      firstChild: firstChild,
      secondChild: secondChild,
      crossFadeState: showFirst ? CrossFadeState.showFirst : CrossFadeState.showSecond,
      duration: Duration(milliseconds: durationMS),
      reverseDuration: Duration(milliseconds: reverseDurationMS ?? durationMS),
      firstCurve: firstCurve ?? allCurves,
      secondCurve: secondCurve ?? allCurves,
      sizeCurve: sizeCurve ?? allCurves,
      layoutBuilder: (topChild, topChildKey, bottomChild, bottomChildKey) {
        return Stack(
          clipBehavior: Clip.none,
          alignment: Alignment.center,
          children: <Widget>[
            Positioned(
              key: bottomChildKey,
              top: 0,
              child: bottomChild,
            ),
            Positioned(
              key: topChildKey,
              child: topChild,
            ),
          ],
        );
      },
    );
  }
}

class CustomAnimatedSwitcher extends StatelessWidget {
  final Duration duration;
  final Duration? reverseDuration;
  final Curve switchInCurve;
  final Curve switchOutCurve;
  final AnimatedSwitcherLayoutBuilder layoutBuilder;
  final Widget? child;

  const CustomAnimatedSwitcher({
    super.key,
    this.child,
    required this.duration,
    this.reverseDuration,
    this.switchInCurve = Curves.linear,
    this.switchOutCurve = Curves.linear,
    this.layoutBuilder = AnimatedSwitcher.defaultLayoutBuilder,
  });

  static Widget defaultTransitionBuilder(Widget child, Animation<double> animation) {
    return FadeTransition(
      key: ValueKey(animation),
      opacity: animation,
      child: child,
    );
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedSwitcher(
      duration: const Duration(milliseconds: 300),
      layoutBuilder: layoutBuilder,
      transitionBuilder: CustomAnimatedSwitcher.defaultTransitionBuilder,
      child: child,
    );
  }
}

class ShimmerWrapper extends StatelessWidget {
  final bool shimmerEnabled;
  final Widget child;
  final int fadeDurationMS;
  final int shimmerDelayMS;
  final int shimmerDurationMS;
  final bool transparent;

  const ShimmerWrapper({
    super.key,
    required this.shimmerEnabled,
    required this.child,
    this.fadeDurationMS = 600,
    this.shimmerDelayMS = 400,
    this.shimmerDurationMS = 700,
    this.transparent = true,
  });

  @override
  Widget build(BuildContext context) {
    final color = transparent ? Colors.transparent : context.theme.cardColor.withAlpha(120);
    return CustomAnimatedSwitcher(
      duration: fadeDurationMS.ms,
      child: shimmerEnabled
          ? Animate(
              onPlay: (controller) => controller.repeat(),
              effects: [
                ShimmerEffect(
                  delay: shimmerDelayMS.ms,
                  duration: shimmerDurationMS.ms,
                  colors: [
                    color,
                    const Color(0x80FFFFFF),
                    color,
                  ],
                ),
              ],
              child: child,
            )
          : child,
    );
  }
}

class LazyLoadListView extends StatefulWidget {
  final ScrollController? scrollController;
  final int extend;
  final FutureOr<bool> Function() onReachingEnd;
  final Widget Function(ScrollController controller) listview;
  final bool requiresNetwork;

  const LazyLoadListView({
    super.key,
    this.scrollController,
    this.extend = 400,
    required this.onReachingEnd,
    required this.listview,
    this.requiresNetwork = true,
  });

  @override
  State<LazyLoadListView> createState() => _LazyLoadListViewState();
}

class _LazyLoadListViewState extends State<LazyLoadListView> {
  ScrollPosition? get _getControllerPositionSafe => controller.positions.firstOrNull;

  late final ScrollController controller;
  bool _isExecuting = false;

  bool? _lastWasSuccess;
  bool _isInExtendRange = false; // prevent re-execution if latest failed & still in range.

  void _scrollListener() async {
    if (_isExecuting) return;

    final position = controller.positions.firstOrNull;
    if (position != null && position.pixels >= position.maxScrollExtent - widget.extend) {
      if (!position.outOfRange) {
        if (_lastWasSuccess == false && _isInExtendRange) return;
        _isInExtendRange = true;
        if (widget.requiresNetwork && !ConnectivityController.inst.hasConnection) return;
        _isExecuting = true;
        _lastWasSuccess = await widget.onReachingEnd();
        _isExecuting = false;
      }
    } else {
      _isInExtendRange = false;
    }
  }

  @override
  void initState() {
    super.initState();
    controller = (widget.scrollController ?? NamidaScrollController.create())..addListener(_scrollListener);
  }

  Future<void> _fetchIfNotScrollable() async {
    if (_isExecuting) return;
    final position = _getControllerPositionSafe;
    if (position == null) return;
    if (position.maxScrollExtent > 0) return;
    if (widget.requiresNetwork && !ConnectivityController.inst.hasConnection) return;

    _isExecuting = true;
    _lastWasSuccess = await widget.onReachingEnd();
    _isExecuting = false;

    if (_lastWasSuccess == true) {
      // -- keep fetching until list is scrollable or source is exhausted
      WidgetsBinding.instance.addPostFrameCallback((_) => _fetchIfNotScrollable());
    }
  }

  @override
  void dispose() {
    controller.removeListener(_scrollListener);
    if (widget.scrollController == null) {
      controller.dispose();
    }
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return NotificationListener<ScrollMetricsNotification>(
      onNotification: (notification) {
        if (notification.metrics.maxScrollExtent == 0) {
          WidgetsBinding.instance.addPostFrameCallback(
            (_) {
              if (_getControllerPositionSafe?.maxScrollExtent == 0) {
                // -- ensure still 0
                _fetchIfNotScrollable();
              }
            },
          );
        }
        return false;
      },
      child: widget.listview(controller),
    );
  }
}

class NamidaAspectRatio extends StatelessWidget {
  final double? aspectRatio;
  final Widget child;

  const NamidaAspectRatio({
    super.key,
    required this.aspectRatio,
    required this.child,
  });

  @override
  Widget build(BuildContext context) {
    return aspectRatio == null
        ? child
        : AspectRatio(
            aspectRatio: aspectRatio!,
            child: child,
          );
  }
}

class NamidaTabView extends StatefulWidget {
  final int initialIndex;
  final List<String>? tabs;
  final List<Widget>? tabWidgets;
  final List<Widget> children;
  final void Function(int index) onIndexChanged;
  final bool isScrollable;
  final bool compact;
  final bool reportIndexChangedOnInit;

  const NamidaTabView({
    super.key,
    required this.children,
    required this.initialIndex,
    this.tabs,
    this.tabWidgets,
    required this.onIndexChanged,
    this.isScrollable = false,
    this.compact = false,
    this.reportIndexChangedOnInit = true,
  });

  @override
  State<NamidaTabView> createState() => NamidaTabViewState();
}

class NamidaTabViewState extends State<NamidaTabView> with SingleTickerProviderStateMixin {
  late TabController controller;
  int? _index;

  void fn() {
    final newIndex = controller.index;
    if (newIndex == _index) return;
    _index = newIndex;
    widget.onIndexChanged(newIndex);
  }

  void animateToTab(int index) {
    controller.animateTo(index);
  }

  void jumpToTab(int index) {
    controller.animateTo(index, duration: Duration.zero);
  }

  @override
  void initState() {
    final initialIndex = widget.initialIndex.clampInt(0, widget.children.length - 1);
    if (widget.reportIndexChangedOnInit) Timer(Duration.zero, () => widget.onIndexChanged(initialIndex));
    controller = TabController(
      length: widget.children.length,
      vsync: this,
      animationDuration: const Duration(milliseconds: 400),
      initialIndex: initialIndex,
    );
    controller.addListener(fn);
    super.initState();
  }

  @override
  void dispose() {
    controller.removeListener(fn);
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final itemsPadding = widget.compact ? const EdgeInsets.symmetric(horizontal: 2.0, vertical: 8.0) : const EdgeInsets.symmetric(horizontal: 8.0, vertical: 10.0);
    return Column(
      children: [
        TabBar(
          indicatorWeight: widget.compact ? 1.0 : 3.0,
          controller: controller,
          isScrollable: widget.isScrollable,
          tabs:
              widget.tabs
                  ?.map(
                    (e) => Padding(
                      padding: itemsPadding,
                      child: Text(e, maxLines: 1, overflow: TextOverflow.ellipsis),
                    ),
                  )
                  .toList() ??
              widget.tabWidgets ??
              widget.children
                  .map(
                    (e) => Padding(
                      padding: itemsPadding,
                      child: Text(e.toString(), maxLines: 1, overflow: TextOverflow.ellipsis),
                    ),
                  )
                  .toList(),
          splashBorderRadius: BorderRadius.circular(12.0.multipliedRadius),
          indicatorSize: TabBarIndicatorSize.label,
        ),
        Expanded(
          child: TabBarView(
            physics: isDesktop ? const NeverScrollableScrollPhysics() : null,
            controller: controller,
            children: widget.children,
          ),
        ),
      ],
    );
  }
}

class ShaderFadingWidget extends StatelessWidget {
  final bool biggerValues;
  final Widget child;
  final Alignment begin;
  final Alignment end;
  const ShaderFadingWidget({
    super.key,
    this.biggerValues = false,
    required this.child,
    this.begin = Alignment.topCenter,
    this.end = Alignment.bottomCenter,
  });

  @override
  Widget build(BuildContext context) {
    return ShaderMask(
      blendMode: BlendMode.dstIn,
      shaderCallback: (rect) {
        final stops = biggerValues ? [0.07, 0.3, 0.6, 0.8, 1.0] : [0.0, 0.2, 0.8, 0.9, 1.0];
        return LinearGradient(
          begin: begin,
          end: end,
          tileMode: TileMode.clamp,
          stops: stops,
          colors: const [Colors.transparent, Colors.white, Colors.white, Colors.white, Colors.transparent],
        ).createShader(Rect.fromLTRB(0, 0, rect.width, rect.height));
      },
      child: child,
    );
  }
}

class NamidaOpacity extends StatelessWidget {
  final bool enabled;
  final double opacity;
  final Widget child;

  const NamidaOpacity({
    super.key,
    required this.opacity,
    required this.child,
    this.enabled = true,
  });

  @override
  Widget build(BuildContext context) {
    if (opacity == 0) {
      return const SizedBox();
    } else if (!enabled || opacity == 1) {
      return child;
    }
    return Opacity(
      key: key,
      opacity: opacity,
      child: child,
    );
  }
}

class NamidaScrollbar extends StatelessWidget {
  final ScrollController? controller;
  final Widget child;
  final double scrollStep;
  final bool showOnStart;

  const NamidaScrollbar({
    super.key,
    required this.controller,
    required this.child,
    this.scrollStep = 0.0,
    this.showOnStart = false,
  });

  @override
  Widget build(BuildContext context) {
    if (controller == null) return child;
    return CupertinoScrollbar(
      controller: controller,
      showOnStart: showOnStart,
      scrollStep: scrollStep,
      thicknessWhileDragging: 8.5,
      minInteractiveSize: 60.0,
      pressDuration: isDesktop ? const Duration(milliseconds: 50) : const Duration(milliseconds: 80),
      tapToScroll: () => settings.extra.tapToScroll ?? false,
      enhancedDragToScroll: () => settings.extra.enhancedDragToScroll ?? true,
      onThumbLongPressStart: () => isScrollbarThumbDragging = true,
      onThumbLongPressEnd: () => isScrollbarThumbDragging = false,
      child: child,
    );
  }
}

class NamidaScrollbarWithController extends StatefulWidget {
  final bool showOnStart;
  final double scrollStep;
  final Widget Function(ScrollController sc) child;
  const NamidaScrollbarWithController({
    super.key,
    this.showOnStart = false,
    this.scrollStep = 0.0,
    required this.child,
  });

  @override
  State<NamidaScrollbarWithController> createState() => _NamidaScrollbarWithControllerState();
}

class _NamidaScrollbarWithControllerState extends State<NamidaScrollbarWithController> {
  late final ScrollController _sc;
  @override
  void initState() {
    _sc = NamidaScrollController.create();
    super.initState();
  }

  @override
  void dispose() {
    _sc.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return NamidaScrollbar(
      controller: _sc,
      showOnStart: widget.showOnStart,
      scrollStep: widget.scrollStep,
      child: widget.child(_sc),
    );
  }
}

class NamidaAZScrollbar extends StatefulWidget {
  final Widget child;
  final ScrollController? controller;
  final Map<String, int> scrollConfig;
  final double? itemExtent;

  const NamidaAZScrollbar({
    super.key,
    required this.child,
    this.controller,
    this.scrollConfig = const {},
    this.itemExtent,
  });

  @override
  State<NamidaAZScrollbar> createState() => _NamidaAZScrollbarState();
}

class _NamidaAZScrollbarState extends State<NamidaAZScrollbar> {
  ScrollController? controller;
  final stackKey = GlobalKey<State<StatefulWidget>>();
  final columnKey = GlobalKey<State<StatefulWidget>>();
  double stackHeight = 0;
  double columnHeight = 1;
  static const verticalPadding = 6.0;
  final characters = <String>[];
  final items = <Text>[];

  final _selectedChar = (0.0, '').obs;

  @override
  void initState() {
    controller = widget.controller;
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      final s = columnKey.currentContext?.size;
      if (s != null) stackHeight = s.height;
      final h = columnKey.currentContext?.size?.height;
      if (h != null) columnHeight = h;
    });
    stackHeight = stackHeight;

    for (final e in widget.scrollConfig.entries) {
      characters.add(e.key);
      items.add(Text(e.key));
    }

    super.initState();
  }

  @override
  void dispose() {
    _selectedChar.close();
    super.dispose();
  }

  void onScroll(double dy) {
    final controller = this.controller!;
    final columnHeight = columnKey.currentContext?.size?.height ?? 1;
    final p = (dy) / (columnHeight - verticalPadding * 2);
    final index = ((p * items.length).clampDouble(0, items.length - 1)).floor();
    final character = characters[index];
    _selectedChar.value = (p, character);
    if (controller.positions.isNotEmpty) {
      final p = controller.positions.last;
      final toOffset = (widget.scrollConfig[character] ?? 1) * (widget.itemExtent ?? 0);
      controller.jumpTo(toOffset.toDouble().clampDouble(0.0, p.maxScrollExtent));
    }
  }

  @override
  Widget build(BuildContext context) {
    if (controller == null || widget.scrollConfig.isEmpty) {
      return NamidaScrollbar(
        controller: controller,
        child: widget.child,
      );
    }

    final theme = context.theme;
    final textTheme = theme.textTheme;
    return Stack(
      key: stackKey,
      alignment: Alignment.center,
      children: [
        widget.child,
        Obx(
          (context) => Positioned(
            right: 14.0,
            top: _selectedChar.valueR.$1 * columnHeight,
            child: Container(
              margin: const EdgeInsets.symmetric(horizontal: 8.0),
              decoration: BoxDecoration(shape: BoxShape.circle, color: theme.cardColor),
              child: Text(_selectedChar.valueR.$2),
            ),
          ),
        ),
        Positioned(
          right: 0,
          child: SizedBox(
            width: 14.0,
            height: stackHeight,
            child: FittedBox(
              child: DefaultTextStyle(
                style: textTheme.displaySmall!.copyWith(fontSize: stackHeight / widget.scrollConfig.length),
                child: Container(
                  padding: const EdgeInsets.symmetric(horizontal: 3.0, vertical: verticalPadding),
                  decoration: BoxDecoration(
                    color: theme.scaffoldBackgroundColor.withOpacityExt(0.8),
                    borderRadius: BorderRadius.circular(8.0.multipliedRadius),
                  ),
                  child: GestureDetector(
                    onVerticalDragDown: (details) => onScroll(details.localPosition.dy),
                    onVerticalDragUpdate: (details) => onScroll(details.localPosition.dy),
                    child: Text(
                      widget.scrollConfig.keys.join('\n'),
                      key: columnKey,
                      textAlign: TextAlign.center,
                    ),
                  ),
                ),
              ),
            ),
          ),
        ),
      ],
    );
  }
}

class AnimatedEnabled extends StatelessWidget {
  final bool enabled;
  final double disabledOpacity;
  final int durationMS;
  final Widget child;

  const AnimatedEnabled({
    super.key,
    required this.enabled,
    this.disabledOpacity = 0.6,
    this.durationMS = 300,
    required this.child,
  });

  @override
  Widget build(BuildContext context) {
    return IgnorePointer(
      ignoring: !enabled,
      child: AnimatedOpacity(
        opacity: enabled ? 1.0 : disabledOpacity,
        duration: Duration(milliseconds: durationMS),
        child: child,
      ),
    );
  }
}

class AnimatedShow extends StatelessWidget {
  final bool show;
  final bool isHorizontal;
  final Duration duration;
  final Curve curve;
  final AlignmentGeometry alignment;
  final Widget child;

  const AnimatedShow({
    super.key,
    required this.show,
    this.isHorizontal = false,
    this.duration = const Duration(milliseconds: 300),
    this.curve = Curves.fastEaseInToSlowEaseOut,
    this.alignment = Alignment.center,
    required this.child,
  });

  @override
  Widget build(BuildContext context) {
    final factor = show ? 1.0 : 0.0;
    return IgnorePointer(
      ignoring: !show,
      child: AnimatedAlign(
        alignment: alignment,
        heightFactor: isHorizontal ? null : factor,
        widthFactor: isHorizontal ? factor : null,
        duration: duration,
        curve: curve,
        child: AnimatedOpacity(
          opacity: factor,
          duration: duration,
          curve: curve,
          child: child,
        ),
      ),
    );
  }
}

class DelayedAnimatedShow extends StatefulWidget {
  final bool show;
  final bool isHorizontal;
  final Duration showDelay;
  final Duration hideDelay;
  final Duration duration;
  final Widget child;

  const DelayedAnimatedShow({
    super.key,
    required this.show,
    this.isHorizontal = false,
    required this.child,
    this.showDelay = const Duration(milliseconds: 600),
    this.hideDelay = Duration.zero,
    this.duration = const Duration(milliseconds: 300),
  });

  @override
  State<DelayedAnimatedShow> createState() => _DelayedAnimatedShowState();
}

class _DelayedAnimatedShowState extends State<DelayedAnimatedShow> {
  Timer? _timer;
  late bool _visible;

  void _setVisible(bool visible, Duration delay) {
    if (delay == Duration.zero) {
      if (mounted) setState(() => _visible = visible);
      return;
    }
    _timer = Timer(delay, () {
      if (mounted && widget.show == visible) setState(() => _visible = visible);
    });
  }

  @override
  void initState() {
    super.initState();
    _visible = widget.show;
  }

  @override
  void didUpdateWidget(DelayedAnimatedShow oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.show == widget.show) return;

    _timer?.cancel();
    if (widget.show) {
      _setVisible(true, widget.showDelay);
    } else {
      _setVisible(false, widget.hideDelay);
    }
  }

  @override
  void dispose() {
    _timer?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedShow(
      show: _visible,
      isHorizontal: widget.isHorizontal,
      duration: widget.duration,
      child: widget.child,
    );
  }
}

class QueueUtilsRow extends StatelessWidget {
  final String Function(int number) itemsKeyword;
  final void Function() onAddItemsTap;
  final Widget scrollQueueWidget;

  const QueueUtilsRow({
    super.key,
    required this.itemsKeyword,
    required this.onAddItemsTap,
    required this.scrollQueueWidget,
  });

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.end,
      children: [
        const SizedBox(width: 12.0),
        NamidaButton(
          tooltip: () => lang.remove,
          icon: Broken.broom,
          onTap: () {
            void removedSnack(int removedCount) => snackyy(
              top: false,
              icon: Broken.filter_remove,
              message: "${lang.removed}: ${itemsKeyword(removedCount)}",
            );
            NamidaPopupWrapper(
              childrenDefault: () => [
                NamidaPopupItem(
                  icon: Broken.copy,
                  title: lang.duplicatedTracks,
                  onTap: () {
                    final removed = Player.inst.removeDuplicatesFromQueue();
                    removedSnack(removed);
                  },
                ),
                NamidaPopupItem(
                  icon: Broken.arrow_left,
                  title: lang.previous,
                  onTap: () {
                    final removed = Player.inst.removeAllPrevious();
                    removedSnack(removed);
                  },
                ),
                NamidaPopupItem(
                  icon: Broken.arrow_right_2,
                  title: lang.next,
                  onTap: () {
                    final removed = Player.inst.removeAllNext();
                    removedSnack(removed);
                  },
                ),
                NamidaPopupItem(
                  icon: Broken.task,
                  title: lang.all,
                  onTap: () {
                    final removed = Player.inst.removeAllQueueExceptCurrent();
                    removedSnack(removed);
                  },
                ),
              ],
            ).showPopupMenu(context);
          },
        ),
        const SizedBox(width: 6.0),
        NamidaButton(
          tooltip: () => lang.newTracksAdd,
          icon: Broken.add_circle,
          onTap: () => onAddItemsTap(),
        ),
        const SizedBox(width: 6.0),
        scrollQueueWidget,
        const SizedBox(width: 6.0),
        GestureDetector(
          onLongPressStart: (details) async {
            Widget buildButton(String title, IconData icon, bool isShuffleAll) {
              final enabled = isShuffleAll == settings.player.shuffleAllTracks.value;
              return Padding(
                padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 2.0),
                child: NamidaInkWell(
                  padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),
                  borderRadius: 10.0,
                  bgColor: enabled ? CurrentColor.inst.color.withOpacityExt(0.2) : null,
                  onTap: () {
                    settings.player.save(shuffleAllTracks: isShuffleAll);
                    NamidaNavigator.inst.popMenu();
                  },
                  child: Row(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      StackedIcon(
                        baseIcon: Broken.shuffle,
                        secondaryIcon: icon,
                        blurRadius: 8.0,
                        secondaryIconSize: 12.0,
                      ),
                      const SizedBox(width: 8.0),
                      Expanded(
                        child: Text(
                          title,
                          style: context.textTheme.displayMedium,
                        ),
                      ),
                      const SizedBox(width: 12.0),
                    ],
                  ),
                ),
              );
            }

            final menu = NamidaPopupWrapper(
              children: () => [
                buildButton(lang.shuffleNext, Broken.forward, false),
                buildButton(lang.shuffleAll, Broken.task, true),
              ],
            );
            menu.showPopupMenu(
              context,
            );
          },
          child: NamidaButton(
            text: lang.shuffle,
            icon: Broken.shuffle,
            onTap: () => Player.inst.shuffleTracks(settings.player.shuffleAllTracks.value),
          ),
        ),
        const SizedBox(width: 12.0),
      ],
    );
  }
}

class RepeatModeIconButton extends StatelessWidget {
  final bool compact;
  final Color? color;
  final double iconSize;
  final VoidCallback? onPressed;
  final Widget Function(Widget child, String Function()? tooltipCallback, void Function() onTap)? builder;

  const RepeatModeIconButton({
    super.key,
    this.compact = false,
    this.color,
    this.iconSize = 20.0,
    this.onPressed,
    this.builder,
  });

  static final _numberOfRepeats = 1.obso;

  void _onPressed(BuildContext context) {
    NamidaPopupWrapper(
      children: () => _popupChildren(context),
    ).showPopupMenu(context);
  }

  // void _switchMode() {
  //   final e = settings.player.repeatMode.value.nextElement(PlayerRepeatMode.values);
  //   settings.player.save(repeatMode: e);
  // }

  // String _buildTooltip() {
  //   final repeat = settings.player.repeatMode.value;
  //   return repeat.buildText();
  // }

  Iterable<Widget> _popupChildren(BuildContext context) {
    return PlayerRepeatMode.values.map(
      (repeatMode) {
        final enabled = repeatMode == settings.player.repeatMode.value;
        final mainIcon = repeatMode.toMainIcon();
        final secondaryIcon = repeatMode.toSecondaryIcon();
        const iconSize = 22.0;
        final iconColor = context.defaultIconColor();
        return Padding(
          padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 2.0),
          child: NamidaInkWell(
            padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),
            borderRadius: 12.0,
            bgColor: enabled ? CurrentColor.inst.color.withOpacityExt(0.2) : null,
            onTap: () {
              settings.player.save(repeatMode: repeatMode);
              if (repeatMode == PlayerRepeatMode.forNtimes) {
                Player.inst.updateNumberOfRepeats(_numberOfRepeats.value);
              }
              NamidaNavigator.inst.popMenu();
            },
            child: Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                Stack(
                  alignment: Alignment.center,
                  children: [
                    secondaryIcon != null
                        ? StackedIcon(
                            baseIcon: mainIcon,
                            secondaryIcon: secondaryIcon,
                            baseIconColor: iconColor,
                            secondaryIconColor: iconColor,
                            iconSize: iconSize,
                            secondaryIconSize: iconSize * 0.6,
                          )
                        : Icon(
                            mainIcon,
                            size: iconSize,
                            color: iconColor,
                          ),

                    if (repeatMode == PlayerRepeatMode.forNtimes)
                      ObxO(
                        rx: _numberOfRepeats,
                        builder: (context, repeats) => Text(
                          '$repeats',
                          style: context.textTheme.displaySmall?.copyWith(color: iconColor),
                        ),
                      ),
                  ],
                ),
                const SizedBox(width: 8.0),
                Expanded(
                  child: ObxPrefer(
                    rx: _numberOfRepeats,
                    enabled: repeatMode == PlayerRepeatMode.forNtimes,
                    builder: (context, repeats) => Text(
                      repeatMode.buildText(numberOfRepeats: repeats),
                      style: context.textTheme.displayMedium,
                    ),
                  ),
                ),

                if (repeatMode == PlayerRepeatMode.forNtimes)
                  NumberOfRepeatsWidgetProvider(
                    numberOfRepeatsRx: _numberOfRepeats,
                    iconColor: iconColor,
                    iconSize: 18.0,
                    horizontalPadding: 4.0,
                  ),
              ],
            ),
          ),
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    final theme = context.theme;
    final textTheme = theme.textTheme;
    final iconColor = color ?? theme.colorScheme.onSecondaryContainer;

    final rawWidget = ObxO(
      rx: settings.player.repeatMode,
      builder: (context, repeatMode) {
        final mainIcon = repeatMode.toMainIcon();
        final secondaryIcon = repeatMode.toSecondaryIcon();

        final child = Stack(
          alignment: Alignment.center,
          children: [
            secondaryIcon != null
                ? StackedIcon(
                    baseIcon: mainIcon,
                    secondaryIcon: secondaryIcon,
                    baseIconColor: iconColor,
                    secondaryIconColor: iconColor,
                    iconSize: iconSize,
                    secondaryIconSize: iconSize * 0.6,
                  )
                : Icon(
                    mainIcon,
                    size: iconSize,
                    color: iconColor,
                  ),
            if (repeatMode == PlayerRepeatMode.forNtimes)
              ObxO(
                rx: Player.inst.numberOfRepeats,
                builder: (context, numberOfRepeats) => Text(
                  '$numberOfRepeats',
                  style: textTheme.displaySmall?.copyWith(color: iconColor),
                ),
              ),
          ],
        );
        if (builder != null) {
          return builder!(
            child,
            null,
            () {
              onPressed?.call();
              _onPressed(context);
            },
          );
        }

        return compact
            ? NamidaIconButton(
                icon: null,
                verticalPadding: 2.0,
                horizontalPadding: 4.0,
                padding: EdgeInsets.zero,
                iconSize: iconSize,
                onPressed: () {
                  onPressed?.call();
                  _onPressed(context);
                },
                child: child,
              )
            : IconButton(
                visualDensity: VisualDensity.compact,
                style: const ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap),
                padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 6.0),
                onPressed: () {
                  onPressed?.call();
                  _onPressed(context);
                },
                icon: child,
              );
      },
    );
    return NamidaPopupWrapper(
      onPop: () {
        _numberOfRepeats.value = 1;
      },
      openOnTap: true,
      openOnLongPress: true,
      children: () => _popupChildren(context),
      child: rawWidget,
    );
  }
}

class NumberOfRepeatsWidgetProvider extends StatelessWidget {
  final RxBase<int> numberOfRepeatsRx;
  final Color? iconColor;
  final double iconSize;
  final double horizontalPadding;

  const NumberOfRepeatsWidgetProvider({
    super.key,
    required this.numberOfRepeatsRx,
    required this.iconColor,
    this.iconSize = 20.0,
    this.horizontalPadding = 8.0,
  });

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        NamidaIconButton(
          horizontalPadding: horizontalPadding,
          icon: Broken.minus_cirlce,
          onPressed: () => numberOfRepeatsRx.value = (numberOfRepeatsRx.value - 1).clampInt(1, 20),
          iconSize: iconSize,
          iconColor: iconColor,
        ),
        NamidaIconButton(
          horizontalPadding: horizontalPadding,
          icon: Broken.add_circle,
          onPressed: () => numberOfRepeatsRx.value = (numberOfRepeatsRx.value + 1).clampInt(1, 20),
          iconSize: iconSize,
          iconColor: iconColor,
        ),
      ],
    );
  }
}

class SoundControlButton extends StatelessWidget {
  final bool compact;
  final Color? color;
  final double iconSize;
  final VoidCallback? onPressed;
  final Widget Function(Widget child, String Function() tooltipCallback, void Function() onTap)? builder;

  const SoundControlButton({
    super.key,
    this.compact = false,
    this.color,
    this.iconSize = 20.0,
    this.onPressed,
    this.builder,
  });

  void _onTap() {
    NamidaOnTaps.inst.openSoundControl();
  }

  String _buildTooltip() => lang.soundControl;

  @override
  Widget build(BuildContext context) {
    final tooltip = lang.soundControl;
    final iconColor = color ?? context.theme.colorScheme.onSecondaryContainer;
    final child = Obx(
      (context) {
        PlayerConfigModificationScale? soundModificationScale;
        final currentItem = Player.inst.currentItem.valueR;
        if (!settings.player.isPerTrackAudioConfigOverriden.valueR) {
          final currentItemConfig = Player.audioConfigs.map.valueR[currentItem?.key ?? ''];
          if (currentItemConfig != null) {
            // -- if any config exists in map, it will be used even it's default, so global config means nothing here
            soundModificationScale = currentItemConfig.getModificationScale();
          }
        }
        soundModificationScale ??= Player.inst.getDefaultPlayerConfigR(currentItem).getModificationScale();

        final secondaryIcon = switch (soundModificationScale) {
          PlayerConfigModificationScale.main => Broken.edit_2,
          PlayerConfigModificationScale.alt => Broken.magicpen,
          PlayerConfigModificationScale.none => null,
        };

        return secondaryIcon != null
            ? StackedIcon(
                baseIcon: Broken.sound,
                secondaryIcon: secondaryIcon,
                iconSize: iconSize,
                secondaryIconSize: iconSize * 0.5,
                baseIconColor: iconColor,
                secondaryIconColor: iconColor,
                blurRadius: 12.0,
              )
            : Icon(
                Broken.sound,
                size: iconSize,
                color: iconColor,
              );
      },
    );

    if (builder != null) {
      return builder!(child, _buildTooltip, _onTap);
    }

    return compact
        ? NamidaIconButton(
            tooltip: () => tooltip,
            icon: null,
            verticalPadding: 2.0,
            horizontalPadding: 4.0,
            padding: EdgeInsets.zero,
            iconSize: iconSize,
            onPressed: () {
              onPressed?.call();
              _onTap();
            },
            child: child,
          )
        : IconButton(
            visualDensity: VisualDensity.compact,
            style: const ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap),
            padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 6.0),
            tooltip: tooltip,
            onPressed: () {
              onPressed?.call();
              _onTap();
            },
            icon: child,
          );
  }
}

class TapDetector extends StatelessWidget {
  final VoidCallback? onTap;
  final void Function(TapGestureRecognizer instance)? initializer;
  final Widget? child;
  final HitTestBehavior? behavior;

  const TapDetector({
    super.key,
    required this.onTap,
    this.initializer,
    this.child,
    this.behavior,
  });

  @override
  Widget build(BuildContext context) {
    final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
    gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
      () => TapGestureRecognizer(debugOwner: this),
      initializer ??
          (TapGestureRecognizer instance) {
            instance
              ..onTap = onTap
              ..gestureSettings = MediaQuery.maybeGestureSettingsOf(context);
          },
    );

    return RawGestureDetector(
      behavior: behavior,
      gestures: gestures,
      child: child,
    );
  }
}

class DoubleTapDetector extends StatelessWidget {
  final VoidCallback? onDoubleTap;
  final void Function(DoubleTapGestureRecognizer instance)? initializer;
  final Widget? child;
  final HitTestBehavior? behavior;

  const DoubleTapDetector({
    super.key,
    required this.onDoubleTap,
    this.initializer,
    this.child,
    this.behavior,
  });

  @override
  Widget build(BuildContext context) {
    final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
    gestures[DoubleTapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
      () => DoubleTapGestureRecognizer(debugOwner: this),
      initializer ??
          (DoubleTapGestureRecognizer instance) {
            instance
              ..onDoubleTap = onDoubleTap
              ..gestureSettings = MediaQuery.maybeGestureSettingsOf(context);
          },
    );

    return RawGestureDetector(
      behavior: behavior,
      gestures: gestures,
      child: child,
    );
  }
}

class LongPressDetector extends StatelessWidget {
  final VoidCallback? onLongPress;
  final void Function(LongPressGestureRecognizer instance)? initializer;
  final Widget? child;
  final HitTestBehavior? behavior;
  final bool enableSecondaryTap;

  const LongPressDetector({
    super.key,
    required this.onLongPress,
    this.initializer,
    this.child,
    this.behavior,
    this.enableSecondaryTap = false,
  });

  @override
  Widget build(BuildContext context) {
    final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
    gestures[LongPressGestureRecognizer] = GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
      () => LongPressGestureRecognizer(debugOwner: this),
      initializer ??
          (LongPressGestureRecognizer instance) {
            instance
              ..onLongPress = onLongPress
              ..gestureSettings = MediaQuery.maybeGestureSettingsOf(context);
          },
    );

    if (enableSecondaryTap) {
      gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
        () => TapGestureRecognizer(debugOwner: this),
        (TapGestureRecognizer instance) {
          instance
            ..onSecondaryTap = onLongPress
            ..gestureSettings = MediaQuery.maybeGestureSettingsOf(context);
        },
      );
    }

    return RawGestureDetector(
      behavior: behavior,
      gestures: gestures,
      child: child,
    );
  }
}

class ScaleDetector extends StatelessWidget {
  final GestureScaleStartCallback? onScaleStart;
  final GestureScaleUpdateCallback? onScaleUpdate;
  final GestureScaleEndCallback? onScaleEnd;
  final Widget? child;
  final HitTestBehavior? behavior;

  const ScaleDetector({
    super.key,
    this.onScaleStart,
    this.onScaleUpdate,
    this.onScaleEnd,
    this.child,
    this.behavior,
  });

  @override
  Widget build(BuildContext context) {
    final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
    gestures[ScaleGestureRecognizer] = GestureRecognizerFactoryWithHandlers<ScaleGestureRecognizer>(
      () => ScaleGestureRecognizer(debugOwner: this),
      (ScaleGestureRecognizer instance) {
        instance
          ..onStart = onScaleStart
          ..onUpdate = onScaleUpdate
          ..onEnd = onScaleEnd
          ..gestureSettings = MediaQuery.maybeGestureSettingsOf(context);
      },
    );

    return RawGestureDetector(
      behavior: behavior,
      gestures: gestures,
      child: child,
    );
  }
}

class HorizontalDragDetector extends _LinearDragDetector<HorizontalDragGestureRecognizer> {
  @override
  HorizontalDragGestureRecognizer create() => HorizontalDragGestureRecognizer(debugOwner: this);

  const HorizontalDragDetector({
    super.key,
    super.initializer,
    super.child,
    super.behavior,
    super.onStart,
    super.onDown,
    super.onUpdate,
    super.onEnd,
    super.onCancel,
  });
}

class VerticalDragDetector extends _LinearDragDetector<VerticalDragGestureRecognizer> {
  @override
  VerticalDragGestureRecognizer create() => VerticalDragGestureRecognizer(debugOwner: this);

  const VerticalDragDetector({
    super.key,
    super.initializer,
    super.child,
    super.behavior,
    super.onStart,
    super.onDown,
    super.onUpdate,
    super.onEnd,
    super.onCancel,
  });
}

abstract class _LinearDragDetector<T extends DragGestureRecognizer> extends StatelessWidget {
  T create();

  final GestureDragStartCallback? onStart;
  final GestureDragDownCallback? onDown;
  final GestureDragUpdateCallback? onUpdate;
  final GestureDragEndCallback? onEnd;
  final GestureDragCancelCallback? onCancel;
  final void Function(T instance)? initializer;
  final Widget? child;
  final HitTestBehavior? behavior;

  const _LinearDragDetector({
    super.key,
    this.initializer,
    this.child,
    this.behavior,
    this.onStart,
    this.onDown,
    this.onUpdate,
    this.onEnd,
    this.onCancel,
  });

  @override
  Widget build(BuildContext context) {
    final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
    gestures[T] = GestureRecognizerFactoryWithHandlers<T>(
      create,
      initializer ??
          (instance) {
            instance
              ..onStart = onStart
              ..onDown = onDown
              ..onUpdate = onUpdate
              ..onEnd = onEnd
              ..onCancel = onCancel
              ..gestureSettings = MediaQuery.maybeGestureSettingsOf(context);
          },
    );

    return RawGestureDetector(
      behavior: behavior,
      gestures: gestures,
      child: child,
    );
  }
}

class DecorationClipper extends CustomClipper<Path> {
  const DecorationClipper({
    this.textDirection = TextDirection.ltr,
    required this.decoration,
  });

  final TextDirection textDirection;
  final Decoration decoration;

  @override
  Path getClip(Size size) {
    return decoration.getClipPath(Offset.zero & size, textDirection);
  }

  @override
  bool shouldReclip(DecorationClipper oldClipper) {
    return oldClipper.decoration != decoration || oldClipper.textDirection != textDirection;
  }
}

class BorderRadiusClip extends StatelessWidget {
  final TextDirection textDirection;
  final BorderRadiusGeometry borderRadius;
  final Clip clipBehavior;
  final Widget child;

  const BorderRadiusClip({
    super.key,
    this.textDirection = TextDirection.ltr,
    this.clipBehavior = Clip.antiAlias,
    required this.borderRadius,
    required this.child,
  });

  @override
  Widget build(BuildContext context) {
    return ClipPath(
      clipper: DecorationClipper(
        textDirection: textDirection,
        decoration: BoxDecoration(
          borderRadius: borderRadius,
        ),
      ),
      clipBehavior: clipBehavior,
      child: child,
    );
  }
}

class NamidaHistoryDayHeaderBox extends StatelessWidget {
  final double height;
  final String title;
  final Widget menu;
  final Color bgColor;
  final Color sideColor;
  final Color shadowColor;

  const NamidaHistoryDayHeaderBox({
    super.key,
    required this.height,
    required this.title,
    required this.menu,
    required this.sideColor,
    required this.bgColor,
    required this.shadowColor,
  });

  @override
  Widget build(BuildContext context) {
    final textTheme = context.textTheme;
    return DecoratedBox(
      decoration: BoxDecoration(
        color: bgColor,
        border: Border(
          left: BorderSide(
            color: sideColor,
            width: 4.0,
          ),
        ),
        boxShadow: [
          BoxShadow(
            offset: const Offset(0, 2.0),
            blurRadius: 4.0,
            color: shadowColor,
          ),
        ],
      ),
      child: SizedBox(
        width: Dimensions.inst.availableAppContentWidth,
        height: height,
        child: Row(
          children: [
            const SizedBox(width: 12.0),
            Expanded(
              child: Text(
                title,
                style: textTheme.displayMedium,
              ),
            ),
            const SizedBox(width: 4.0),
            menu,
            const SizedBox(width: 4.0),
          ],
        ),
      ),
    );
  }
}

class NamidaClearDialogExpansionTile<T> extends StatelessWidget {
  final String title;
  final String subtitle;
  final IconData icon;
  final List<T> items;
  final ({String title, String? subtitle, String path}) Function(T item) itemBuilder;
  final int Function(T item) itemSize;
  final RxMap<File, int>? tempFilesSize;
  final Rx<bool>? tempFilesDelete;
  final RxMap<String, bool> pathsToDelete;
  final Rx<int> totalSizeToDelete;
  final Rx<bool> allSelected;

  const NamidaClearDialogExpansionTile({
    super.key,
    required this.title,
    required this.subtitle,
    required this.icon,
    required this.items,
    required this.itemBuilder,
    required this.itemSize,
    required this.tempFilesSize,
    required this.tempFilesDelete,
    required this.pathsToDelete,
    required this.totalSizeToDelete,
    required this.allSelected,
  });

  @override
  Widget build(BuildContext context) {
    final theme = context.theme;
    final tempFilesSize = this.tempFilesSize;
    final tempFilesDelete = this.tempFilesDelete;
    return NamidaExpansionTile(
      borderless: true,
      initiallyExpanded: true,
      titleText: title,
      subtitleText: subtitle,
      icon: icon,
      trailingBuilder: (iconWidget) => Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          DecoratedBox(
            decoration: BoxDecoration(
              color: theme.cardColor,
              borderRadius: BorderRadius.circular(6.0.multipliedRadius),
            ),
            child: Padding(
              padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 3.0),
              child: Text("${items.length}"),
            ),
          ),
          const SizedBox(width: 6.0),
          iconWidget,
        ],
      ),
      childrenPadding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0),
      children: [
        ...items.map(
          (item) {
            final data = itemBuilder(item);
            return SmallListTile(
              borderRadius: 12.0,
              icon: Broken.arrow_right_3,
              iconSize: 20.0,
              color: theme.cardColor,
              visualDensity: const VisualDensity(horizontal: -3.0, vertical: -3.0),
              title: data.title,
              subtitle: data.subtitle,
              active: false,
              onTap: () {
                final wasTrue = pathsToDelete[data.path] == true;
                final willEnable = !wasTrue;
                pathsToDelete[data.path] = willEnable;
                if (willEnable) {
                  totalSizeToDelete.value += itemSize(item);
                } else {
                  totalSizeToDelete.value -= itemSize(item);
                }
                allSelected.value = false;
              },
              trailing: Obx(
                (context) => NamidaCheckMark(
                  size: 16.0,
                  active: pathsToDelete[data.path] == true,
                ),
              ),
            );
          },
        ),
        if (tempFilesSize != null && tempFilesDelete != null)
          Obx(
            (context) {
              final size = tempFilesSize.values.fold(0, (p, e) => p + e);
              if (size <= 0) return const SizedBox();
              return SmallListTile(
                borderRadius: 12.0,
                icon: Broken.broom,
                iconSize: 20.0,
                color: theme.cardColor,
                visualDensity: const VisualDensity(horizontal: -3.0, vertical: -3.0),
                title: lang.deleteTempFiles,
                subtitle: size.fileSizeFormatted,
                active: false,
                onTap: () {
                  tempFilesDelete.value = !tempFilesDelete.value;
                  if (tempFilesDelete.value) {
                    totalSizeToDelete.value += size;
                  } else {
                    totalSizeToDelete.value -= size;
                  }
                },
                trailing: ObxO(
                  rx: tempFilesDelete,
                  builder: (context, deletetemp) => NamidaCheckMark(
                    size: 16.0,
                    active: deletetemp,
                  ),
                ),
              );
            },
          ),
      ],
    );
  }
}

/// Returns [AnimatedTheme] or [Theme] based on [settings.animatedTheme].
class AnimatedThemeOrTheme extends StatelessWidget {
  final ThemeData data;
  final Widget child;
  final Duration duration;

  const AnimatedThemeOrTheme({
    super.key,
    required this.data,
    required this.child,
    this.duration = kThemeAnimationDuration,
  });

  @override
  Widget build(BuildContext context) {
    return settings.animatedTheme.value
        ? AnimatedTheme(
            data: data,
            duration: duration,
            child: child,
          )
        : Theme(
            data: data,
            child: child,
          );
  }
}

class EnableDisablePlaylistReordering extends StatelessWidget {
  final String playlistName;
  final PlaylistManager playlistManager;

  const EnableDisablePlaylistReordering({
    super.key,
    required this.playlistName,
    required this.playlistManager,
  });

  @override
  Widget build(BuildContext context) {
    return ObxO(
      key: UniqueKey(), // i have no f idea why this happens.. namida ghosts are here again
      rx: playlistManager.canReorderItems,
      builder: (context, reorderable) => NamidaAppBarIcon(
        tooltip: () => playlistManager.canReorderItems.value ? lang.disableReordering : lang.enableReordering,
        icon: reorderable ? Broken.forward_item : Broken.lock_1,
        onPressed: () {
          final playlist = playlistManager.getPlaylist(playlistName);
          if (playlist == null) return;
          if (playlist.sortsType?.isNotEmpty ?? false) {
            snackyy(
              isError: true,
              title: lang.warning,
              message: lang.thisPlaylistHasActiveSortersDisableThemBeforeReordering,
            );
            return;
          }
          playlistManager.canReorderItems.value = !playlistManager.canReorderItems.value;
        },
      ),
    );
  }
}

class SetVideosPriorityChipController {
  // -- worst of my creations so far
  SetVideosPriorityChipController();

  NamidaPopupWrapper? Function()? _menuWrapperFn;
  BuildContext? _currentContext;

  void showMenu() {
    final menuWrapper = _menuWrapperFn?.call();
    if (menuWrapper != null) {
      menuWrapper.showPopupMenu(_currentContext!);
    }
  }
}

class SetVideosPriorityChip extends StatefulWidget {
  final SetVideosPriorityChipController? controller;
  final bool smaller;
  final int totalCount;
  final Iterable<String> videosId;
  final String Function(int count) countToText;
  final void Function(CacheVideoPriority? priority)? onInitialPriority;
  final void Function(CacheVideoPriority priority) onChanged;

  const SetVideosPriorityChip({
    super.key,
    this.controller,
    this.smaller = false,
    required this.totalCount,
    required this.videosId,
    required this.countToText,
    required this.onChanged,
    this.onInitialPriority,
  });

  @override
  State<SetVideosPriorityChip> createState() => _SetVideosPriorityChipState();
}

class _SetVideosPriorityChipState extends State<SetVideosPriorityChip> {
  CacheVideoPriority? cachePriority;

  @override
  void initState() {
    _initCachePriority();
    widget.controller?._menuWrapperFn = _getPopupWrapper;
    super.initState();
  }

  void _initCachePriority() async {
    if (widget.totalCount == 1) {
      final newCP = await VideoController.inst.videosPriorityManager.getVideoPriority(widget.videosId.first);
      refreshState(() => cachePriority = newCP);
      widget.onInitialPriority?.call(cachePriority);
    }
  }

  Future<bool> _confirmSetPriorityForAll(int count) async {
    bool confirmed = false;
    await NamidaNavigator.inst.navigateDialog(
      dialog: CustomBlurryDialog(
        isWarning: true,
        normalTitleStyle: true,
        bodyText: "${lang.update}: ${widget.countToText(count)}",
        actions: [
          const CancelButton(),
          NamidaButton(
            text: lang.confirm.toUpperCase(),
            onTap: () async {
              confirmed = true;
              NamidaNavigator.inst.closeDialog();
            },
          ),
        ],
      ),
    );
    return confirmed;
  }

  NamidaPopupWrapper? _getPopupWrapper() {
    final theme = context.theme;
    final textTheme = theme.textTheme;
    return cachePriority != null || widget.totalCount > 1
        ? NamidaPopupWrapper(
            childrenAfterChildrenDefault: false,
            children: () => [
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0),
                child: Row(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    const Icon(
                      Broken.info_circle,
                      size: 14.0,
                    ),
                    const SizedBox(width: 6.0),
                    Text(
                      lang.priority,
                      maxLines: 2,
                      overflow: TextOverflow.ellipsis,
                      style: textTheme.displaySmall,
                    ),
                  ],
                ),
              ),
              const NamidaContainerDivider(),
            ],
            childrenDefault: () => CacheVideoPriority.values.map(
              (e) => NamidaPopupItem(
                icon: Broken.cpu,
                title: e.toText(),
                selected: e == cachePriority,
                onTap: () async {
                  if (widget.totalCount == 1) {
                    VideoController.inst.videosPriorityManager.setVideoPriority(widget.videosId.first, e);
                    setState(() => cachePriority = e);
                    widget.onChanged(e);
                  } else {
                    final confirmed = await _confirmSetPriorityForAll(widget.totalCount);
                    if (confirmed) {
                      VideoController.inst.videosPriorityManager.setVideosPriority(widget.videosId, e);
                      if (mounted) setState(() => cachePriority = e);
                      widget.onChanged(e);
                    }
                  }
                },
              ),
            ),
            child: NamidaInkWell(
              padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 12.0),
              bgColor: theme.cardColor.withOpacityExt(0.5),
              child: Text(
                cachePriority?.toText() ?? '?',
                style: widget.smaller ? theme.textTheme.displaySmall : theme.textTheme.displayMedium,
              ),
            ),
          )
        : null;
  }

  @override
  Widget build(BuildContext context) {
    widget.controller?._currentContext = context;
    return _getPopupWrapper() ?? const SizedBox();
  }
}

class SwipeQueueAddTileInfo {
  final QueueSourceBase queueSource;
  final String? heroTag;
  final String? videoTitle;

  const SwipeQueueAddTileInfo({
    required this.queueSource,
    required this.heroTag,
    this.videoTitle,
  });

  Color? get getCurrentColor =>
      queueSource == QueueSourceYoutubeID.ytPlayerQueue || queueSource == QueueSourceYoutubeID.ytPlayerQueue ? CurrentColor.inst.miniplayerColor : CurrentColor.inst.color;

  void copyToClipboard(String text) {
    NamidaUtils.copyToClipboard(
      content: text,
      leftBarIndicatorColor: getCurrentColor,
    );
  }
}

class SwipeQueueAddTile<Q extends Playable> extends StatelessWidget {
  final Q item;
  final SwipeQueueAddTileInfo Function() infoCallback;
  final Object dismissibleKey;
  final bool allowSwipeLeft;
  final bool allowSwipeRight;
  final Widget child;

  const SwipeQueueAddTile({
    super.key,
    required this.item,
    required this.infoCallback,
    required this.dismissibleKey,
    required this.allowSwipeLeft,
    required this.allowSwipeRight,
    required this.child,
  });

  @override
  Widget build(BuildContext context) {
    return FadeDismissible(
      key: ValueKey(dismissibleKey),
      direction: allowSwipeLeft && allowSwipeRight
          ? DismissDirection.horizontal
          : allowSwipeLeft
          ? DismissDirection.endToStart
          : allowSwipeRight
          ? DismissDirection.startToEnd
          : DismissDirection.none,
      removeOnDismiss: false,
      dismissThreshold: 0.12,
      friction: 0.58,
      onDismissed: (direction) {
        final swipedLeft = direction == DismissDirection.endToStart;
        final action = swipedLeft ? settings.onTrackSwipeLeft.value : settings.onTrackSwipeRight.value;
        if (action == TrackExecuteActions.none) return;
        action.execute(
          item,
          info: infoCallback(),
        );
      },
      leftWidget: () => _SwipeQueueActionBox(
        isLeft: true,
        action: settings.onTrackSwipeRight.value,
      ),
      rightWidget: () => _SwipeQueueActionBox(
        isLeft: false,
        action: settings.onTrackSwipeLeft.value,
      ),
      child: child,
    );
  }
}

class _SwipeQueueActionBox extends StatelessWidget {
  final bool isLeft;
  final TrackExecuteActions action;
  const _SwipeQueueActionBox({required this.isLeft, required this.action});

  @override
  Widget build(BuildContext context) {
    final radius = Radius.circular(8.0.multipliedRadius);
    return DecoratedBox(
      decoration: BoxDecoration(
        borderRadius: BorderRadius.horizontal(
          left: isLeft ? Radius.zero : radius,
          right: isLeft ? radius : Radius.zero,
        ),
        color: context.theme.cardColor,
      ),
      child: ConstrainedBox(
        constraints: BoxConstraints(maxWidth: context.width * 0.25),
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 8.0),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Icon(
                action.toIcon(),
                size: 22.0,
              ),
              SizedBox(height: 4.0),
              Text(
                action.toText(),
                style: context.textTheme.displaySmall,
                softWrap: false,
                overflow: TextOverflow.fade,
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class LayoutWidthProvider extends StatelessWidget {
  final double? maxWidth;
  final Widget Function(BuildContext context, double maxWidth) builder;
  const LayoutWidthProvider({super.key, this.maxWidth, required this.builder});

  @override
  Widget build(BuildContext context) {
    if (maxWidth != null) {
      return builder(context, maxWidth!);
    }
    return LayoutBuilder(
      builder: (context, constraints) => builder(
        context,
        constraints.maxWidth.withMaximum(context.width),
      ),
    );
  }
}

class LayoutWidthHeightProvider extends StatelessWidget {
  final Widget Function(BuildContext context, double maxWidth, double maxHeight) builder;
  const LayoutWidthHeightProvider({super.key, required this.builder});

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        final maxWidth = constraints.maxWidth.withMaximum(context.width);
        final maxHeight = constraints.maxHeight.withMaximum(context.height);
        return builder(
          context,
          maxWidth,
          maxHeight,
        );
      },
    );
  }
}

class NamidaUpdateButton extends StatelessWidget {
  const NamidaUpdateButton({super.key});

  void onTap() {
    void popSheet(BuildContext context) => Navigator.pop(context);
    void onUpdateTap(BuildContext context) {
      popSheet(context);
      final latestVersion = VersionController.inst.latestVersion.value;
      final link = (latestVersion?.isBeta ?? false) ? AppSocial.GITHUB_RELEASES_BETA : AppSocial.GITHUB_RELEASES;
      NamidaLinkUtils.openLink(link);
    }

    String versionToDate(VersionWrapper? version) {
      String buildDateText = '';
      final buildDate = version?.buildDate;
      if (buildDate != null) {
        buildDateText = ' (${TimeAgoController.dateFromNow(buildDate, long: true)})';
      }
      return buildDateText;
    }

    final currentVersion = VersionWrapper.current;
    final currentVersionDateText = versionToDate(currentVersion);

    NamidaNavigator.inst.showSheet(
      builder: (context, bottomPadding, maxWidth, maxHeight) {
        final textTheme = context.textTheme;
        return SizedBox(
          height: maxHeight,
          child: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 24.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                SizedBox(height: 32.0),
                ObxO(
                  rx: VersionController.inst.latestVersion,
                  builder: (context, latestVersion) {
                    final buildDateText = versionToDate(latestVersion);

                    return RichText(
                      text: TextSpan(
                        text: latestVersion?.prettyVersion ?? '',
                        children: buildDateText.isEmpty
                            ? null
                            : [
                                TextSpan(
                                  text: buildDateText,
                                  style: textTheme.displayMedium,
                                ),
                              ],
                        style: textTheme.displayLarge,
                      ),
                    );
                  },
                ),
                if (currentVersion != null)
                  Row(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      RotatedBox(
                        quarterTurns: 2,
                        child: RichText(
                          text: TextSpan(
                            text: '⤵ ',
                            style: textTheme.displaySmall,
                          ),
                        ),
                      ),
                      RichText(
                        text: TextSpan(
                          text: currentVersion.prettyVersion,
                          style: textTheme.displaySmall?.copyWith(fontSize: 11.0),
                          children: currentVersionDateText.isEmpty
                              ? null
                              : [
                                  TextSpan(
                                    text: currentVersionDateText,
                                    style: textTheme.displaySmall?.copyWith(fontSize: 10.0),
                                  ),
                                ],
                        ),
                      ),
                    ],
                  ),
                SizedBox(height: 12.0),
                Expanded(
                  child: _NamidaVersionReleasesInfoList(
                    maxHeight: maxHeight,
                  ),
                ),
                SizedBox(height: 12.0),
                Row(
                  mainAxisAlignment: MainAxisAlignment.end,
                  children: [
                    Expanded(
                      flex: 1,
                      child: NamidaTextButton(
                        onTap: () => popSheet(context),
                        text: lang.cancel,
                      ),
                    ),
                    const SizedBox(width: 12.0),
                    Expanded(
                      flex: 2,
                      child: NamidaInkWell(
                        onTap: () => onUpdateTap(context),
                        borderRadius: 12.0,
                        padding: const EdgeInsets.all(12.0),
                        height: 48.0,
                        bgColor: CurrentColor.inst.color.withOpacityExt(0.9),
                        child: Center(
                          child: Text(
                            lang.update.toUpperCase(),
                            style: textTheme.displayMedium?.copyWith(color: Colors.white.withOpacityExt(0.9)),
                          ),
                        ),
                      ),
                    ),
                  ],
                ),
                SizedBox(height: 12.0),
              ],
            ),
          ),
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return NamidaAppBarIcon(
      icon: Broken.send_square,
      onPressed: onTap,
    );
  }
}

class _NamidaVersionReleasesInfoList extends StatefulWidget {
  final double maxHeight;
  const _NamidaVersionReleasesInfoList({required this.maxHeight});

  @override
  State<_NamidaVersionReleasesInfoList> createState() => _NamidaVersionReleasesInfoListState();
}

class _NamidaVersionReleasesInfoListState extends State<_NamidaVersionReleasesInfoList> {
  @override
  void initState() {
    VersionController.inst.fetchReleasesAfterCurrent();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    final theme = context.theme;
    final textTheme = theme.textTheme;
    return ObxO(
      rx: VersionController.inst.releasesAfterCurrent,
      builder: (context, releasesAfterCurrent) => releasesAfterCurrent == null
          ? ShimmerWrapper(
              shimmerEnabled: true,
              child: SuperSmoothListView.builder(
                padding: EdgeInsets.zero,
                itemCount: 3,
                itemBuilder: (context, index) {
                  return NamidaInkWell(
                    animationDurationMS: 200,
                    margin: const EdgeInsets.symmetric(vertical: 4.0),
                    width: context.width,
                    height: widget.maxHeight * 0.5,
                    bgColor: theme.cardColor,
                  );
                },
              ),
            )
          : SuperSmoothListView.builder(
              padding: EdgeInsets.zero,
              itemCount: releasesAfterCurrent.length,
              itemBuilder: (context, index) {
                final info = releasesAfterCurrent[index];
                return Column(
                  mainAxisSize: MainAxisSize.min,
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    if (index > 0)
                      Text(
                        info.version.prettyVersion,
                        style: textTheme.displayMedium,
                      ),
                    SizedBox(height: 4.0),
                    NamidaInkWell(
                      bgColor: theme.cardColor,
                      padding: EdgeInsets.all(8.0),
                      child: NamidaMarkdown(
                        selectable: false,
                        smallBodySize: false,
                        smallerNestedtBullets: true,
                        data: info.body.replaceAll(RegExp(r'https:\/\/\S+'), ''),
                        physics: const NeverScrollableScrollPhysics(),
                        shrinkWrap: true,
                        padding: EdgeInsets.zero,
                      ),
                    ),
                    SizedBox(height: 12.0),
                  ],
                );
              },
            ),
    );
  }
}

class ShortcutsInfoWidget extends StatefulWidget {
  final ShortcutsManager manager;
  const ShortcutsInfoWidget({super.key, required this.manager});

  @override
  State<ShortcutsInfoWidget> createState() => _ShortcutsInfoWidgetState();
}

class _ShortcutsInfoWidgetState extends State<ShortcutsInfoWidget> {
  final organizedMap = <String, List<ShortcutKeyActivator>>{};

  @override
  void initState() {
    for (final k in widget.manager.bindings.keys) {
      organizedMap.addForce(k.title(), k);
    }

    super.initState();
  }

  void _onAddOrEditTap(String title, HotkeyAction action) {
    NamidaNavigator.inst.navigateDialog(
      dialog: _HotKeyRecorderDialog(
        title: title,
        initalHotKey: settings.shortcuts.shortcuts.value[action],
        onDelete: () {
          ShortcutsController.instance?.setUserShortcut(action: action, data: null);
        },
        onHotKeyRecorded: (data) {
          if (data != null) {
            // -- its not likely for already registered system hotkeys to be caught again here, but anyways
            for (final userKey in settings.shortcuts.shortcuts.value.values) {
              final keyAlrExists = userKey != null && data.isSimilarTo(userKey);
              if (keyAlrExists) {
                return '(${userKey.buildKeyLabel()})';
              }
            }

            final defaultKeys = ShortcutsController.instance?.bindings.keys;
            if (defaultKeys != null) {
              for (final defaultKey in defaultKeys) {
                final keyData = ShortcutKeyData.fromShortcutKeyActivator(defaultKey);
                final keyAlrExists = data.isSimilarTo(keyData);
                if (keyAlrExists) {
                  return '(${keyData.buildKeyLabel()}) => ${defaultKey.title}';
                }
              }
            }
          }

          ShortcutsController.instance?.setUserShortcut(action: action, data: data);
          return null;
        },
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    final theme = context.theme;
    final textTheme = theme.textTheme;
    return ObxO(
      rx: settings.shortcuts.shortcuts,
      builder: (context, userShortcuts) => SuperSmoothListView(
        shrinkWrap: true,
        children: organizedMap.entries
            .map(
              (e) {
                var shortcutsTexts = <String>[];
                String title = e.key;
                if (title == lang.libraryTabs) {
                  shortcutsTexts = ['Ctrl + 1..9'];
                } else if (title == lang.setRating) {
                  shortcutsTexts = ['Ctrl + alt + 1..9'];
                } else {
                  shortcutsTexts = e.value.map((e) {
                    return ShortcutKeyData.fromShortcutKeyActivator(e).buildKeyLabel();
                  }).toList();
                }
                final action = e.value[0].action;
                final data = action == null ? null : userShortcuts[action];
                void addOrEditTapLocal() => action == null ? null : _onAddOrEditTap(title, action);
                return Wrap(
                  runSpacing: 2.0,
                  crossAxisAlignment: WrapCrossAlignment.center,
                  children: [
                    ...shortcutsTexts.map(
                      (shortcut) => NamidaInkWell(
                        borderRadius: 4.0,
                        padding: EdgeInsets.symmetric(horizontal: 4.0, vertical: 2.0),
                        bgColor: theme.cardColor,
                        child: RichText(
                          text: TextSpan(
                            text: shortcut,
                            style: textTheme.displaySmall?.copyWith(fontSize: 13.0, fontWeight: FontWeight.w600),
                          ),
                        ),
                      ),
                    ),
                    if (data != null)
                      NamidaContainerDivider(
                        height: 16.0,
                        width: 2.0,
                        margin: EdgeInsets.symmetric(horizontal: 3.0),
                      ),
                    if (data != null)
                      NamidaInkWell(
                        borderRadius: 4.0,
                        padding: EdgeInsets.symmetric(vertical: 2.0),
                        bgColor: theme.cardColor,
                        onTap: addOrEditTapLocal,
                        child: Row(
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            SizedBox(width: 4.0),
                            Icon(
                              Broken.cpu,
                              size: 14.0,
                            ),
                            SizedBox(width: 4.0),
                            RichText(
                              text: TextSpan(
                                text: data.buildKeyLabel(),
                                style: textTheme.displaySmall?.copyWith(fontSize: 13.0, fontWeight: FontWeight.w600),
                              ),
                            ),
                            SizedBox(width: 4.0),
                            if (action != null)
                              NamidaInkWell(
                                onTap: addOrEditTapLocal,
                                borderRadius: 4.0,
                                padding: EdgeInsets.symmetric(horizontal: 4.0, vertical: 2.0),
                                bgColor: theme.colorScheme.secondaryContainer.withOpacityExt(0.6),
                                child: Icon(
                                  Broken.edit_2,
                                  size: 13.0,
                                ),
                              ),
                            SizedBox(width: 2.0),
                          ],
                        ),
                      ),
                    if (action != null && data == null) ...[
                      SizedBox(width: 4.0),
                      NamidaInkWell(
                        onTap: addOrEditTapLocal,
                        borderRadius: 4.0,
                        padding: EdgeInsets.symmetric(horizontal: 4.0, vertical: 2.0),
                        bgColor: theme.colorScheme.secondaryContainer.withOpacityExt(0.6),
                        child: Icon(
                          Broken.add_circle,
                          size: 14.0,
                        ),
                      ),
                    ],
                    SizedBox(width: 4.0),
                    RichText(
                      text: TextSpan(
                        text: title,
                        style: textTheme.displayMedium?.copyWith(fontSize: 13.0),
                      ),
                    ),
                  ],
                );
              },
            )
            .addSeparators(
              separator: const NamidaContainerDivider(
                margin: EdgeInsets.symmetric(vertical: 3.0),
              ),
            )
            .toList(),
      ),
    );
  }
}

class _HotKeyRecorderDialog extends StatefulWidget {
  final String? title;
  final ShortcutKeyData? initalHotKey;
  final String? Function(ShortcutKeyData? data) onHotKeyRecorded;
  final void Function() onDelete;

  const _HotKeyRecorderDialog({
    required this.title,
    this.initalHotKey,
    required this.onHotKeyRecorded,
    required this.onDelete,
  });

  @override
  State<_HotKeyRecorderDialog> createState() => _HotKeyRecorderDialogState();
}

class _HotKeyRecorderDialogState extends State<_HotKeyRecorderDialog> {
  ShortcutKeyData? _hotKey;
  String? _errorMsg;

  @override
  void initState() {
    _hotKey = widget.initalHotKey;
    HardwareKeyboard.instance.addHandler(_handleKeyEvent);
    super.initState();
  }

  @override
  void dispose() {
    HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
    super.dispose();
  }

  bool _handleKeyEvent(KeyEvent keyEvent) {
    if (keyEvent is! KeyDownEvent) return false;
    if (keyEvent.physicalKey == PhysicalKeyboardKey.enter || keyEvent.physicalKey == PhysicalKeyboardKey.numpadEnter) {
      _confirmHotkey();
      return true;
    }

    bool ctrl = false;
    bool shift = false;
    bool alt = false;
    bool meta = false;

    void investigateForModifiers(Set<PhysicalKeyboardKey> keys) {
      for (final k in keys) {
        if (k == PhysicalKeyboardKey.controlLeft || k == PhysicalKeyboardKey.controlRight) {
          ctrl = true;
          continue;
        }
        if (k == PhysicalKeyboardKey.shiftLeft || k == PhysicalKeyboardKey.shiftRight) {
          shift = true;
          continue;
        }
        if (k == PhysicalKeyboardKey.altLeft || k == PhysicalKeyboardKey.altRight) {
          alt = true;
          continue;
        }
        if (k == PhysicalKeyboardKey.metaLeft || k == PhysicalKeyboardKey.metaRight) {
          meta = true;
          continue;
        }
      }
    }

    investigateForModifiers({keyEvent.physicalKey});
    final isKeyModifier = ctrl || shift || alt || meta; // check after pressed key is investigated
    investigateForModifiers(HardwareKeyboard.instance.physicalKeysPressed);

    setState(() {
      _hotKey = ShortcutKeyData(
        key: isKeyModifier ? null : keyEvent.logicalKey,
        ctrl: ctrl,
        shift: shift,
        alt: alt,
        meta: meta,
      );
      _errorMsg = null;
    });

    return true;
  }

  void _confirmHotkey() {
    final error = widget.onHotKeyRecorded(_hotKey);
    if (error != null) {
      setState(() => _errorMsg = error);
    } else {
      NamidaNavigator.inst.closeDialog();
    }
  }

  void _confirmDelete() {
    widget.onDelete();
    NamidaNavigator.inst.closeDialog();
  }

  @override
  Widget build(BuildContext context) {
    final keyLabelNormalized = _hotKey?.buildKeyLabel() ?? '';
    final isKeyGood = keyLabelNormalized.isNotEmpty;
    final isEdit = widget.initalHotKey != null;
    return CustomBlurryDialog(
      icon: isEdit ? Broken.edit : Broken.add_circle,
      title: widget.title ?? (isEdit ? lang.edit : lang.add),
      normalTitleStyle: true,
      trailingWidgets: [
        if (isEdit)
          NamidaIconButton(
            icon: Broken.trash,
            tooltip: () => lang.delete,
            onPressed: _confirmDelete,
          ),
        if (isKeyGood)
          NamidaIconButton(
            icon: Broken.refresh,
            tooltip: () => lang.clear,
            onPressed: () {
              setState(() {
                _hotKey = null;
                _errorMsg = null;
              });
            },
          ),
      ],
      actions: [
        const CancelButton(),
        NamidaButton(
          text: lang.save,
          onTap: _confirmHotkey,
        ),
      ],
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          Align(
            alignment: Alignment.center,
            child: NamidaInkWell(
              borderRadius: 8.0,
              bgColor: context.theme.cardColor,
              padding: EdgeInsetsGeometry.symmetric(
                horizontal: 12.0,
                vertical: 12.0,
              ),
              child: Text(
                isKeyGood ? keyLabelNormalized : '?',
                style: context.textTheme.displayMedium,
              ),
            ),
          ),
          if (_errorMsg != null) ...[
            SizedBox(height: 8.0),
            Text(
              _errorMsg!,
              style: context.textTheme.displayMedium?.copyWith(color: context.theme.colorScheme.error),
            ),
          ],
        ],
      ),
    );
  }
}

class NamidaArtworkExpandableToFullscreen extends StatelessWidget {
  final Widget artwork;
  final Object? heroTag;
  final FutureOr<File?> Function() imageFile;
  final FutureOr<FArtwork?> Function() fetchImage;
  final FutureOr<String?> Function(File? imgFile, Uint8List? bytes) onSave;
  final Color? Function()? themeColor;

  const NamidaArtworkExpandableToFullscreen({
    super.key,
    required this.artwork,
    required this.heroTag,
    required this.imageFile,
    required this.fetchImage,
    required this.onSave,
    required this.themeColor,
  });

  void openInFullscreen() async {
    File? imgFile;
    Uint8List? imgBytes;

    imgFile = await imageFile();
    if (imgFile != null && await imgFile.exists()) {
      // -- good
    } else {
      final res = await fetchImage();
      imgFile = res?.file;
      imgBytes = res?.bytes;
    }

    final ImageProvider<Object>? imgProvider = imgFile != null
        ? FileImage(imgFile)
        : imgBytes != null
        ? MemoryImage(imgBytes)
        : null;
    if (imgProvider == null) return;

    NamidaNavigator.inst.navigateDialog(
      scale: 1.0,
      blackBg: true,
      dialog: NamidaArtworkFullscreen(
        title: '',
        artwork: artwork,
        imgProvider: imgProvider,
        heroTag: heroTag,
        save: () async {
          final savePath = await onSave(imgFile, imgBytes);
          NamidaOnTaps.inst.showSavedImageInSnack(savePath, themeColor?.call());
        },
        close: NamidaNavigator.inst.closeDialog,
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return TapDetector(
      onTap: openInFullscreen,
      child: artwork,
    );
  }
}

class NamidaArtworkFullscreen extends StatefulWidget {
  final String title;
  final Widget artwork;
  final ImageProvider<Object>? imgProvider;
  final Object? heroTag;
  final void Function() save;
  final void Function() close;

  const NamidaArtworkFullscreen({
    super.key,
    required this.title,
    required this.artwork,
    required this.imgProvider,
    required this.heroTag,
    required this.save,
    required this.close,
  });

  @override
  State<NamidaArtworkFullscreen> createState() => _NamidaArtworkFullscreenState();
}

class _NamidaArtworkFullscreenState extends State<NamidaArtworkFullscreen> {
  bool _showTopBar = false;
  double _heighestTopPadding = 0;

  @override
  void initState() {
    NamidaNavigator.setSystemUIImmersiveMode(true);
    super.initState();
  }

  @override
  void dispose() {
    MiniPlayerController.inst.setImmersiveMode(null); // let that decide
    super.dispose();
  }

  void _toggleAppBars() {
    final newShow = !_showTopBar;
    if (newShow != _showTopBar) {
      setState(() => _showTopBar = newShow);
      NamidaNavigator.setSystemUIImmersiveMode(!newShow);
    }
  }

  @override
  Widget build(BuildContext context) {
    final heroTag = widget.heroTag;
    final topPadding = context.padding.top;
    if (topPadding > _heighestTopPadding) _heighestTopPadding = topPadding;
    return Stack(
      alignment: AlignmentGeometry.center,
      children: [
        LongPressDetector(
          onLongPress: widget.save,
          child: PhotoView(
            heroAttributes: heroTag == null ? null : PhotoViewHeroAttributes(tag: heroTag),
            gaplessPlayback: true,
            onTapUp: (context, details, controllerValue) {
              _toggleAppBars();
            },
            tightMode: true,
            minScale: PhotoViewComputedScale.contained,
            loadingBuilder: (context, event) => widget.artwork,
            backgroundDecoration: const BoxDecoration(color: Colors.transparent),
            filterQuality: FilterQuality.high,
            imageProvider: widget.imgProvider,
          ),
        ),
        Positioned(
          top: 0,
          child: AnimatedOpacity(
            duration: const Duration(milliseconds: 150),
            opacity: _showTopBar ? 1.0 : 0.0,
            child: IgnorePointer(
              ignoring: !_showTopBar,
              child: SizedBox(
                width: context.width,
                child: ColoredBox(
                  color: context.theme.scaffoldBackgroundColor,
                  child: Padding(
                    padding: EdgeInsets.only(
                      top: 12.0 + _heighestTopPadding,
                      bottom: 12.0,
                      left: 8.0,
                      right: 8.0,
                    ),
                    child: Row(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        Align(
                          alignment: Alignment.centerLeft,
                          child: NamidaIconButton(
                            icon: Broken.arrow_left_2,
                            onPressed: widget.close,
                          ),
                        ),
                        Expanded(
                          child: widget.title.isEmpty
                              ? const SizedBox()
                              : Text(
                                  widget.title,
                                  style: context.textTheme.displayMedium,
                                ),
                        ),
                        Align(
                          alignment: Alignment.centerRight,
                          child: NamidaIconButton(
                            icon: Broken.gallery_import,
                            onPressed: widget.save,
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ),
            ),
          ),
        ),
      ],
    );
  }
}

class ObxPrefer<T> extends StatelessWidget {
  final RxBaseCore<T> rx;
  final Widget Function(BuildContext context, T? value) builder;
  final bool enabled;
  const ObxPrefer({required this.rx, required this.builder, required this.enabled, super.key});

  @override
  Widget build(BuildContext context) {
    return enabled ? ObxO(rx: rx, builder: builder) : builder(context, null);
  }
}

class ObxOrNull<T> extends StatelessWidget {
  final RxBaseCore<T>? rx;
  final Widget Function(BuildContext context, T? value) builder;
  const ObxOrNull({required this.rx, required this.builder, super.key});

  @override
  Widget build(BuildContext context) {
    return rx != null ? ObxO(rx: rx!, builder: builder) : builder(context, null);
  }
}

class NamidaCoolBox extends StatelessWidget {
  final Color colorScheme;
  final String text;
  final Widget Function(BuildContext context)? builder;
  final bool extraVPadding;
  final bool extraBorder;
  final bool reducedColors;
  final BorderRadius? borderRadius;
  final double hPadding;

  const NamidaCoolBox({
    super.key,
    required this.colorScheme,
    this.text = '',
    this.builder,
    this.extraVPadding = false,
    this.extraBorder = false,
    this.reducedColors = false,
    this.borderRadius,
    this.hPadding = 12.0,
  });

  @override
  Widget build(BuildContext context) {
    double vPadding = 6.0;
    if (extraVPadding) vPadding += 4.0;
    return DecoratedBox(
      decoration: BoxDecoration(
        borderRadius: borderRadius ?? BorderRadius.circular(8.0.multipliedRadius),
        color: colorScheme.withOpacityExt(reducedColors ? 0.06 : 0.08),
        border: Border.all(
          width: extraBorder ? 2.0 : 1.0,
          color: colorScheme.withOpacityExt(reducedColors ? 0.3 : 0.4),
        ),
      ),
      child: Padding(
        padding: EdgeInsets.symmetric(horizontal: hPadding, vertical: vPadding),
        child:
            builder?.call(context) ??
            Text(
              text,
              style: context.theme.textTheme.displaySmall,
            ),
      ),
    );
  }
}

class NamidaMouseRegion extends StatelessWidget {
  final bool enabled;
  final Widget? child;

  const NamidaMouseRegion({
    super.key,
    this.enabled = true,
    this.child,
  });

  @override
  Widget build(BuildContext context) {
    if (enabled && isDesktop) {
      return MouseRegion(
        hitTestBehavior: HitTestBehavior.translucent,
        cursor: SystemMouseCursors.click,
        child: child,
      );
    }
    return child ?? const SizedBox();
  }
}

class NamidaNavigatorWidget extends StatelessWidget {
  final GlobalKey<NavigatorState> navKey;
  final List<Page<dynamic>> pages;
  final List<NavigatorObserver> observers;
  final bool allowPop;
  final String? restorationScopeId;
  final RouteListFactory onGenerateInitialRoutes;

  const NamidaNavigatorWidget({
    super.key,
    required this.navKey,
    this.pages = const <Page<dynamic>>[],
    this.observers = const <NavigatorObserver>[],
    this.allowPop = true,
    this.restorationScopeId,
    this.onGenerateInitialRoutes = Navigator.defaultGenerateInitialRoutes,
  });

  @override
  Widget build(BuildContext context) {
    return Navigator(
      key: navKey,
      restorationScopeId: restorationScopeId,
      requestFocus: false,
      // ignore: deprecated_member_use
      onPopPage: allowPop ? null : (route, result) => false,
      observers: observers,
      pages: pages,
      onGenerateInitialRoutes: onGenerateInitialRoutes,
      onUnknownRoute: (_) => MaterialPageRoute(builder: (_) => const SizedBox()),
    );
  }
}

class NamidaPopScope extends StatelessWidget {
  // ignore: deprecated_member_use
  final WillPopCallback? onWillPop;
  final Widget child;

  const NamidaPopScope({
    super.key,
    this.onWillPop,
    required this.child,
  });

  @override
  Widget build(BuildContext context) {
    // ignore: deprecated_member_use
    return WillPopScope(
      onWillPop: onWillPop,
      child: child,
    );
  }
}

class CustomIconButtonTonal extends StatelessWidget {
  final IconData icon;
  final double? iconSize;
  final String? tooltip;
  final bool dense;
  final Color? colorScheme;
  final void Function()? onTap;

  const CustomIconButtonTonal({
    super.key,
    required this.icon,
    this.iconSize,
    this.tooltip,
    this.dense = true,
    this.colorScheme,
    required this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    return IconButton.filledTonal(
      padding: dense ? EdgeInsets.zero : null,
      style: ButtonStyle(
        tapTargetSize: dense ? MaterialTapTargetSize.shrinkWrap : null,
        visualDensity: dense ? VisualDensity.compact : null,
        backgroundColor: WidgetStatePropertyAll((colorScheme ?? context.theme.colorScheme.secondary).withOpacityExt(0.18)),
      ),
      onPressed: onTap,
      icon: Icon(
        icon,
        size: iconSize,
        color: context.theme.iconTheme.color?.withOpacityExt(0.7) ?? context.defaultIconColor(colorScheme).withOpacityExt(0.75),
      ),
      tooltip: tooltip,
    );
  }
}

class SplitPageInfo {
  final String title;
  final Widget page;
  final Widget? titleIconWidget;

  const SplitPageInfo({
    required this.title,
    required this.page,
    this.titleIconWidget,
  });
}

class SplitPage extends StatefulWidget {
  final int initialIndex;
  final void Function(int index) onIndexChanged;
  final List<SplitPageInfo> pages;
  final bool expanded;
  final bool joinHeaderChips;
  final bool showDivider;

  const SplitPage({
    super.key,
    required this.initialIndex,
    required this.onIndexChanged,
    required this.pages,
    this.expanded = true,
    this.joinHeaderChips = false,
    this.showDivider = true,
  });

  @override
  State<SplitPage> createState() => SplitPageState();
}

class SplitPageState extends State<SplitPage> {
  int _selectedIndex = 0;

  @override
  void initState() {
    _selectedIndex = widget.initialIndex.clampInt(0, widget.pages.length - 1);
    super.initState();
  }

  void _onButtonTap(int index) {
    if (index == _selectedIndex) return;
    setState(() => _selectedIndex = index);
    widget.onIndexChanged(index);
  }

  @override
  Widget build(BuildContext context) {
    final theme = context.theme;
    final pages = widget.pages;
    final selectedIndex = _selectedIndex;
    return Column(
      mainAxisSize: .min,
      children: [
        const SizedBox(height: 8.0),
        Padding(
          padding: const EdgeInsets.symmetric(horizontal: 12.0),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: pages
                .mapIndexed(
                  (e, i) {
                    final isSelected = i == selectedIndex;
                    BorderRadius borderRadius;
                    final brValue = 8.0.multipliedRadius;
                    if (widget.joinHeaderChips) {
                      final brValueSmall = brValue * 0.25;
                      borderRadius = i == 0
                          ? BorderRadius.horizontal(
                              left: Radius.circular(brValue),
                              right: Radius.circular(brValueSmall),
                            )
                          : i == pages.length - 1
                          ? BorderRadius.horizontal(
                              left: Radius.circular(brValueSmall),
                              right: Radius.circular(brValue),
                            )
                          : BorderRadius.circular(brValueSmall);
                    } else {
                      borderRadius = BorderRadius.circular(brValue);
                    }
                    return Expanded(
                      child: NamidaInkWell(
                        alignment: Alignment.center,
                        animationDurationMS: 200,
                        borderRadius: 8.0,
                        bgColor: theme.cardTheme.color,
                        padding: const EdgeInsets.all(8.0),
                        decoration: BoxDecoration(
                          border: isSelected
                              ? Border.all(
                                  color: theme.colorScheme.primary.withOpacityExt(0.6),
                                  width: 1.2,
                                )
                              : null,
                          borderRadius: borderRadius,
                        ),
                        onTap: () => _onButtonTap(i),
                        child: Row(
                          mainAxisAlignment: .center,
                          children: [
                            ?e.titleIconWidget,
                            Flexible(
                              fit: e.titleIconWidget == null ? FlexFit.tight : FlexFit.loose,
                              child: Text(
                                e.title,
                                style: theme.textTheme.displayMedium,
                                textAlign: TextAlign.center,
                              ),
                            ),
                          ],
                        ),
                      ),
                    );
                  },
                )
                .addSeparators(
                  separator: widget.joinHeaderChips ? const SizedBox(width: 4.0) : const SizedBox(width: 8.0),
                  skipFirst: 1,
                )
                .toList(),
          ),
        ),

        const SizedBox(height: 6.0),

        if (widget.showDivider)
          NamidaContainerDivider(
            margin: const EdgeInsets.symmetric(horizontal: 18.0),
          ),

        Flexible(
          fit: widget.expanded ? FlexFit.tight : FlexFit.loose,
          child: pages[_selectedIndex].page,
        ),
      ],
    );
  }
}

class PlayableTitleSubtitleWidget extends StatefulWidget {
  final bool isYTID;
  final Widget Function(String? title, String? artist) builder;

  const PlayableTitleSubtitleWidget({
    super.key,
    required this.isYTID,
    required this.builder,
  });

  @override
  State<PlayableTitleSubtitleWidget> createState() => _PlayableTitleSubtitleWidgetState();
}

class _PlayableTitleSubtitleWidgetState extends State<PlayableTitleSubtitleWidget> {
  String? _videoName;
  String? _channelName;

  @override
  void initState() {
    super.initState();
    _onPlayableChange();
    Player.inst.currentItem.addListener(_onPlayableChange);
    if (widget.isYTID) {
      YoutubeInfoController.current.currentVideoPage.addListener(_onPlayableChange);
      YoutubeInfoController.current.currentYTStreams.addListener(_onPlayableChange);
    }
  }

  @override
  void dispose() {
    super.dispose();
    Player.inst.currentItem.removeListener(_onPlayableChange);
    YoutubeInfoController.current.currentVideoPage.removeListener(_onPlayableChange);
    YoutubeInfoController.current.currentYTStreams.removeListener(_onPlayableChange);
  }

  void _onPlayableChange() async {
    final item = Player.inst.currentItem.value;
    item?.execute(
      selectable: _onLocalChange,
      youtubeID: _onYTChange,
    );
  }

  void _onLocalChange(Selectable item) async {
    final track = item.track;
    _videoName = track.title;
    _channelName = track.originalArtist;

    refreshState();
  }

  void _onYTChange(YoutubeID item) async {
    final vidId = item.id;

    String? videoName = YoutubeInfoController.current.currentVideoPage.value?.videoInfo?.title;
    if (videoName == null || videoName.isEmpty) videoName = YoutubeInfoController.current.currentYTStreams.value?.info?.title;
    if (videoName == null || videoName.isEmpty) videoName = await YoutubeInfoController.utils.getVideoName(vidId);

    String? channelName = YoutubeInfoController.current.currentVideoPage.value?.channelInfo?.title;
    if (channelName == null || channelName.isEmpty) channelName = YoutubeInfoController.current.currentYTStreams.value?.info?.channelName;
    if (channelName == null || channelName.isEmpty) channelName = await YoutubeInfoController.utils.getVideoChannelName(vidId);

    if (videoName != _videoName || channelName != _channelName) {
      _videoName = videoName;
      _channelName = channelName;
      refreshState();
    }
  }

  @override
  Widget build(BuildContext context) {
    return widget.builder(_videoName, _channelName);
  }
}
