mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-07 21:48:14 -08:00
feat: UI 2.0 and other Improvements (#357)
Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
parent
9ca06eaa37
commit
e7b5bb40ff
169 changed files with 4584 additions and 3626 deletions
|
|
@ -1,198 +0,0 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:fladder/providers/settings/home_settings_provider.dart';
|
||||
import 'package:fladder/util/debug_banner.dart';
|
||||
import 'package:fladder/util/poster_defaults.dart';
|
||||
|
||||
enum InputDevice {
|
||||
touch,
|
||||
pointer,
|
||||
}
|
||||
|
||||
class LayoutPoints {
|
||||
final double start;
|
||||
final double end;
|
||||
final ViewSize type;
|
||||
LayoutPoints({
|
||||
required this.start,
|
||||
required this.end,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
LayoutPoints copyWith({
|
||||
double? start,
|
||||
double? end,
|
||||
ViewSize? type,
|
||||
}) {
|
||||
return LayoutPoints(
|
||||
start: start ?? this.start,
|
||||
end: end ?? this.end,
|
||||
type: type ?? this.type,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => 'LayoutPoints(start: $start, end: $end, type: $type)';
|
||||
|
||||
@override
|
||||
bool operator ==(covariant LayoutPoints other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.start == start && other.end == end && other.type == type;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => start.hashCode ^ end.hashCode ^ type.hashCode;
|
||||
}
|
||||
|
||||
class AdaptiveLayout extends InheritedWidget {
|
||||
final ViewSize viewSize;
|
||||
final LayoutMode layoutMode;
|
||||
final InputDevice inputDevice;
|
||||
final TargetPlatform platform;
|
||||
final bool isDesktop;
|
||||
final PosterDefaults posterDefaults;
|
||||
final ScrollController controller;
|
||||
|
||||
const AdaptiveLayout({
|
||||
super.key,
|
||||
required this.viewSize,
|
||||
required this.layoutMode,
|
||||
required this.inputDevice,
|
||||
required this.platform,
|
||||
required this.isDesktop,
|
||||
required this.posterDefaults,
|
||||
required this.controller,
|
||||
required super.child,
|
||||
});
|
||||
|
||||
static AdaptiveLayout? maybeOf(BuildContext context) {
|
||||
return context.dependOnInheritedWidgetOfExactType<AdaptiveLayout>();
|
||||
}
|
||||
|
||||
static ViewSize layoutOf(BuildContext context) {
|
||||
final AdaptiveLayout? result = maybeOf(context);
|
||||
return result!.viewSize;
|
||||
}
|
||||
|
||||
static PosterDefaults poster(BuildContext context) {
|
||||
final AdaptiveLayout? result = maybeOf(context);
|
||||
return result!.posterDefaults;
|
||||
}
|
||||
|
||||
static AdaptiveLayout of(BuildContext context) {
|
||||
final AdaptiveLayout? result = maybeOf(context);
|
||||
return result!;
|
||||
}
|
||||
|
||||
static ScrollController scrollOf(BuildContext context) {
|
||||
final AdaptiveLayout? result = maybeOf(context);
|
||||
return result!.controller;
|
||||
}
|
||||
|
||||
static LayoutMode layoutModeOf(BuildContext context) => maybeOf(context)!.layoutMode;
|
||||
static ViewSize viewSizeOf(BuildContext context) => maybeOf(context)!.viewSize;
|
||||
|
||||
static InputDevice inputDeviceOf(BuildContext context) => maybeOf(context)!.inputDevice;
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(AdaptiveLayout oldWidget) {
|
||||
return viewSize != oldWidget.viewSize ||
|
||||
layoutMode != oldWidget.layoutMode ||
|
||||
platform != oldWidget.platform ||
|
||||
inputDevice != oldWidget.inputDevice ||
|
||||
isDesktop != oldWidget.isDesktop;
|
||||
}
|
||||
}
|
||||
|
||||
const defaultTitleBarHeight = 35.0;
|
||||
|
||||
class AdaptiveLayoutBuilder extends ConsumerStatefulWidget {
|
||||
final List<LayoutPoints> layoutPoints;
|
||||
final ViewSize fallBack;
|
||||
final Widget child;
|
||||
const AdaptiveLayoutBuilder({required this.layoutPoints, required this.child, required this.fallBack, super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() => _AdaptiveLayoutBuilderState();
|
||||
}
|
||||
|
||||
class _AdaptiveLayoutBuilderState extends ConsumerState<AdaptiveLayoutBuilder> {
|
||||
late ViewSize viewSize = widget.fallBack;
|
||||
late LayoutMode layoutMode = LayoutMode.single;
|
||||
late TargetPlatform currentPlatform = defaultTargetPlatform;
|
||||
late ScrollController controller = ScrollController();
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
calculateLayout();
|
||||
calculateSize();
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
bool get isDesktop {
|
||||
if (kIsWeb) return false;
|
||||
return [
|
||||
TargetPlatform.macOS,
|
||||
TargetPlatform.windows,
|
||||
TargetPlatform.linux,
|
||||
].contains(currentPlatform);
|
||||
}
|
||||
|
||||
void calculateLayout() {
|
||||
ViewSize? newType;
|
||||
for (var element in widget.layoutPoints) {
|
||||
if (MediaQuery.of(context).size.width > element.start && MediaQuery.of(context).size.width < element.end) {
|
||||
newType = element.type;
|
||||
}
|
||||
}
|
||||
viewSize = newType ?? widget.fallBack;
|
||||
}
|
||||
|
||||
void calculateSize() {
|
||||
LayoutMode newSize;
|
||||
if (MediaQuery.of(context).size.width > 0 && MediaQuery.of(context).size.width < 960) {
|
||||
newSize = LayoutMode.single;
|
||||
} else {
|
||||
newSize = LayoutMode.dual;
|
||||
}
|
||||
layoutMode = newSize;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final acceptedLayouts = ref.watch(homeSettingsProvider.select((value) => value.screenLayouts));
|
||||
final acceptedViewSizes = ref.watch(homeSettingsProvider.select((value) => value.layoutStates));
|
||||
return MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(
|
||||
padding: isDesktop || kIsWeb ? const EdgeInsets.only(top: defaultTitleBarHeight, bottom: 16) : null,
|
||||
viewPadding: isDesktop || kIsWeb ? const EdgeInsets.only(top: defaultTitleBarHeight, bottom: 16) : null,
|
||||
),
|
||||
child: AdaptiveLayout(
|
||||
viewSize: selectAvailableOrSmaller<ViewSize>(viewSize, acceptedViewSizes, ViewSize.values),
|
||||
controller: controller,
|
||||
layoutMode: selectAvailableOrSmaller<LayoutMode>(layoutMode, acceptedLayouts, LayoutMode.values),
|
||||
inputDevice: (isDesktop || kIsWeb) ? InputDevice.pointer : InputDevice.touch,
|
||||
platform: currentPlatform,
|
||||
isDesktop: isDesktop,
|
||||
posterDefaults: switch (viewSize) {
|
||||
ViewSize.phone => const PosterDefaults(size: 300, ratio: 0.55),
|
||||
ViewSize.tablet => const PosterDefaults(size: 350, ratio: 0.55),
|
||||
ViewSize.desktop => const PosterDefaults(size: 400, ratio: 0.55),
|
||||
},
|
||||
child: DebugBanner(child: widget.child),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
double? get topPadding {
|
||||
return switch (defaultTargetPlatform) {
|
||||
TargetPlatform.linux || TargetPlatform.windows || TargetPlatform.macOS => 35,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
223
lib/util/adaptive_layout/adaptive_layout.dart
Normal file
223
lib/util/adaptive_layout/adaptive_layout.dart
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:fladder/providers/settings/home_settings_provider.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout_model.dart';
|
||||
import 'package:fladder/util/debug_banner.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/poster_defaults.dart';
|
||||
|
||||
enum InputDevice {
|
||||
touch,
|
||||
pointer,
|
||||
}
|
||||
|
||||
enum ViewSize {
|
||||
phone,
|
||||
tablet,
|
||||
desktop;
|
||||
|
||||
const ViewSize();
|
||||
|
||||
String label(BuildContext context) => switch (this) {
|
||||
ViewSize.phone => context.localized.phone,
|
||||
ViewSize.tablet => context.localized.tablet,
|
||||
ViewSize.desktop => context.localized.desktop,
|
||||
};
|
||||
|
||||
bool operator >(ViewSize other) => index > other.index;
|
||||
bool operator >=(ViewSize other) => index >= other.index;
|
||||
bool operator <(ViewSize other) => index < other.index;
|
||||
bool operator <=(ViewSize other) => index <= other.index;
|
||||
}
|
||||
|
||||
enum LayoutMode {
|
||||
single,
|
||||
dual;
|
||||
|
||||
const LayoutMode();
|
||||
|
||||
String label(BuildContext context) => switch (this) {
|
||||
LayoutMode.single => context.localized.layoutModeSingle,
|
||||
LayoutMode.dual => context.localized.layoutModeDual,
|
||||
};
|
||||
|
||||
bool operator >(ViewSize other) => index > other.index;
|
||||
bool operator >=(ViewSize other) => index >= other.index;
|
||||
bool operator <(ViewSize other) => index < other.index;
|
||||
bool operator <=(ViewSize other) => index <= other.index;
|
||||
}
|
||||
|
||||
class AdaptiveLayout extends InheritedWidget {
|
||||
final AdaptiveLayoutModel data;
|
||||
|
||||
const AdaptiveLayout({
|
||||
super.key,
|
||||
required this.data,
|
||||
required super.child,
|
||||
});
|
||||
|
||||
static AdaptiveLayoutModel of(BuildContext context) {
|
||||
final inherited = context.dependOnInheritedWidgetOfExactType<AdaptiveLayout>();
|
||||
assert(inherited != null, 'No AdaptiveLayout found in context');
|
||||
return inherited!.data;
|
||||
}
|
||||
|
||||
static AdaptiveLayout? maybeOf(BuildContext context) {
|
||||
return context.dependOnInheritedWidgetOfExactType<AdaptiveLayout>();
|
||||
}
|
||||
|
||||
static ViewSize layoutOf(BuildContext context) {
|
||||
final AdaptiveLayout? result = maybeOf(context);
|
||||
return result!.data.viewSize;
|
||||
}
|
||||
|
||||
static PosterDefaults poster(BuildContext context) {
|
||||
final AdaptiveLayout? result = maybeOf(context);
|
||||
return result!.data.posterDefaults;
|
||||
}
|
||||
|
||||
static ScrollController scrollOf(BuildContext context) {
|
||||
final AdaptiveLayout? result = maybeOf(context);
|
||||
return result!.data.controller;
|
||||
}
|
||||
|
||||
static EdgeInsets adaptivePadding(BuildContext context, {double horizontalPadding = 16}) {
|
||||
final viewPadding = MediaQuery.paddingOf(context);
|
||||
final padding = viewPadding.copyWith(
|
||||
left: AdaptiveLayout.of(context).sideBarWidth + horizontalPadding + viewPadding.left,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
right: viewPadding.left + horizontalPadding,
|
||||
);
|
||||
return padding;
|
||||
}
|
||||
|
||||
static LayoutMode layoutModeOf(BuildContext context) => maybeOf(context)!.data.layoutMode;
|
||||
static ViewSize viewSizeOf(BuildContext context) => maybeOf(context)!.data.viewSize;
|
||||
|
||||
static InputDevice inputDeviceOf(BuildContext context) => maybeOf(context)!.data.inputDevice;
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(AdaptiveLayout oldWidget) => data != oldWidget.data;
|
||||
}
|
||||
|
||||
const defaultTitleBarHeight = 35.0;
|
||||
|
||||
class AdaptiveLayoutBuilder extends ConsumerStatefulWidget {
|
||||
final AdaptiveLayoutModel? adaptiveLayout;
|
||||
final Widget Function(BuildContext context) child;
|
||||
const AdaptiveLayoutBuilder({
|
||||
this.adaptiveLayout,
|
||||
required this.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() => _AdaptiveLayoutBuilderState();
|
||||
}
|
||||
|
||||
class _AdaptiveLayoutBuilderState extends ConsumerState<AdaptiveLayoutBuilder> {
|
||||
List<LayoutPoints> layoutPoints = [
|
||||
LayoutPoints(start: 0, end: 599, type: ViewSize.phone),
|
||||
LayoutPoints(start: 600, end: 1919, type: ViewSize.tablet),
|
||||
LayoutPoints(start: 1920, end: 3180, type: ViewSize.desktop),
|
||||
];
|
||||
late ViewSize viewSize = ViewSize.tablet;
|
||||
late LayoutMode layoutMode = LayoutMode.single;
|
||||
late TargetPlatform currentPlatform = defaultTargetPlatform;
|
||||
late ScrollController controller = ScrollController();
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
calculateLayout();
|
||||
calculateSize();
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
bool get isDesktop {
|
||||
if (kIsWeb) return false;
|
||||
return [
|
||||
TargetPlatform.macOS,
|
||||
TargetPlatform.windows,
|
||||
TargetPlatform.linux,
|
||||
].contains(currentPlatform);
|
||||
}
|
||||
|
||||
void calculateLayout() {
|
||||
ViewSize? newType;
|
||||
for (var element in layoutPoints) {
|
||||
if (MediaQuery.of(context).size.width > element.start && MediaQuery.of(context).size.width < element.end) {
|
||||
newType = element.type;
|
||||
}
|
||||
}
|
||||
viewSize = newType ?? ViewSize.tablet;
|
||||
}
|
||||
|
||||
void calculateSize() {
|
||||
LayoutMode newSize;
|
||||
if (MediaQuery.of(context).size.width > 0 && MediaQuery.of(context).size.width < 960) {
|
||||
newSize = LayoutMode.single;
|
||||
} else {
|
||||
newSize = LayoutMode.dual;
|
||||
}
|
||||
layoutMode = newSize;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final acceptedLayouts = ref.watch(homeSettingsProvider.select((value) => value.screenLayouts));
|
||||
final acceptedViewSizes = ref.watch(homeSettingsProvider.select((value) => value.layoutStates));
|
||||
|
||||
final selectedViewSize = selectAvailableOrSmaller<ViewSize>(viewSize, acceptedViewSizes, ViewSize.values);
|
||||
final selectedLayoutMode = selectAvailableOrSmaller<LayoutMode>(layoutMode, acceptedLayouts, LayoutMode.values);
|
||||
final input = (isDesktop || kIsWeb) ? InputDevice.pointer : InputDevice.touch;
|
||||
|
||||
final posterDefaults = switch (selectedViewSize) {
|
||||
ViewSize.phone => const PosterDefaults(size: 300, ratio: 0.55),
|
||||
ViewSize.tablet => const PosterDefaults(size: 350, ratio: 0.55),
|
||||
ViewSize.desktop => const PosterDefaults(size: 400, ratio: 0.55),
|
||||
};
|
||||
|
||||
final currentLayout = widget.adaptiveLayout ??
|
||||
AdaptiveLayoutModel(
|
||||
viewSize: selectedViewSize,
|
||||
layoutMode: selectedLayoutMode,
|
||||
inputDevice: input,
|
||||
platform: currentPlatform,
|
||||
isDesktop: isDesktop,
|
||||
sideBarWidth: 0,
|
||||
controller: controller,
|
||||
posterDefaults: posterDefaults,
|
||||
);
|
||||
|
||||
return MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(
|
||||
padding: isDesktop || kIsWeb ? const EdgeInsets.only(top: defaultTitleBarHeight, bottom: 16) : null,
|
||||
viewPadding: isDesktop || kIsWeb ? const EdgeInsets.only(top: defaultTitleBarHeight, bottom: 16) : null,
|
||||
),
|
||||
child: AdaptiveLayout(
|
||||
data: currentLayout.copyWith(
|
||||
viewSize: selectedViewSize,
|
||||
layoutMode: selectedLayoutMode,
|
||||
inputDevice: input,
|
||||
platform: currentPlatform,
|
||||
isDesktop: isDesktop,
|
||||
controller: controller,
|
||||
posterDefaults: posterDefaults,
|
||||
),
|
||||
child: widget.adaptiveLayout == null ? DebugBanner(child: widget.child(context)) : widget.child(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
double? get topPadding {
|
||||
return switch (defaultTargetPlatform) {
|
||||
TargetPlatform.linux || TargetPlatform.windows || TargetPlatform.macOS => 35,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
93
lib/util/adaptive_layout/adaptive_layout_model.dart
Normal file
93
lib/util/adaptive_layout/adaptive_layout_model.dart
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/poster_defaults.dart';
|
||||
|
||||
class LayoutPoints {
|
||||
final double start;
|
||||
final double end;
|
||||
final ViewSize type;
|
||||
LayoutPoints({
|
||||
required this.start,
|
||||
required this.end,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
LayoutPoints copyWith({
|
||||
double? start,
|
||||
double? end,
|
||||
ViewSize? type,
|
||||
}) {
|
||||
return LayoutPoints(
|
||||
start: start ?? this.start,
|
||||
end: end ?? this.end,
|
||||
type: type ?? this.type,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => 'LayoutPoints(start: $start, end: $end, type: $type)';
|
||||
|
||||
@override
|
||||
bool operator ==(covariant LayoutPoints other) {
|
||||
if (identical(this, other)) return true;
|
||||
return other.start == start && other.end == end && other.type == type;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => start.hashCode ^ end.hashCode ^ type.hashCode;
|
||||
}
|
||||
|
||||
@immutable
|
||||
class AdaptiveLayoutModel {
|
||||
final ViewSize viewSize;
|
||||
final LayoutMode layoutMode;
|
||||
final InputDevice inputDevice;
|
||||
final TargetPlatform platform;
|
||||
final bool isDesktop;
|
||||
final PosterDefaults posterDefaults;
|
||||
final ScrollController controller;
|
||||
final double sideBarWidth;
|
||||
|
||||
const AdaptiveLayoutModel({
|
||||
required this.viewSize,
|
||||
required this.layoutMode,
|
||||
required this.inputDevice,
|
||||
required this.platform,
|
||||
required this.isDesktop,
|
||||
required this.posterDefaults,
|
||||
required this.controller,
|
||||
required this.sideBarWidth,
|
||||
});
|
||||
|
||||
AdaptiveLayoutModel copyWith({
|
||||
ViewSize? viewSize,
|
||||
LayoutMode? layoutMode,
|
||||
InputDevice? inputDevice,
|
||||
TargetPlatform? platform,
|
||||
bool? isDesktop,
|
||||
PosterDefaults? posterDefaults,
|
||||
ScrollController? controller,
|
||||
double? sideBarWidth,
|
||||
}) {
|
||||
return AdaptiveLayoutModel(
|
||||
viewSize: viewSize ?? this.viewSize,
|
||||
layoutMode: layoutMode ?? this.layoutMode,
|
||||
inputDevice: inputDevice ?? this.inputDevice,
|
||||
platform: platform ?? this.platform,
|
||||
isDesktop: isDesktop ?? this.isDesktop,
|
||||
posterDefaults: posterDefaults ?? this.posterDefaults,
|
||||
controller: controller ?? this.controller,
|
||||
sideBarWidth: sideBarWidth ?? this.sideBarWidth,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(covariant AdaptiveLayoutModel other) {
|
||||
if (identical(this, other)) return true;
|
||||
return other.viewSize == viewSize && other.layoutMode == layoutMode && other.sideBarWidth == sideBarWidth;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => viewSize.hashCode ^ layoutMode.hashCode ^ sideBarWidth.hashCode;
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ class FladderImage extends ConsumerWidget {
|
|||
final Widget Function(BuildContext context, Object object, StackTrace? stack)? imageErrorBuilder;
|
||||
final Widget? placeHolder;
|
||||
final BoxFit fit;
|
||||
final BoxFit? blurFit;
|
||||
final AlignmentGeometry? alignment;
|
||||
final bool disableBlur;
|
||||
final bool blurOnly;
|
||||
|
|
@ -22,6 +23,7 @@ class FladderImage extends ConsumerWidget {
|
|||
this.imageErrorBuilder,
|
||||
this.placeHolder,
|
||||
this.fit = BoxFit.cover,
|
||||
this.blurFit,
|
||||
this.alignment,
|
||||
this.disableBlur = false,
|
||||
this.blurOnly = false,
|
||||
|
|
@ -41,7 +43,7 @@ class FladderImage extends ConsumerWidget {
|
|||
children: [
|
||||
if (!disableBlur && useBluredPlaceHolder && newImage.hash.isNotEmpty)
|
||||
Image(
|
||||
fit: fit,
|
||||
fit: blurFit ?? fit,
|
||||
excludeFromSemantics: true,
|
||||
filterQuality: FilterQuality.low,
|
||||
image: BlurHashImage(
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
import 'package:fladder/models/book_model.dart';
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
|
|
@ -41,6 +41,37 @@ extension ItemBaseModelsBooleans on List<ItemBaseModel> {
|
|||
}
|
||||
return groupedItems;
|
||||
}
|
||||
|
||||
FladderItemType get getMostCommonType {
|
||||
final Map<FladderItemType, int> counts = {};
|
||||
|
||||
for (final item in this) {
|
||||
final type = item.type;
|
||||
counts[type] = (counts[type] ?? 0) + 1;
|
||||
}
|
||||
|
||||
return counts.entries.reduce((a, b) => a.value >= b.value ? a : b).key;
|
||||
}
|
||||
|
||||
double? getMostCommonAspectRatio({double tolerance = 0.01}) {
|
||||
final Map<int, List<double>> buckets = {};
|
||||
|
||||
for (final item in this) {
|
||||
final aspectRatio = item.primaryRatio;
|
||||
if (aspectRatio == null) continue;
|
||||
|
||||
final bucketKey = (aspectRatio / tolerance).round();
|
||||
|
||||
buckets.putIfAbsent(bucketKey, () => []).add(aspectRatio);
|
||||
}
|
||||
|
||||
if (buckets.isEmpty) return null;
|
||||
|
||||
final mostCommonBucket = buckets.entries.reduce((a, b) => a.value.length >= b.value.length ? a : b);
|
||||
|
||||
final average = mostCommonBucket.value.reduce((a, b) => a + b) / mostCommonBucket.value.length;
|
||||
return average;
|
||||
}
|
||||
}
|
||||
|
||||
enum ItemActions {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
|
@ -23,7 +22,7 @@ import 'package:fladder/screens/photo_viewer/photo_viewer_screen.dart';
|
|||
import 'package:fladder/screens/shared/adaptive_dialog.dart';
|
||||
import 'package:fladder/screens/shared/fladder_snackbar.dart';
|
||||
import 'package:fladder/screens/video_player/video_player.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/list_extensions.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/refresh_state.dart';
|
||||
|
|
|
|||
|
|
@ -1,100 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
class KeyedListView<K, T> extends ConsumerStatefulWidget {
|
||||
final Map<K, T> map;
|
||||
final Widget Function(BuildContext context, int index) itemBuilder;
|
||||
const KeyedListView({required this.map, required this.itemBuilder, super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() => _KeyedListViewState();
|
||||
}
|
||||
|
||||
class _KeyedListViewState extends ConsumerState<KeyedListView> {
|
||||
final ItemScrollController itemScrollController = ItemScrollController();
|
||||
final ScrollOffsetController scrollOffsetController = ScrollOffsetController();
|
||||
final ItemPositionsListener itemPositionsListener = ItemPositionsListener.create();
|
||||
final ScrollOffsetListener scrollOffsetListener = ScrollOffsetListener.create();
|
||||
int currentIndex = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
itemPositionsListener.itemPositions.addListener(() {
|
||||
if (currentIndex != itemPositionsListener.itemPositions.value.toList()[0].index) {
|
||||
setState(() {
|
||||
currentIndex = itemPositionsListener.itemPositions.value.toList()[0].index;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: ScrollablePositionedList.builder(
|
||||
itemCount: widget.map.length,
|
||||
itemBuilder: widget.itemBuilder,
|
||||
itemScrollController: itemScrollController,
|
||||
scrollOffsetController: scrollOffsetController,
|
||||
itemPositionsListener: itemPositionsListener,
|
||||
scrollOffsetListener: scrollOffsetListener,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: widget.map.keys.mapIndexed(
|
||||
(index, e) {
|
||||
final atPosition = currentIndex == index;
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: atPosition ? Theme.of(context).colorScheme.secondary : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
height: 28,
|
||||
width: 28,
|
||||
child: TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
textStyle: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold),
|
||||
iconColor: atPosition
|
||||
? Theme.of(context).colorScheme.onSecondary
|
||||
: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.35),
|
||||
foregroundColor: atPosition
|
||||
? Theme.of(context).colorScheme.onSecondary
|
||||
: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.35),
|
||||
),
|
||||
onPressed: () {
|
||||
itemScrollController.scrollTo(
|
||||
index: index,
|
||||
duration: const Duration(seconds: 1),
|
||||
opacityAnimationWeights: [20, 20, 60],
|
||||
curve: Curves.easeOutCubic,
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
e,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
|
||||
class DefautlSliverBottomPadding extends StatelessWidget {
|
||||
const DefautlSliverBottomPadding({super.key});
|
||||
|
|
@ -9,8 +8,8 @@ class DefautlSliverBottomPadding extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return (AdaptiveLayout.viewSizeOf(context) != ViewSize.phone)
|
||||
? SliverPadding(padding: EdgeInsets.only(bottom: 35 + MediaQuery.of(context).padding.bottom))
|
||||
: SliverPadding(padding: EdgeInsets.only(bottom: 85 + MediaQuery.of(context).padding.bottom));
|
||||
? SliverPadding(padding: EdgeInsets.only(bottom: 60 + MediaQuery.of(context).padding.bottom))
|
||||
: SliverPadding(padding: EdgeInsets.only(bottom: 100 + MediaQuery.of(context).padding.bottom));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
import 'package:fladder/screens/shared/flat_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
import 'package:fladder/screens/shared/flat_button.dart';
|
||||
|
||||
class StickyHeaderText extends ConsumerStatefulWidget {
|
||||
final String label;
|
||||
|
|
@ -21,16 +23,21 @@ class StickyHeaderTextState extends ConsumerState<StickyHeaderText> {
|
|||
return FlatButton(
|
||||
onTap: widget.onClick,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
spacing: 6,
|
||||
children: [
|
||||
Text(
|
||||
widget.label,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
widget.label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.onClick != null)
|
||||
Padding(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue