mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2026-02-17 23:09:36 +00:00
fix: 优化播放页面章节列表
This commit is contained in:
parent
0a26871bb1
commit
612e8b3f50
7 changed files with 96 additions and 153 deletions
|
|
@ -152,9 +152,9 @@ class PlayerState extends _$PlayerState {
|
||||||
}
|
}
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
Duration? currentTime(Ref ref, String libraryItemId) {
|
Future<Duration?> currentTime(Ref ref, String libraryItemId) async {
|
||||||
final me = ref.watch(meProvider);
|
final me = await ref.watch(meProvider.future);
|
||||||
final userProgress = me.valueOrNull?.mediaProgress
|
final userProgress = me.mediaProgress
|
||||||
?.firstWhereOrNull((element) => element.libraryItemId == libraryItemId);
|
?.firstWhereOrNull((element) => element.libraryItemId == libraryItemId);
|
||||||
return userProgress?.currentTime;
|
return userProgress?.currentTime;
|
||||||
}
|
}
|
||||||
|
|
@ -192,7 +192,8 @@ class CurrentBook extends _$CurrentBook {
|
||||||
}
|
}
|
||||||
final book = await ref.read(libraryItemProvider(libraryItemId).future);
|
final book = await ref.read(libraryItemProvider(libraryItemId).future);
|
||||||
state = book.media.asBookExpanded;
|
state = book.media.asBookExpanded;
|
||||||
final currentTime = ref.read(currentTimeProvider(libraryItemId));
|
final currentTime =
|
||||||
|
await ref.read(currentTimeProvider(libraryItemId).future);
|
||||||
await ref
|
await ref
|
||||||
.read(absPlayerProvider.notifier)
|
.read(absPlayerProvider.notifier)
|
||||||
.load(state!, initialPosition: currentTime, play: play);
|
.load(state!, initialPosition: currentTime, play: play);
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ final playerActiveProvider = AutoDisposeProvider<bool>.internal(
|
||||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
// ignore: unused_element
|
// ignore: unused_element
|
||||||
typedef PlayerActiveRef = AutoDisposeProviderRef<bool>;
|
typedef PlayerActiveRef = AutoDisposeProviderRef<bool>;
|
||||||
String _$currentTimeHash() => r'079945f118884b57d2e038117c7a7a5b873bc7d1';
|
String _$currentTimeHash() => r'3e7f99dbf48242a5fa0a4239a0f696535d0b4ac9';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
|
|
@ -85,7 +85,7 @@ class _SystemHash {
|
||||||
const currentTimeProvider = CurrentTimeFamily();
|
const currentTimeProvider = CurrentTimeFamily();
|
||||||
|
|
||||||
/// See also [currentTime].
|
/// See also [currentTime].
|
||||||
class CurrentTimeFamily extends Family<Duration?> {
|
class CurrentTimeFamily extends Family<AsyncValue<Duration?>> {
|
||||||
/// See also [currentTime].
|
/// See also [currentTime].
|
||||||
const CurrentTimeFamily();
|
const CurrentTimeFamily();
|
||||||
|
|
||||||
|
|
@ -123,7 +123,7 @@ class CurrentTimeFamily extends Family<Duration?> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// See also [currentTime].
|
/// See also [currentTime].
|
||||||
class CurrentTimeProvider extends AutoDisposeProvider<Duration?> {
|
class CurrentTimeProvider extends AutoDisposeFutureProvider<Duration?> {
|
||||||
/// See also [currentTime].
|
/// See also [currentTime].
|
||||||
CurrentTimeProvider(
|
CurrentTimeProvider(
|
||||||
String libraryItemId,
|
String libraryItemId,
|
||||||
|
|
@ -158,7 +158,7 @@ class CurrentTimeProvider extends AutoDisposeProvider<Duration?> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Override overrideWith(
|
Override overrideWith(
|
||||||
Duration? Function(CurrentTimeRef provider) create,
|
FutureOr<Duration?> Function(CurrentTimeRef provider) create,
|
||||||
) {
|
) {
|
||||||
return ProviderOverride(
|
return ProviderOverride(
|
||||||
origin: this,
|
origin: this,
|
||||||
|
|
@ -175,7 +175,7 @@ class CurrentTimeProvider extends AutoDisposeProvider<Duration?> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
AutoDisposeProviderElement<Duration?> createElement() {
|
AutoDisposeFutureProviderElement<Duration?> createElement() {
|
||||||
return _CurrentTimeProviderElement(this);
|
return _CurrentTimeProviderElement(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -195,13 +195,13 @@ class CurrentTimeProvider extends AutoDisposeProvider<Duration?> {
|
||||||
|
|
||||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
// ignore: unused_element
|
// ignore: unused_element
|
||||||
mixin CurrentTimeRef on AutoDisposeProviderRef<Duration?> {
|
mixin CurrentTimeRef on AutoDisposeFutureProviderRef<Duration?> {
|
||||||
/// The parameter `libraryItemId` of this provider.
|
/// The parameter `libraryItemId` of this provider.
|
||||||
String get libraryItemId;
|
String get libraryItemId;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CurrentTimeProviderElement extends AutoDisposeProviderElement<Duration?>
|
class _CurrentTimeProviderElement
|
||||||
with CurrentTimeRef {
|
extends AutoDisposeFutureProviderElement<Duration?> with CurrentTimeRef {
|
||||||
_CurrentTimeProviderElement(super.provider);
|
_CurrentTimeProviderElement(super.provider);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -275,7 +275,7 @@ final playerStateProvider =
|
||||||
);
|
);
|
||||||
|
|
||||||
typedef _$PlayerState = AutoDisposeNotifier<core.AbsPlayerState>;
|
typedef _$PlayerState = AutoDisposeNotifier<core.AbsPlayerState>;
|
||||||
String _$currentBookHash() => r'eed66894cb003d9d8ebd7b128d6ebb4efd5cda1b';
|
String _$currentBookHash() => r'b4f6b6ccc772631db3dfd9070be3d7487333544d';
|
||||||
|
|
||||||
/// See also [CurrentBook].
|
/// See also [CurrentBook].
|
||||||
@ProviderFor(CurrentBook)
|
@ProviderFor(CurrentBook)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shelfsdk/audiobookshelf_api.dart';
|
|
||||||
import 'package:vaani/constants/sizes.dart';
|
import 'package:vaani/constants/sizes.dart';
|
||||||
import 'package:vaani/features/player/providers/abs_provider.dart';
|
import 'package:vaani/features/player/providers/abs_provider.dart';
|
||||||
import 'package:vaani/features/player/view/player_expanded.dart'
|
import 'package:vaani/features/player/view/player_expanded.dart'
|
||||||
|
|
@ -11,14 +9,13 @@ import 'package:vaani/features/player/view/player_expanded.dart'
|
||||||
import 'package:vaani/features/player/view/player_minimized.dart';
|
import 'package:vaani/features/player/view/player_minimized.dart';
|
||||||
import 'package:vaani/features/player/view/widgets/audiobook_player_seek_button.dart';
|
import 'package:vaani/features/player/view/widgets/audiobook_player_seek_button.dart';
|
||||||
import 'package:vaani/features/player/view/widgets/audiobook_player_seek_chapter_button.dart';
|
import 'package:vaani/features/player/view/widgets/audiobook_player_seek_chapter_button.dart';
|
||||||
|
import 'package:vaani/features/player/view/widgets/chapter_selection_button.dart';
|
||||||
import 'package:vaani/features/player/view/widgets/player_player_pause_button.dart';
|
import 'package:vaani/features/player/view/widgets/player_player_pause_button.dart';
|
||||||
import 'package:vaani/features/player/view/widgets/player_progress_bar.dart';
|
import 'package:vaani/features/player/view/widgets/player_progress_bar.dart';
|
||||||
import 'package:vaani/features/player/view/widgets/player_speed_adjust_button.dart';
|
import 'package:vaani/features/player/view/widgets/player_speed_adjust_button.dart';
|
||||||
import 'package:vaani/features/skip_start_end/view/skip_start_end_button.dart';
|
import 'package:vaani/features/skip_start_end/view/skip_start_end_button.dart';
|
||||||
import 'package:vaani/features/sleep_timer/view/sleep_timer_button.dart';
|
import 'package:vaani/features/sleep_timer/view/sleep_timer_button.dart';
|
||||||
import 'package:vaani/globals.dart';
|
import 'package:vaani/globals.dart';
|
||||||
import 'package:vaani/shared/extensions/chapter.dart';
|
|
||||||
import 'package:vaani/shared/extensions/duration_format.dart';
|
|
||||||
|
|
||||||
var pendingPlayerModals = 0;
|
var pendingPlayerModals = 0;
|
||||||
|
|
||||||
|
|
@ -104,7 +101,7 @@ class PlayerExpandedDesktop extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: ChapterSelection(),
|
child: ChapterSelectionModal(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -158,76 +155,3 @@ class PlayerExpandedDesktop extends HookConsumerWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChapterSelection extends HookConsumerWidget {
|
|
||||||
const ChapterSelection({super.key});
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final currentChapter = ref.watch(currentChapterProvider);
|
|
||||||
if (currentChapter == null) {
|
|
||||||
return SizedBox.shrink();
|
|
||||||
}
|
|
||||||
final chapters = useState(<BookChapter>[]);
|
|
||||||
final scrollController = useScrollController();
|
|
||||||
useEffect(
|
|
||||||
() {
|
|
||||||
int page = 0;
|
|
||||||
void load(page) {
|
|
||||||
chapters.value.addAll(ref.watch(currentChaptersProvider));
|
|
||||||
}
|
|
||||||
|
|
||||||
load(page);
|
|
||||||
void listener() {
|
|
||||||
if (scrollController.position.pixels /
|
|
||||||
scrollController.position.maxScrollExtent >
|
|
||||||
0.8) {
|
|
||||||
print('滚动到底部');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollController.addListener(listener);
|
|
||||||
return () => scrollController.removeListener(listener);
|
|
||||||
},
|
|
||||||
[scrollController],
|
|
||||||
);
|
|
||||||
|
|
||||||
final currentChapterIndex = chapters.value.indexOf(currentChapter);
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
return Scrollbar(
|
|
||||||
controller: scrollController,
|
|
||||||
child: ListView.builder(
|
|
||||||
controller: scrollController,
|
|
||||||
itemCount: chapters.value.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final chapter = chapters.value[index];
|
|
||||||
final isCurrent = currentChapterIndex == index;
|
|
||||||
final isPlayed = index < currentChapterIndex;
|
|
||||||
return ListTile(
|
|
||||||
autofocus: isCurrent,
|
|
||||||
iconColor: isPlayed && !isCurrent ? theme.disabledColor : null,
|
|
||||||
title: Text(
|
|
||||||
chapter.title,
|
|
||||||
style: isPlayed && !isCurrent
|
|
||||||
? TextStyle(color: theme.disabledColor)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
subtitle: Text(
|
|
||||||
'(${chapter.duration.smartBinaryFormat})',
|
|
||||||
style: isPlayed && !isCurrent
|
|
||||||
? TextStyle(color: theme.disabledColor)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
// trailing: isCurrent
|
|
||||||
// ? const PlayingIndicatorIcon()
|
|
||||||
// : const Icon(Icons.play_arrow),
|
|
||||||
selected: isCurrent,
|
|
||||||
// key: isCurrent ? chapterKey : null,
|
|
||||||
onTap: () {
|
|
||||||
ref.read(absPlayerProvider).switchChapter(chapter.id);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:super_sliver_list/super_sliver_list.dart';
|
||||||
import 'package:vaani/features/player/providers/abs_provider.dart';
|
import 'package:vaani/features/player/providers/abs_provider.dart';
|
||||||
import 'package:vaani/features/player/view/player_expanded.dart'
|
import 'package:vaani/features/player/view/player_expanded.dart'
|
||||||
show pendingPlayerModals;
|
show pendingPlayerModals;
|
||||||
import 'package:vaani/features/player/view/widgets/playing_indicator_icon.dart';
|
import 'package:vaani/features/player/view/widgets/playing_indicator_icon.dart';
|
||||||
import 'package:vaani/generated/l10n.dart';
|
import 'package:vaani/generated/l10n.dart';
|
||||||
import 'package:vaani/globals.dart';
|
|
||||||
import 'package:vaani/shared/extensions/chapter.dart' show ChapterDuration;
|
import 'package:vaani/shared/extensions/chapter.dart' show ChapterDuration;
|
||||||
import 'package:vaani/shared/extensions/duration_format.dart'
|
import 'package:vaani/shared/extensions/duration_format.dart'
|
||||||
show DurationFormat;
|
show DurationFormat;
|
||||||
import 'package:vaani/shared/hooks.dart' show useTimer;
|
import 'package:vaani/shared/hooks.dart' show useLayoutEffect;
|
||||||
|
|
||||||
class ChapterSelectionButton extends HookConsumerWidget {
|
class ChapterSelectionButton extends HookConsumerWidget {
|
||||||
const ChapterSelectionButton({
|
const ChapterSelectionButton({
|
||||||
|
|
@ -53,77 +53,66 @@ class ChapterSelectionModal extends HookConsumerWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final session = ref.watch(currentBookProvider);
|
final book = ref.watch(currentBookProvider);
|
||||||
final currentChapter = ref.watch(currentChapterProvider);
|
final currentChapter = ref.watch(currentChapterProvider);
|
||||||
|
if (currentChapter == null || book == null) {
|
||||||
final currentChapterIndex = currentChapter?.id;
|
return SizedBox.shrink();
|
||||||
final chapterKey = GlobalKey();
|
|
||||||
scrollToCurrentChapter() async {
|
|
||||||
appLogger.fine('scrolling to chapter');
|
|
||||||
await Scrollable.ensureVisible(
|
|
||||||
chapterKey.currentContext!,
|
|
||||||
duration: 200.ms,
|
|
||||||
alignment: 0.5,
|
|
||||||
curve: Curves.easeInOut,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
final initialIndex = book.chapters.indexOf(currentChapter);
|
||||||
|
final scrollController = useScrollController();
|
||||||
|
final listController = ListController();
|
||||||
|
|
||||||
useTimer(scrollToCurrentChapter, 100.ms);
|
// 首次布局完成之后跳转到指定位置
|
||||||
// useInterval(scrollToCurrentChapter, 500.ms);
|
useLayoutEffect(() {
|
||||||
|
listController.jumpToItem(
|
||||||
|
index: initialIndex,
|
||||||
|
scrollController: scrollController,
|
||||||
|
alignment: 0.5,
|
||||||
|
);
|
||||||
|
});
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
'${S.of(context).chapters} ${currentChapterIndex == null ? '' : ' (${currentChapterIndex + 1}/${session?.chapters.length})'}',
|
'${S.of(context).chapters} (${initialIndex + 1}/${book.chapters.length})',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// scroll to current chapter after opening the dialog
|
// scroll to current chapter after opening the dialog
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Scrollbar(
|
child: SuperListView.builder(
|
||||||
child: SingleChildScrollView(
|
listController: listController,
|
||||||
primary: true,
|
controller: scrollController,
|
||||||
child: session?.chapters == null
|
itemCount: book.chapters.length,
|
||||||
? Text(S.of(context).chapterNotFound)
|
itemBuilder: (BuildContext context, int index) {
|
||||||
: Column(
|
final chapter = book.chapters[index];
|
||||||
children: session!.chapters.map(
|
final isCurrent = currentChapter.id == chapter.id;
|
||||||
(chapter) {
|
final isPlayed = index < initialIndex;
|
||||||
final isCurrent = currentChapterIndex == chapter.id;
|
return ListTile(
|
||||||
final isPlayed = currentChapterIndex != null &&
|
autofocus: isCurrent,
|
||||||
chapter.id < currentChapterIndex;
|
iconColor: isPlayed && !isCurrent ? theme.disabledColor : null,
|
||||||
return ListTile(
|
title: Text(
|
||||||
autofocus: isCurrent,
|
chapter.title,
|
||||||
iconColor: isPlayed && !isCurrent
|
style: isPlayed && !isCurrent
|
||||||
? theme.disabledColor
|
? TextStyle(color: theme.disabledColor)
|
||||||
: null,
|
: null,
|
||||||
title: Text(
|
),
|
||||||
chapter.title,
|
subtitle: Text(
|
||||||
style: isPlayed && !isCurrent
|
'(${chapter.duration.smartBinaryFormat})',
|
||||||
? TextStyle(color: theme.disabledColor)
|
style: isPlayed && !isCurrent
|
||||||
: null,
|
? TextStyle(color: theme.disabledColor)
|
||||||
),
|
: null,
|
||||||
subtitle: Text(
|
),
|
||||||
'(${chapter.duration.smartBinaryFormat})',
|
trailing: isCurrent
|
||||||
style: isPlayed && !isCurrent
|
? const PlayingIndicatorIcon()
|
||||||
? TextStyle(color: theme.disabledColor)
|
: const Icon(Icons.play_arrow),
|
||||||
: null,
|
selected: isCurrent,
|
||||||
),
|
onTap: () {
|
||||||
trailing: isCurrent
|
Navigator.of(context).pop();
|
||||||
? const PlayingIndicatorIcon()
|
ref.read(absPlayerProvider).switchChapter(chapter.id);
|
||||||
: const Icon(Icons.play_arrow),
|
},
|
||||||
selected: isCurrent,
|
);
|
||||||
key: isCurrent ? chapterKey : null,
|
},
|
||||||
onTap: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
ref
|
|
||||||
.read(absPlayerProvider)
|
|
||||||
.switchChapter(chapter.id);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
).toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,11 @@ import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
|
||||||
|
// useEffect((){}, []) 组件构建之后执行,但并不保证布局已经完成
|
||||||
|
|
||||||
|
// 自定义
|
||||||
|
// useLayoutEffect((){}) 组件布局完成之后执行 仅执行一次
|
||||||
|
|
||||||
void useInterval(VoidCallback callback, Duration delay) {
|
void useInterval(VoidCallback callback, Duration delay) {
|
||||||
final savedCallback = useRef(callback);
|
final savedCallback = useRef(callback);
|
||||||
savedCallback.value = callback;
|
savedCallback.value = callback;
|
||||||
|
|
@ -28,3 +33,18 @@ void useTimer(VoidCallback callback, Duration delay) {
|
||||||
[delay],
|
[delay],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void useLayoutEffect(VoidCallback callback) {
|
||||||
|
final savedCallback = useRef(callback);
|
||||||
|
savedCallback.value = callback;
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
savedCallback.value();
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1390,6 +1390,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
version: "1.4.1"
|
||||||
|
super_sliver_list:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: super_sliver_list
|
||||||
|
sha256: b1e1e64d08ce40e459b9bb5d9f8e361617c26b8c9f3bb967760b0f436b6e3f56
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.4.1"
|
||||||
synchronized:
|
synchronized:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ dependencies:
|
||||||
# cupertino_icons: ^1.0.6
|
# cupertino_icons: ^1.0.6
|
||||||
# flutter_platform_widgets: ^9.0.0
|
# flutter_platform_widgets: ^9.0.0
|
||||||
flutter_staggered_grid_view: ^0.7.0
|
flutter_staggered_grid_view: ^0.7.0
|
||||||
|
super_sliver_list: ^0.4.1
|
||||||
duration_picker: ^1.2.0
|
duration_picker: ^1.2.0
|
||||||
dynamic_color: ^1.7.0
|
dynamic_color: ^1.7.0
|
||||||
# easy_stepper: ^0.8.4
|
# easy_stepper: ^0.8.4
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue