mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2026-02-16 06:19:35 +00:00
媒体库上拉刷新,下拉加载
This commit is contained in:
parent
bdd85efcd8
commit
ebcbe1774a
20 changed files with 351 additions and 126 deletions
|
|
@ -12,7 +12,7 @@ import 'package:vaani/shared/extensions/obfuscation.dart';
|
||||||
|
|
||||||
final _logger = Logger('AudiobookDownloadManager');
|
final _logger = Logger('AudiobookDownloadManager');
|
||||||
final tq = MemoryTaskQueue();
|
final tq = MemoryTaskQueue();
|
||||||
const downloadDirectory = BaseDirectory.applicationSupport;
|
// const downloadDirectory = BaseDirectory.applicationSupport;
|
||||||
|
|
||||||
class AudiobookDownloadManager {
|
class AudiobookDownloadManager {
|
||||||
// takes in the baseUrl and the token
|
// takes in the baseUrl and the token
|
||||||
|
|
@ -72,16 +72,12 @@ class AudiobookDownloadManager {
|
||||||
late StreamSubscription<TaskUpdate> _updatesSubscription;
|
late StreamSubscription<TaskUpdate> _updatesSubscription;
|
||||||
|
|
||||||
Future<void> queueAudioBookDownload(
|
Future<void> queueAudioBookDownload(
|
||||||
LibraryItemExpanded item, {
|
LibraryItemExpanded item,
|
||||||
String prePath = '',
|
) async {
|
||||||
}) async {
|
|
||||||
_logger.info('queuing download for item: ${item.id}');
|
_logger.info('queuing download for item: ${item.id}');
|
||||||
// create a download task for each file in the item
|
// create a download task for each file in the item
|
||||||
for (final file in item.libraryFiles) {
|
// for (final file in item.libraryFiles) {
|
||||||
// 仅下载音频和视频
|
for (final file in item.media.asBookExpanded.audioFiles) {
|
||||||
if (![FileType.audio, FileType.video].contains(file.fileType)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// check if the file is already downloaded
|
// check if the file is already downloaded
|
||||||
if (isFileDownloaded(
|
if (isFileDownloaded(
|
||||||
constructFilePath(item, file),
|
constructFilePath(item, file),
|
||||||
|
|
@ -93,13 +89,13 @@ class AudiobookDownloadManager {
|
||||||
final task = DownloadTask(
|
final task = DownloadTask(
|
||||||
taskId: file.ino,
|
taskId: file.ino,
|
||||||
url: file.url(baseUrl, item.id, token).toString(),
|
url: file.url(baseUrl, item.id, token).toString(),
|
||||||
directory: prePath + item.relPath,
|
directory: getPath(item.relPath),
|
||||||
filename: file.metadata.filename,
|
filename: file.metadata.filename,
|
||||||
requiresWiFi: requiresWiFi,
|
requiresWiFi: requiresWiFi,
|
||||||
retries: retries,
|
retries: retries,
|
||||||
allowPause: allowPause,
|
allowPause: allowPause,
|
||||||
group: item.id,
|
group: item.id,
|
||||||
baseDirectory: downloadDirectory,
|
baseDirectory: baseDirectory,
|
||||||
updates: Updates.statusAndProgress,
|
updates: Updates.statusAndProgress,
|
||||||
// metaData: token
|
// metaData: token
|
||||||
);
|
);
|
||||||
|
|
@ -111,9 +107,9 @@ class AudiobookDownloadManager {
|
||||||
|
|
||||||
String constructFilePath(
|
String constructFilePath(
|
||||||
LibraryItemExpanded item,
|
LibraryItemExpanded item,
|
||||||
LibraryFile file,
|
AudioFile file,
|
||||||
) =>
|
) =>
|
||||||
'${appSupportDir.path}/${item.relPath}/${file.metadata.filename}';
|
'$basePath/${item.relPath}/${file.metadata.filename}';
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_updatesSubscription.cancel();
|
_updatesSubscription.cancel();
|
||||||
|
|
@ -129,11 +125,12 @@ class AudiobookDownloadManager {
|
||||||
return File(filePath).existsSync();
|
return File(filePath).existsSync();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<LibraryFile>> getDownloadedFilesMetadata(
|
Future<List<AudioFile>> getDownloadedFilesMetadata(
|
||||||
LibraryItemExpanded item,
|
LibraryItemExpanded item,
|
||||||
) async {
|
) async {
|
||||||
final downloadedFiles = <LibraryFile>[];
|
final downloadedFiles = <AudioFile>[];
|
||||||
for (final file in item.libraryFiles) {
|
// for (final file in item.libraryFiles) {
|
||||||
|
for (final file in item.media.asBookExpanded.audioFiles) {
|
||||||
final filePath = constructFilePath(item, file);
|
final filePath = constructFilePath(item, file);
|
||||||
if (isFileDownloaded(filePath)) {
|
if (isFileDownloaded(filePath)) {
|
||||||
downloadedFiles.add(file);
|
downloadedFiles.add(file);
|
||||||
|
|
@ -152,7 +149,8 @@ class AudiobookDownloadManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> isItemDownloaded(LibraryItemExpanded item) async {
|
Future<bool> isItemDownloaded(LibraryItemExpanded item) async {
|
||||||
for (final file in item.libraryFiles) {
|
// for (final file in item.libraryFiles) {
|
||||||
|
for (final file in item.media.asBookExpanded.audioFiles) {
|
||||||
if (!isFileDownloaded(constructFilePath(item, file))) {
|
if (!isFileDownloaded(constructFilePath(item, file))) {
|
||||||
_logger.info('file not downloaded: ${file.metadata.filename}');
|
_logger.info('file not downloaded: ${file.metadata.filename}');
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -164,7 +162,8 @@ class AudiobookDownloadManager {
|
||||||
|
|
||||||
Future<void> deleteDownloadedItem(LibraryItemExpanded item) async {
|
Future<void> deleteDownloadedItem(LibraryItemExpanded item) async {
|
||||||
_logger.info('deleting downloaded item with id: ${item.id}');
|
_logger.info('deleting downloaded item with id: ${item.id}');
|
||||||
for (final file in item.libraryFiles) {
|
// for (final file in item.libraryFiles) {
|
||||||
|
for (final file in item.media.asBookExpanded.audioFiles) {
|
||||||
final filePath = constructFilePath(item, file);
|
final filePath = constructFilePath(item, file);
|
||||||
if (isFileDownloaded(filePath)) {
|
if (isFileDownloaded(filePath)) {
|
||||||
File(filePath).deleteSync();
|
File(filePath).deleteSync();
|
||||||
|
|
@ -175,7 +174,8 @@ class AudiobookDownloadManager {
|
||||||
|
|
||||||
Future<List<Uri>> getDownloadedFilesUri(LibraryItemExpanded item) async {
|
Future<List<Uri>> getDownloadedFilesUri(LibraryItemExpanded item) async {
|
||||||
final files = <Uri>[];
|
final files = <Uri>[];
|
||||||
for (final file in item.libraryFiles) {
|
// for (final file in item.libraryFiles) {
|
||||||
|
for (final file in item.media.asBookExpanded.audioFiles) {
|
||||||
final filePath = constructFilePath(item, file);
|
final filePath = constructFilePath(item, file);
|
||||||
if (isFileDownloaded(filePath)) {
|
if (isFileDownloaded(filePath)) {
|
||||||
files.add(Uri.file(filePath));
|
files.add(Uri.file(filePath));
|
||||||
|
|
@ -208,4 +208,29 @@ class AudiobookDownloadManager {
|
||||||
_logger.warning('Error when listening to download manager updates: $e');
|
_logger.warning('Error when listening to download manager updates: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String getPath(String relPath) {
|
||||||
|
if (path.isNotEmpty) {
|
||||||
|
return '$path/$relPath';
|
||||||
|
}
|
||||||
|
return relPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
String get basePath => switch (baseDirectory) {
|
||||||
|
BaseDirectory.applicationSupport => appSupportDir.path,
|
||||||
|
BaseDirectory.applicationDocuments => appDocumentsDir.path,
|
||||||
|
_ => path,
|
||||||
|
};
|
||||||
|
|
||||||
|
BaseDirectory get baseDirectory {
|
||||||
|
if (path.isNotEmpty) {
|
||||||
|
return BaseDirectory.root;
|
||||||
|
} else if (Platform.isIOS || Platform.isMacOS) {
|
||||||
|
return BaseDirectory.applicationDocuments;
|
||||||
|
}
|
||||||
|
// else if (Platform.isAndroid) {
|
||||||
|
// return BaseDirectory.temporary;
|
||||||
|
// }
|
||||||
|
return BaseDirectory.applicationSupport;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:background_downloader/background_downloader.dart';
|
import 'package:background_downloader/background_downloader.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
@ -71,24 +69,6 @@ class DownloadManager extends _$DownloadManager {
|
||||||
await state.deleteDownloadedItem(item);
|
await state.deleteDownloadedItem(item);
|
||||||
ref.notifyListeners();
|
ref.notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
String _getDirectory(String path) {
|
|
||||||
if (Platform.isWindows) {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
BaseDirectory _getBaseDirectory() {
|
|
||||||
if (Platform.isIOS) {
|
|
||||||
return BaseDirectory.applicationDocuments;
|
|
||||||
} else if (Platform.isAndroid) {
|
|
||||||
return BaseDirectory.temporary;
|
|
||||||
} else if (Platform.isWindows) {
|
|
||||||
return BaseDirectory.root;
|
|
||||||
}
|
|
||||||
return BaseDirectory.applicationSupport;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,7 @@ final simpleDownloadManagerProvider = NotifierProvider<SimpleDownloadManager,
|
||||||
);
|
);
|
||||||
|
|
||||||
typedef _$SimpleDownloadManager = Notifier<core.AudiobookDownloadManager>;
|
typedef _$SimpleDownloadManager = Notifier<core.AudiobookDownloadManager>;
|
||||||
String _$downloadManagerHash() => r'92afe484d6735d5de53473011ea9ecbad107fc1c';
|
String _$downloadManagerHash() => r'852012e32e613f86445afc7f7e4e85bec808e982';
|
||||||
|
|
||||||
/// See also [DownloadManager].
|
/// See also [DownloadManager].
|
||||||
@ProviderFor(DownloadManager)
|
@ProviderFor(DownloadManager)
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import 'package:vaani/globals.dart';
|
||||||
import 'package:vaani/router/router.dart';
|
import 'package:vaani/router/router.dart';
|
||||||
import 'package:vaani/shared/extensions/model_conversions.dart';
|
import 'package:vaani/shared/extensions/model_conversions.dart';
|
||||||
import 'package:vaani/shared/utils.dart';
|
import 'package:vaani/shared/utils.dart';
|
||||||
|
import 'package:vaani/shared/utils/custom_dialog.dart';
|
||||||
|
|
||||||
class LibraryItemActions extends HookConsumerWidget {
|
class LibraryItemActions extends HookConsumerWidget {
|
||||||
const LibraryItemActions({
|
const LibraryItemActions({
|
||||||
|
|
@ -236,6 +237,8 @@ class LibItemDownSheet extends HookConsumerWidget {
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
// final downloadHistory =
|
// final downloadHistory =
|
||||||
// ref.watch(downloadHistoryProvider(group: libraryItemId));
|
// ref.watch(downloadHistoryProvider(group: libraryItemId));
|
||||||
|
// final downloadManager = ref.watch(downloadManagerProvider);
|
||||||
|
// downloadManager.
|
||||||
final libraryItem = ref.watch(libraryItemProvider(libraryItemId));
|
final libraryItem = ref.watch(libraryItemProvider(libraryItemId));
|
||||||
return libraryItem.when(
|
return libraryItem.when(
|
||||||
data: (item) {
|
data: (item) {
|
||||||
|
|
@ -247,19 +250,24 @@ class LibItemDownSheet extends HookConsumerWidget {
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text('下载管理'),
|
Text(S.of(context).bookDownloads),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
appLogger.fine('Pressed delete download button');
|
DialogUtils.deleteDialog(
|
||||||
ref
|
context,
|
||||||
.read(downloadManagerProvider.notifier)
|
name: item.media.metadata.title,
|
||||||
.deleteDownloadedItem(
|
onPressed: () {
|
||||||
item,
|
ref
|
||||||
);
|
.read(downloadManagerProvider.notifier)
|
||||||
|
.deleteDownloadedItem(
|
||||||
|
item,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
icon: Icon(Icons.delete_outlined),
|
icon: Icon(Icons.delete_outlined),
|
||||||
),
|
),
|
||||||
|
|
@ -307,38 +315,6 @@ class LibItemDownSheet extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showDialog(context, task) {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) {
|
|
||||||
return AlertDialog(
|
|
||||||
title: const Text('Delete'),
|
|
||||||
content: Text(
|
|
||||||
'Are you sure you want to delete ${task.filename}?',
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
// delete the file
|
|
||||||
FileDownloader().database.deleteRecordWithId(
|
|
||||||
task.taskId,
|
|
||||||
);
|
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
|
||||||
child: const Text('Yes'),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
|
||||||
child: const Text('No'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class LibItemDownloadButton extends HookConsumerWidget {
|
class LibItemDownloadButton extends HookConsumerWidget {
|
||||||
|
|
@ -572,12 +548,12 @@ class _LibraryItemPlayButton extends HookConsumerWidget {
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final currentBook = ref.watch(currentBookProvider);
|
final currentBook = ref.watch(currentBookProvider);
|
||||||
final book = item.media.asBookExpanded;
|
final book = item.media.asBookExpanded;
|
||||||
final playerStateNotifier = ref.watch(playerStateProvider.notifier);
|
final playing = ref.watch(playerStateProvider.select((v) => v.playing));
|
||||||
|
final playerStateNotifier = ref.read(playerStateProvider.notifier);
|
||||||
final isLoading = playerStateNotifier.isLoading(book.libraryItemId);
|
final isLoading = playerStateNotifier.isLoading(book.libraryItemId);
|
||||||
final isCurrentBookSetInPlayer =
|
final isCurrentBookSetInPlayer =
|
||||||
currentBook?.libraryItemId == book.libraryItemId;
|
currentBook?.libraryItemId == book.libraryItemId;
|
||||||
final isPlayingThisBook =
|
final isPlayingThisBook = playing && isCurrentBookSetInPlayer;
|
||||||
playerStateNotifier.isPlaying() && isCurrentBookSetInPlayer;
|
|
||||||
|
|
||||||
final userMediaProgress = item.userMediaProgress;
|
final userMediaProgress = item.userMediaProgress;
|
||||||
final isBookCompleted = userMediaProgress?.isFinished ?? false;
|
final isBookCompleted = userMediaProgress?.isFinished ?? false;
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,8 @@ class AbsPlayer extends _$AbsPlayer {
|
||||||
final api = ref.read(authenticatedApiProvider);
|
final api = ref.read(authenticatedApiProvider);
|
||||||
|
|
||||||
final downloadManager = ref.read(simpleDownloadManagerProvider);
|
final downloadManager = ref.read(simpleDownloadManagerProvider);
|
||||||
|
print(downloadManager.basePath);
|
||||||
|
|
||||||
final libItem =
|
final libItem =
|
||||||
await ref.read(libraryItemProvider(book.libraryItemId).future);
|
await ref.read(libraryItemProvider(book.libraryItemId).future);
|
||||||
final downloadedUris = await downloadManager.getDownloadedFilesUri(libItem);
|
final downloadedUris = await downloadManager.getDownloadedFilesUri(libItem);
|
||||||
|
|
|
||||||
|
|
@ -243,7 +243,7 @@ final currentChaptersProvider =
|
||||||
@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 CurrentChaptersRef = AutoDisposeProviderRef<List<api.BookChapter>>;
|
typedef CurrentChaptersRef = AutoDisposeProviderRef<List<api.BookChapter>>;
|
||||||
String _$absPlayerHash() => r'74a59dbf0f9396fef6bb60363fb186f5e4619a63';
|
String _$absPlayerHash() => r'e682fea03793a0370cb143602980d5c1e37396c7';
|
||||||
|
|
||||||
/// 音频播放器 riverpod状态
|
/// 音频播放器 riverpod状态
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -90,32 +90,41 @@ class ChapterSelectionModal extends HookConsumerWidget {
|
||||||
final chapter = book.chapters[index];
|
final chapter = book.chapters[index];
|
||||||
final isCurrent = currentChapter.id == chapter.id;
|
final isCurrent = currentChapter.id == chapter.id;
|
||||||
final isPlayed = index < initialIndex;
|
final isPlayed = index < initialIndex;
|
||||||
return ListTile(
|
return Container(
|
||||||
autofocus: isCurrent,
|
// 自定义autofocus,防止autofocus出现在其他组件底层
|
||||||
iconColor: isPlayed && !isCurrent ? theme.disabledColor : null,
|
decoration: isCurrent
|
||||||
title: Text(
|
? BoxDecoration(
|
||||||
chapter.title,
|
color: Theme.of(context).focusColor, // 背景色
|
||||||
style: isPlayed && !isCurrent
|
)
|
||||||
? TextStyle(color: theme.disabledColor)
|
: null,
|
||||||
: null,
|
child: 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,
|
||||||
|
onTap: () {
|
||||||
|
if (back) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
} else {
|
||||||
|
ref.read(absPlayerProvider).switchChapter(chapter.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
|
||||||
'(${chapter.duration.smartBinaryFormat})',
|
|
||||||
style: isPlayed && !isCurrent
|
|
||||||
? TextStyle(color: theme.disabledColor)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
trailing: isCurrent
|
|
||||||
? const PlayingIndicatorIcon()
|
|
||||||
: const Icon(Icons.play_arrow),
|
|
||||||
selected: isCurrent,
|
|
||||||
onTap: () {
|
|
||||||
if (back) {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
} else {
|
|
||||||
ref.read(absPlayerProvider).switchChapter(chapter.id);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -163,6 +163,17 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||||
"delete": MessageLookupByLibrary.simpleMessage("Delete"),
|
"delete": MessageLookupByLibrary.simpleMessage("Delete"),
|
||||||
"deleteDialog": m2,
|
"deleteDialog": m2,
|
||||||
"deleted": m3,
|
"deleted": m3,
|
||||||
|
"erArmedText": MessageLookupByLibrary.simpleMessage("Release ready"),
|
||||||
|
"erDragText": MessageLookupByLibrary.simpleMessage("Pull to refresh"),
|
||||||
|
"erDragTextUp": MessageLookupByLibrary.simpleMessage("Pull to refresh"),
|
||||||
|
"erFailedText": MessageLookupByLibrary.simpleMessage("Failed"),
|
||||||
|
"erMessageText":
|
||||||
|
MessageLookupByLibrary.simpleMessage("Last updated at %T"),
|
||||||
|
"erNoMoreText": MessageLookupByLibrary.simpleMessage("No more"),
|
||||||
|
"erProcessedText": MessageLookupByLibrary.simpleMessage("Succeeded"),
|
||||||
|
"erProcessingText":
|
||||||
|
MessageLookupByLibrary.simpleMessage("Refreshing..."),
|
||||||
|
"erReadyText": MessageLookupByLibrary.simpleMessage("Refreshing..."),
|
||||||
"explore": MessageLookupByLibrary.simpleMessage("explore"),
|
"explore": MessageLookupByLibrary.simpleMessage("explore"),
|
||||||
"exploreHint": MessageLookupByLibrary.simpleMessage(
|
"exploreHint": MessageLookupByLibrary.simpleMessage(
|
||||||
"Seek and you shall discover...",
|
"Seek and you shall discover...",
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||||
|
|
||||||
static String m1(user) => "用户数: ${user}";
|
static String m1(user) => "用户数: ${user}";
|
||||||
|
|
||||||
static String m2(item) => "确定要删除 ${item} 吗?";
|
static String m2(item) => "是否要删除 ${item} ?";
|
||||||
|
|
||||||
static String m3(item) => "已删除 ${item}";
|
static String m3(item) => "已删除 ${item}";
|
||||||
|
|
||||||
|
|
@ -129,6 +129,15 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||||
"delete": MessageLookupByLibrary.simpleMessage("删除"),
|
"delete": MessageLookupByLibrary.simpleMessage("删除"),
|
||||||
"deleteDialog": m2,
|
"deleteDialog": m2,
|
||||||
"deleted": m3,
|
"deleted": m3,
|
||||||
|
"erArmedText": MessageLookupByLibrary.simpleMessage("准备就绪"),
|
||||||
|
"erDragText": MessageLookupByLibrary.simpleMessage("下拉刷新"),
|
||||||
|
"erDragTextUp": MessageLookupByLibrary.simpleMessage("上拉加载"),
|
||||||
|
"erFailedText": MessageLookupByLibrary.simpleMessage("失败"),
|
||||||
|
"erMessageText": MessageLookupByLibrary.simpleMessage("最后更新于 %T"),
|
||||||
|
"erNoMoreText": MessageLookupByLibrary.simpleMessage("没有更多"),
|
||||||
|
"erProcessedText": MessageLookupByLibrary.simpleMessage("成功"),
|
||||||
|
"erProcessingText": MessageLookupByLibrary.simpleMessage("刷新..."),
|
||||||
|
"erReadyText": MessageLookupByLibrary.simpleMessage("刷新..."),
|
||||||
"explore": MessageLookupByLibrary.simpleMessage("探索"),
|
"explore": MessageLookupByLibrary.simpleMessage("探索"),
|
||||||
"exploreHint": MessageLookupByLibrary.simpleMessage("搜索与探索..."),
|
"exploreHint": MessageLookupByLibrary.simpleMessage("搜索与探索..."),
|
||||||
"exploreTooltip": MessageLookupByLibrary.simpleMessage("搜索和探索"),
|
"exploreTooltip": MessageLookupByLibrary.simpleMessage("搜索和探索"),
|
||||||
|
|
|
||||||
|
|
@ -1894,6 +1894,86 @@ class S {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `Pull to refresh`
|
||||||
|
String get erDragText {
|
||||||
|
return Intl.message(
|
||||||
|
'Pull to refresh',
|
||||||
|
name: 'erDragText',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Pull to refresh`
|
||||||
|
String get erDragTextUp {
|
||||||
|
return Intl.message(
|
||||||
|
'Pull to refresh',
|
||||||
|
name: 'erDragTextUp',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Release ready`
|
||||||
|
String get erArmedText {
|
||||||
|
return Intl.message(
|
||||||
|
'Release ready',
|
||||||
|
name: 'erArmedText',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Refreshing...`
|
||||||
|
String get erReadyText {
|
||||||
|
return Intl.message(
|
||||||
|
'Refreshing...',
|
||||||
|
name: 'erReadyText',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Refreshing...`
|
||||||
|
String get erProcessingText {
|
||||||
|
return Intl.message(
|
||||||
|
'Refreshing...',
|
||||||
|
name: 'erProcessingText',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Succeeded`
|
||||||
|
String get erProcessedText {
|
||||||
|
return Intl.message(
|
||||||
|
'Succeeded',
|
||||||
|
name: 'erProcessedText',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `No more`
|
||||||
|
String get erNoMoreText {
|
||||||
|
return Intl.message('No more', name: 'erNoMoreText', desc: '', args: []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Failed`
|
||||||
|
String get erFailedText {
|
||||||
|
return Intl.message('Failed', name: 'erFailedText', desc: '', args: []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Last updated at %T`
|
||||||
|
String get erMessageText {
|
||||||
|
return Intl.message(
|
||||||
|
'Last updated at %T',
|
||||||
|
name: 'erMessageText',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// `Logs`
|
/// `Logs`
|
||||||
String get logs {
|
String get logs {
|
||||||
return Intl.message('Logs', name: 'logs', desc: '', args: []);
|
return Intl.message('Logs', name: 'logs', desc: '', args: []);
|
||||||
|
|
|
||||||
|
|
@ -272,6 +272,16 @@
|
||||||
"resetAppSettingsDescription": "Reset the app settings to the default values",
|
"resetAppSettingsDescription": "Reset the app settings to the default values",
|
||||||
"resetAppSettingsDialog": "Are you sure you want to reset the app settings?",
|
"resetAppSettingsDialog": "Are you sure you want to reset the app settings?",
|
||||||
|
|
||||||
|
"erDragText": "Pull to refresh",
|
||||||
|
"erDragTextUp": "Pull to refresh",
|
||||||
|
"erArmedText": "Release ready",
|
||||||
|
"erReadyText": "Refreshing...",
|
||||||
|
"erProcessingText": "Refreshing...",
|
||||||
|
"erProcessedText": "Succeeded",
|
||||||
|
"erNoMoreText": "No more",
|
||||||
|
"erFailedText": "Failed",
|
||||||
|
"erMessageText": "Last updated at %T",
|
||||||
|
|
||||||
"logs": "Logs",
|
"logs": "Logs",
|
||||||
"notImplemented": "Not implemented"
|
"notImplemented": "Not implemented"
|
||||||
}
|
}
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
"reset": "重置",
|
"reset": "重置",
|
||||||
"retry": "重试",
|
"retry": "重试",
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
"deleteDialog": "确定要删除 {item} 吗?",
|
"deleteDialog": "是否要删除 {item} ?",
|
||||||
"@deleteDialog": {
|
"@deleteDialog": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"item": {
|
"item": {
|
||||||
|
|
@ -272,6 +272,15 @@
|
||||||
"resetAppSettingsDescription": "将应用程序设置重置为默认值",
|
"resetAppSettingsDescription": "将应用程序设置重置为默认值",
|
||||||
"resetAppSettingsDialog": "您确定要重置应用程序设置吗?",
|
"resetAppSettingsDialog": "您确定要重置应用程序设置吗?",
|
||||||
|
|
||||||
|
"erDragText": "下拉刷新",
|
||||||
|
"erDragTextUp": "上拉加载",
|
||||||
|
"erArmedText": "准备就绪",
|
||||||
|
"erReadyText": "刷新...",
|
||||||
|
"erProcessingText": "刷新...",
|
||||||
|
"erProcessedText": "成功",
|
||||||
|
"erNoMoreText": "没有更多",
|
||||||
|
"erFailedText": "失败",
|
||||||
|
"erMessageText": "最后更新于 %T",
|
||||||
"logs": "日志",
|
"logs": "日志",
|
||||||
"notImplemented": "未实现"
|
"notImplemented": "未实现"
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:easy_refresh/easy_refresh.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||||
|
|
@ -16,6 +17,7 @@ import 'package:vaani/router/router.dart';
|
||||||
import 'package:vaani/shared/extensions/model_conversions.dart';
|
import 'package:vaani/shared/extensions/model_conversions.dart';
|
||||||
import 'package:vaani/shared/extensions/style.dart';
|
import 'package:vaani/shared/extensions/style.dart';
|
||||||
import 'package:vaani/shared/icons/abs_icons.dart';
|
import 'package:vaani/shared/icons/abs_icons.dart';
|
||||||
|
import 'package:vaani/shared/utils/components.dart';
|
||||||
import 'package:vaani/shared/widgets/skeletons.dart';
|
import 'package:vaani/shared/widgets/skeletons.dart';
|
||||||
|
|
||||||
// TODO: implement the library page
|
// TODO: implement the library page
|
||||||
|
|
@ -70,6 +72,11 @@ class LibraryPage extends HookConsumerWidget {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.next_plan),
|
||||||
|
tooltip: '加载下一页', // Helpful tooltip for users
|
||||||
|
onPressed: () => ref.read(libraryItemsProvider.notifier).loadMore(),
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.refresh),
|
icon: Icon(Icons.refresh),
|
||||||
tooltip: '刷新', // Helpful tooltip for users
|
tooltip: '刷新', // Helpful tooltip for users
|
||||||
|
|
@ -85,8 +92,12 @@ class LibraryPage extends HookConsumerWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
// drawer: const MyDrawer(),
|
// drawer: const MyDrawer(),
|
||||||
body: RefreshIndicator(
|
|
||||||
|
body: EasyRefresh(
|
||||||
|
header: Components.easyRefreshHeader(context),
|
||||||
|
footer: Components.easyRefreshFooter(context),
|
||||||
onRefresh: () => ref.read(libraryItemsProvider.notifier).refresh(),
|
onRefresh: () => ref.read(libraryItemsProvider.notifier).refresh(),
|
||||||
|
onLoad: () => ref.read(libraryItemsProvider.notifier).loadMore(),
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final height = getDefaultHeight(context);
|
final height = getDefaultHeight(context);
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,20 @@ extension UserConversion on User {
|
||||||
User get asUser => User.fromJson(toJson());
|
User get asUser => User.fromJson(toJson());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ContentUrlExtension on AudioFile {
|
||||||
|
Uri url(String baseUrl, String itemId, String token) {
|
||||||
|
// /api/items/{itemId}/file/{ino}?{token}
|
||||||
|
// return Uri.parse('$baseUrl/api/items/$itemId/file/$ino?token=$token');
|
||||||
|
var baseUri = Uri.parse(baseUrl);
|
||||||
|
return Uri(
|
||||||
|
scheme: baseUri.scheme,
|
||||||
|
host: baseUri.host,
|
||||||
|
path: '/api/items/$itemId/file/$ino',
|
||||||
|
queryParameters: {'token': token},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension ContentUrl on LibraryFile {
|
extension ContentUrl on LibraryFile {
|
||||||
Uri url(String baseUrl, String itemId, String token) {
|
Uri url(String baseUrl, String itemId, String token) {
|
||||||
// /api/items/{itemId}/file/{ino}?{token}
|
// /api/items/{itemId}/file/{ino}?{token}
|
||||||
|
|
|
||||||
33
lib/shared/utils/components.dart
Normal file
33
lib/shared/utils/components.dart
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import 'package:easy_refresh/easy_refresh.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:vaani/generated/l10n.dart';
|
||||||
|
|
||||||
|
class Components {
|
||||||
|
Components._();
|
||||||
|
static ClassicHeader easyRefreshHeader(BuildContext context) {
|
||||||
|
return ClassicHeader(
|
||||||
|
dragText: S.of(context).erDragText,
|
||||||
|
armedText: S.of(context).erArmedText,
|
||||||
|
readyText: S.of(context).erReadyText,
|
||||||
|
processingText: S.of(context).erProcessingText,
|
||||||
|
processedText: S.of(context).erProcessedText,
|
||||||
|
noMoreText: S.of(context).erNoMoreText,
|
||||||
|
failedText: S.of(context).erFailedText,
|
||||||
|
messageText: S.of(context).erMessageText,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ClassicFooter easyRefreshFooter(BuildContext context) {
|
||||||
|
return ClassicFooter(
|
||||||
|
dragText: S.of(context).erDragTextUp,
|
||||||
|
armedText: S.of(context).erArmedText,
|
||||||
|
readyText: S.of(context).erReadyText,
|
||||||
|
processingText: S.of(context).erProcessingText,
|
||||||
|
processedText: S.of(context).erProcessedText,
|
||||||
|
noMoreText: S.of(context).erNoMoreText,
|
||||||
|
failedText: S.of(context).erFailedText,
|
||||||
|
messageText: S.of(context).erMessageText,
|
||||||
|
infiniteOffset: 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
38
lib/shared/utils/custom_dialog.dart
Normal file
38
lib/shared/utils/custom_dialog.dart
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:vaani/generated/l10n.dart';
|
||||||
|
|
||||||
|
class DialogUtils {
|
||||||
|
DialogUtils._();
|
||||||
|
|
||||||
|
// 自定义删除 dialog
|
||||||
|
static deleteDialog(
|
||||||
|
BuildContext context, {
|
||||||
|
String? name,
|
||||||
|
required Function() onPressed,
|
||||||
|
}) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(S.of(context).delete),
|
||||||
|
content: Text(S.of(context).deleteDialog(name ?? '')),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
onPressed();
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: Text(S.of(context).yes),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: Text(S.of(context).no),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shelfsdk/audiobookshelf_api.dart';
|
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||||
|
import 'package:vaani/api/image_provider.dart';
|
||||||
import 'package:vaani/shared/extensions/model_conversions.dart';
|
import 'package:vaani/shared/extensions/model_conversions.dart';
|
||||||
import 'package:vaani/shared/widgets/shelves/home_shelf.dart';
|
import 'package:vaani/shared/widgets/shelves/home_shelf.dart';
|
||||||
|
|
||||||
|
|
@ -40,7 +41,7 @@ class AuthorOnShelf extends HookConsumerWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final author = item.asMinified;
|
final author = item.asMinified;
|
||||||
// final coverImage = ref.watch(coverImageProvider(item));
|
final coverImage = ref.watch(coverImageProvider(item.id));
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.only(right: 10, bottom: 10),
|
margin: const EdgeInsets.only(right: 10, bottom: 10),
|
||||||
|
|
@ -53,17 +54,17 @@ class AuthorOnShelf extends HookConsumerWidget {
|
||||||
aspectRatio: 1,
|
aspectRatio: 1,
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: const BoxConstraints(maxWidth: 50),
|
constraints: const BoxConstraints(maxWidth: 50),
|
||||||
// child: coverImage.when(
|
child: coverImage.when(
|
||||||
// data: (image) {
|
data: (image) {
|
||||||
// return Image.memory(image, fit: BoxFit.cover);
|
return Image.memory(image, fit: BoxFit.cover);
|
||||||
// },
|
},
|
||||||
// loading: () {
|
loading: () {
|
||||||
// return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
// },
|
},
|
||||||
// error: (error, stack) {
|
error: (error, stack) {
|
||||||
// return const Icon(Icons.error);
|
return const Icon(Icons.error);
|
||||||
// },
|
},
|
||||||
// ),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -213,12 +213,12 @@ class _BookOnShelfPlayButton extends HookConsumerWidget {
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final me = ref.watch(meProvider);
|
final me = ref.watch(meProvider);
|
||||||
final currentBook = ref.watch(currentBookProvider);
|
final currentBook = ref.watch(currentBookProvider);
|
||||||
final playerStateNotifier = ref.watch(playerStateProvider.notifier);
|
final playing = ref.watch(playerStateProvider.select((v) => v.playing));
|
||||||
|
final playerStateNotifier = ref.read(playerStateProvider.notifier);
|
||||||
final isLoading = playerStateNotifier.isLoading(libraryItemId);
|
final isLoading = playerStateNotifier.isLoading(libraryItemId);
|
||||||
final isCurrentBookSetInPlayer =
|
final isCurrentBookSetInPlayer =
|
||||||
currentBook?.libraryItemId == libraryItemId;
|
currentBook?.libraryItemId == libraryItemId;
|
||||||
final isPlayingThisBook =
|
final isPlayingThisBook = playing && isCurrentBookSetInPlayer;
|
||||||
playerStateNotifier.isPlaying() && isCurrentBookSetInPlayer;
|
|
||||||
|
|
||||||
final userProgress = me.valueOrNull?.mediaProgress
|
final userProgress = me.valueOrNull?.mediaProgress
|
||||||
?.firstWhereOrNull((element) => element.libraryItemId == libraryItemId);
|
?.firstWhereOrNull((element) => element.libraryItemId == libraryItemId);
|
||||||
|
|
|
||||||
16
pubspec.lock
16
pubspec.lock
|
|
@ -398,6 +398,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.1"
|
version: "1.8.1"
|
||||||
|
easy_refresh:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: easy_refresh
|
||||||
|
sha256: "486e30abfcaae66c0f2c2798a10de2298eb9dc5e0bb7e1dba9328308968cae0c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.4.0"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -954,6 +962,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.1"
|
version: "1.9.1"
|
||||||
|
path_drawing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_drawing
|
||||||
|
sha256: bbb1934c0cbb03091af082a6389ca2080345291ef07a5fa6d6e078ba8682f977
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
path_parsing:
|
path_parsing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ dependencies:
|
||||||
# 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
|
super_sliver_list: ^0.4.1
|
||||||
|
easy_refresh: ^3.4.0
|
||||||
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