Added ability to change font size + added new grid view and list view also to discover tabs + update library search on homescreen
This commit is contained in:
@@ -59,9 +59,6 @@ class BookViewBloc extends Bloc<BookViewEvent, BookViewState> {
|
||||
isLoading: false,
|
||||
books: books,
|
||||
offset: state.limit,
|
||||
// OPDS returns the whole feed at once; everything else paginates until
|
||||
// an empty page. Don't gate on the parsed count (a dropped/short page
|
||||
// would falsely stop pagination — e.g. the authors sort).
|
||||
hasMoreBooks: isOpds ? false : books.isNotEmpty,
|
||||
isOpds: isOpds,
|
||||
),
|
||||
|
||||
@@ -19,6 +19,7 @@ import 'package:calibre_web_companion/features/book_view/presentation/widgets/se
|
||||
import 'package:calibre_web_companion/features/scan_book/presentation/pages/scan_book_page.dart';
|
||||
import 'package:calibre_web_companion/features/scan_book/presentation/scan_flow.dart';
|
||||
import 'package:calibre_web_companion/shared/widgets/app_options_sheet.dart';
|
||||
import 'package:calibre_web_companion/shared/widgets/book_view_mode_selector.dart';
|
||||
|
||||
class BookViewPage extends StatefulWidget {
|
||||
const BookViewPage({super.key});
|
||||
@@ -76,13 +77,32 @@ class _BookViewPageState extends State<BookViewPage> {
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
final bool isSearching = (state.searchQuery ?? '').isNotEmpty;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(localizations.books),
|
||||
leading:
|
||||
isSearching
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.arrow_back_rounded),
|
||||
tooltip: localizations.clearSearch,
|
||||
onPressed: () => _clearSearch(context),
|
||||
)
|
||||
: null,
|
||||
title:
|
||||
isSearching
|
||||
? Text('${localizations.search}: ${state.searchQuery}')
|
||||
: Text(localizations.books),
|
||||
actions: [
|
||||
_buildColumnSelector(context, state, localizations),
|
||||
const BookViewModeSelector(),
|
||||
if (!state.isOpds) _buildSortOptions(context, localizations),
|
||||
_buildSearchButton(context, localizations),
|
||||
if (isSearching)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
tooltip: localizations.clearSearch,
|
||||
onPressed: () => _clearSearch(context),
|
||||
)
|
||||
else
|
||||
_buildSearchButton(context, localizations),
|
||||
],
|
||||
),
|
||||
body: _buildBody(context, state, localizations),
|
||||
@@ -259,71 +279,6 @@ class _BookViewPageState extends State<BookViewPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildColumnSelector(
|
||||
BuildContext context,
|
||||
BookViewState state,
|
||||
AppLocalizations localizations,
|
||||
) {
|
||||
return PopupMenuButton<dynamic>(
|
||||
icon: Icon(
|
||||
state.isListView ? Icons.view_list_rounded : Icons.grid_view_rounded,
|
||||
),
|
||||
tooltip: localizations.columnsCount,
|
||||
onSelected: (dynamic value) {
|
||||
if (value == 'list') {
|
||||
context.read<BookViewBloc>().add(const SetViewMode(true));
|
||||
} else if (value is int) {
|
||||
context.read<BookViewBloc>().add(ChangeColumnCount(value));
|
||||
}
|
||||
},
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
PopupMenuItem(
|
||||
value: 'list',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.view_list,
|
||||
color:
|
||||
state.isListView
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(localizations.listView),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
for (int i = 1; i <= 5; i++)
|
||||
PopupMenuItem<int>(
|
||||
value: i,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
i == 1
|
||||
? Icons.looks_one
|
||||
: i == 2
|
||||
? Icons.looks_two
|
||||
: i == 3
|
||||
? Icons.looks_3
|
||||
: i == 4
|
||||
? Icons.looks_4
|
||||
: Icons.looks_5,
|
||||
color:
|
||||
!state.isListView && state.columnCount == i
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text('$i ${localizations.columns}'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSortOptions(
|
||||
BuildContext context,
|
||||
AppLocalizations localizations,
|
||||
@@ -433,6 +388,10 @@ class _BookViewPageState extends State<BookViewPage> {
|
||||
);
|
||||
}
|
||||
|
||||
void _clearSearch(BuildContext context) {
|
||||
context.read<BookViewBloc>().add(const SearchBooks(''));
|
||||
}
|
||||
|
||||
Future<void> _pickAndUploadBook(
|
||||
BuildContext context,
|
||||
AppLocalizations localizations,
|
||||
|
||||
@@ -9,12 +9,16 @@ import 'package:calibre_web_companion/l10n/app_localizations.dart';
|
||||
import 'package:calibre_web_companion/features/book_details/presentation/pages/book_details_page.dart';
|
||||
import 'package:calibre_web_companion/shared/widgets/book_card_skeleton_widget.dart';
|
||||
import 'package:calibre_web_companion/shared/widgets/book_card_widget.dart';
|
||||
import 'package:calibre_web_companion/shared/widgets/book_cover_widget.dart';
|
||||
import 'package:calibre_web_companion/shared/widgets/book_view_mode_selector.dart';
|
||||
import 'package:calibre_web_companion/features/book_view/bloc/book_view_bloc.dart';
|
||||
import 'package:calibre_web_companion/core/services/app_transition.dart';
|
||||
import 'package:calibre_web_companion/core/services/snackbar.dart';
|
||||
import 'package:calibre_web_companion/features/discover/blocs/discover_event.dart';
|
||||
import 'package:calibre_web_companion/features/discover_details/data/models/category_feed_model.dart';
|
||||
import 'package:calibre_web_companion/features/discover_details/data/models/category_model.dart';
|
||||
import 'package:calibre_web_companion/features/discover_details/data/models/discover_feed_model.dart';
|
||||
import 'package:calibre_web_companion/features/discover_details/data/models/discover_details_model.dart';
|
||||
import 'package:calibre_web_companion/features/discover_details/presentation/widgets/category_list_item_skeleton_widget.dart';
|
||||
import 'package:calibre_web_companion/features/discover_details/presentation/widgets/category_list_item_widget.dart';
|
||||
import 'package:calibre_web_companion/main.dart';
|
||||
@@ -71,6 +75,8 @@ class DiscoverDetailsPage extends StatelessWidget {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: _buildAppBarTitle(context, title, categoryType),
|
||||
actions:
|
||||
state.isShowingBooks ? const [BookViewModeSelector()] : null,
|
||||
),
|
||||
body: _buildBody(context, state, localizations),
|
||||
);
|
||||
@@ -216,11 +222,23 @@ class DiscoverDetailsPage extends StatelessWidget {
|
||||
Widget _buildBookGrid(BuildContext context, DiscoverFeedModel feed) {
|
||||
return BlocBuilder<DiscoverDetailsBloc, DiscoverDetailsState>(
|
||||
builder: (context, state) {
|
||||
final viewState = context.watch<BookViewBloc>().state;
|
||||
|
||||
if (viewState.isListView) {
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
itemCount: feed.books.length,
|
||||
itemBuilder:
|
||||
(context, index) =>
|
||||
_buildBookListTile(context, feed.books[index], state),
|
||||
);
|
||||
}
|
||||
|
||||
return GridView.builder(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: 0.7,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: viewState.columnCount,
|
||||
childAspectRatio: viewState.columnCount <= 2 ? 0.7 : 0.9,
|
||||
crossAxisSpacing: 16.0,
|
||||
mainAxisSpacing: 16.0,
|
||||
),
|
||||
@@ -233,39 +251,7 @@ class DiscoverDetailsPage extends StatelessWidget {
|
||||
title: book.title,
|
||||
authors: book.authors,
|
||||
isLoading: state.loadingBookId == book.id,
|
||||
onTap: () async {
|
||||
final int parsedId = int.tryParse(book.id)!;
|
||||
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder:
|
||||
(context) => BookDetailsPage(
|
||||
bookViewModel: BookDetailsModel(
|
||||
id: parsedId,
|
||||
uuid: book.uuid,
|
||||
title: book.title,
|
||||
authors: book.authors,
|
||||
cover: book.coverUrl ?? '',
|
||||
coverUrl: book.coverUrl,
|
||||
comments: book.summary ?? '',
|
||||
data:
|
||||
book.summary ??
|
||||
'', // <--- DAS HIER HAT GEFEHLT!
|
||||
tags: book.tags,
|
||||
hasCover: book.coverUrl != null,
|
||||
path: '',
|
||||
pubdate: '',
|
||||
series: '',
|
||||
seriesIndex: 0,
|
||||
rating: 0,
|
||||
languages: '',
|
||||
publishers: '',
|
||||
),
|
||||
bookUuid: book.uuid,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onTap: () => _openBook(context, book),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -273,6 +259,103 @@ class DiscoverDetailsPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBookListTile(
|
||||
BuildContext context,
|
||||
DiscoverDetailsModel book,
|
||||
DiscoverDetailsState state,
|
||||
) {
|
||||
final parsedId = int.tryParse(book.id) ?? 0;
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: () => _openBook(context, book),
|
||||
child: SizedBox(
|
||||
height: 140,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 2 / 3,
|
||||
child: BookCoverWidget(
|
||||
bookId: parsedId,
|
||||
coverUrl: book.coverUrl,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
book.title,
|
||||
style: Theme.of(context).textTheme.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
book.authors,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (state.loadingBookId == book.id)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(12),
|
||||
child: SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openBook(BuildContext context, DiscoverDetailsModel book) {
|
||||
final int parsedId = int.tryParse(book.id)!;
|
||||
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder:
|
||||
(context) => BookDetailsPage(
|
||||
bookViewModel: BookDetailsModel(
|
||||
id: parsedId,
|
||||
uuid: book.uuid,
|
||||
title: book.title,
|
||||
authors: book.authors,
|
||||
cover: book.coverUrl ?? '',
|
||||
coverUrl: book.coverUrl,
|
||||
comments: book.summary ?? '',
|
||||
data: book.summary ?? '',
|
||||
tags: book.tags,
|
||||
hasCover: book.coverUrl != null,
|
||||
path: '',
|
||||
pubdate: '',
|
||||
series: '',
|
||||
seriesIndex: 0,
|
||||
rating: 0,
|
||||
languages: '',
|
||||
publishers: '',
|
||||
),
|
||||
bookUuid: book.uuid,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCategoryList(BuildContext context, CategoryFeed feed) {
|
||||
return ListView.builder(
|
||||
itemCount: feed.categories.length,
|
||||
|
||||
@@ -39,6 +39,7 @@ class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
|
||||
on<ResetConnectionTestStatus>(_onResetConnectionTestStatus);
|
||||
on<SetShowSendToEReaderButton>(_onSetShowSendToEReaderButton);
|
||||
on<SetEInkMode>(_onSetEInkMode);
|
||||
on<SetTextScale>(_onSetTextScale);
|
||||
on<SetBookActionsOrder>(_onSetBookActionsOrder);
|
||||
on<SetBookActionEnabled>(_onSetBookActionEnabled);
|
||||
on<ResetBookActionsCustomization>(_onResetBookActionsCustomization);
|
||||
@@ -92,6 +93,7 @@ class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
|
||||
webDavPassword: settings.webDavPassword,
|
||||
isWebDavSyncEnabled: settings.isWebDavSyncEnabled,
|
||||
isEInkMode: settings.isEInkMode,
|
||||
textScale: settings.textScale,
|
||||
bookActionsOrder: settings.bookActionsOrder,
|
||||
enabledBookActions: settings.enabledBookActions,
|
||||
bookDetailsSectionsOrder: settings.bookDetailsSectionsOrder,
|
||||
@@ -527,6 +529,23 @@ class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onSetTextScale(
|
||||
SetTextScale event,
|
||||
Emitter<SettingsState> emit,
|
||||
) async {
|
||||
try {
|
||||
await repository.setTextScale(event.scale);
|
||||
emit(state.copyWith(textScale: event.scale));
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: SettingsStatus.error,
|
||||
errorMessage: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onSetEInkMode(
|
||||
SetEInkMode event,
|
||||
Emitter<SettingsState> emit,
|
||||
|
||||
@@ -220,6 +220,15 @@ class SetEInkMode extends SettingsEvent {
|
||||
List<Object?> get props => [enabled];
|
||||
}
|
||||
|
||||
class SetTextScale extends SettingsEvent {
|
||||
final double scale;
|
||||
|
||||
const SetTextScale(this.scale);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [scale];
|
||||
}
|
||||
|
||||
class SetBookActionsOrder extends SettingsEvent {
|
||||
final List<String> actionKeys;
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ class SettingsState extends Equatable {
|
||||
final String webDavUsername;
|
||||
final String webDavPassword;
|
||||
final bool isEInkMode;
|
||||
final double textScale;
|
||||
final List<String> bookActionsOrder;
|
||||
final List<String> enabledBookActions;
|
||||
final List<String> bookDetailsSectionsOrder;
|
||||
@@ -78,6 +79,7 @@ class SettingsState extends Equatable {
|
||||
this.webDavUsername = '',
|
||||
this.webDavPassword = '',
|
||||
this.isEInkMode = false,
|
||||
this.textScale = 1.0,
|
||||
this.bookActionsOrder = const [],
|
||||
this.enabledBookActions = const [],
|
||||
this.bookDetailsSectionsOrder = const [],
|
||||
@@ -122,6 +124,7 @@ class SettingsState extends Equatable {
|
||||
String? webDavUsername,
|
||||
String? webDavPassword,
|
||||
bool? isEInkMode,
|
||||
double? textScale,
|
||||
List<String>? bookActionsOrder,
|
||||
List<String>? enabledBookActions,
|
||||
List<String>? bookDetailsSectionsOrder,
|
||||
@@ -166,6 +169,7 @@ class SettingsState extends Equatable {
|
||||
webDavUsername: webDavUsername ?? this.webDavUsername,
|
||||
webDavPassword: webDavPassword ?? this.webDavPassword,
|
||||
isEInkMode: isEInkMode ?? this.isEInkMode,
|
||||
textScale: textScale ?? this.textScale,
|
||||
bookActionsOrder: bookActionsOrder ?? this.bookActionsOrder,
|
||||
enabledBookActions: enabledBookActions ?? this.enabledBookActions,
|
||||
bookDetailsSectionsOrder:
|
||||
@@ -213,6 +217,7 @@ class SettingsState extends Equatable {
|
||||
webDavUsername,
|
||||
webDavPassword,
|
||||
isEInkMode,
|
||||
textScale,
|
||||
bookActionsOrder,
|
||||
enabledBookActions,
|
||||
bookDetailsSectionsOrder,
|
||||
|
||||
@@ -58,6 +58,7 @@ class SettingsLocalDataSource {
|
||||
'epub_scroll_direction':
|
||||
sharedPreferences.getString('epub_scroll_direction') ?? 'vertical',
|
||||
'is_eink_mode': sharedPreferences.getBool('is_eink_mode') ?? false,
|
||||
'text_scale': sharedPreferences.getDouble('text_scale') ?? 1.0,
|
||||
'book_actions_order':
|
||||
sharedPreferences.getStringList('book_actions_order') ??
|
||||
BookDetailsActionConfig.defaultOrder,
|
||||
@@ -328,6 +329,15 @@ class SettingsLocalDataSource {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveTextScale(double scale) async {
|
||||
try {
|
||||
await sharedPreferences.setDouble('text_scale', scale);
|
||||
} catch (e) {
|
||||
logger.e('Error saving text scale: $e');
|
||||
throw Exception('Failed to save text scale: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveBookActionsOrder(List<String> actionKeys) async {
|
||||
try {
|
||||
await sharedPreferences.setStringList('book_actions_order', actionKeys);
|
||||
|
||||
@@ -28,6 +28,7 @@ class SettingsModel extends Equatable {
|
||||
final String webDavPassword;
|
||||
final bool isWebDavSyncEnabled;
|
||||
final bool isEInkMode;
|
||||
final double textScale;
|
||||
final List<String> bookActionsOrder;
|
||||
final List<String> enabledBookActions;
|
||||
final List<String> bookDetailsSectionsOrder;
|
||||
@@ -60,6 +61,7 @@ class SettingsModel extends Equatable {
|
||||
required this.webDavPassword,
|
||||
required this.isWebDavSyncEnabled,
|
||||
required this.isEInkMode,
|
||||
required this.textScale,
|
||||
required this.bookActionsOrder,
|
||||
required this.enabledBookActions,
|
||||
required this.bookDetailsSectionsOrder,
|
||||
@@ -95,6 +97,7 @@ class SettingsModel extends Equatable {
|
||||
webDavPassword: json['webdav_password'] ?? '',
|
||||
isWebDavSyncEnabled: json['webdav_enabled'] ?? false,
|
||||
isEInkMode: json['is_eink_mode'] ?? false,
|
||||
textScale: (json['text_scale'] as num?)?.toDouble() ?? 1.0,
|
||||
bookActionsOrder: BookDetailsActionConfig.normalizeOrder(
|
||||
List<String>.from(
|
||||
json['book_actions_order'] ?? BookDetailsActionConfig.defaultOrder,
|
||||
@@ -180,6 +183,7 @@ class SettingsModel extends Equatable {
|
||||
webDavPassword,
|
||||
isWebDavSyncEnabled,
|
||||
isEInkMode,
|
||||
textScale,
|
||||
bookActionsOrder,
|
||||
enabledBookActions,
|
||||
bookDetailsSectionsOrder,
|
||||
|
||||
@@ -243,6 +243,14 @@ class SettingsRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setTextScale(double scale) async {
|
||||
try {
|
||||
await dataSource.saveTextScale(scale);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setBookActionsOrder(List<String> actionKeys) async {
|
||||
try {
|
||||
await dataSource.saveBookActionsOrder(actionKeys);
|
||||
|
||||
@@ -300,6 +300,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
_buildSectionTitle(context, localizations.appearance),
|
||||
const ThemeSelectorWidget(),
|
||||
_buildEInkModeToggle(context, state, localizations),
|
||||
_buildTextScaleSelector(context, state, localizations),
|
||||
const SizedBox(height: 24),
|
||||
_buildSectionTitle(context, localizations.language),
|
||||
_buildLanguageSelector(context, state, localizations),
|
||||
@@ -485,6 +486,83 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTextScaleSelector(
|
||||
BuildContext context,
|
||||
SettingsState state,
|
||||
AppLocalizations localizations,
|
||||
) {
|
||||
double current = state.textScale;
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
elevation: 3,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
|
||||
child: StatefulBuilder(
|
||||
builder: (context, setLocal) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.format_size_rounded,
|
||||
size: 28,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
localizations.fontSize,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
localizations.fontSizeDescription,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${(current * 100).round()}%',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Slider(
|
||||
value: current.clamp(0.8, 1.6),
|
||||
min: 0.8,
|
||||
max: 1.6,
|
||||
divisions: 8,
|
||||
label: '${(current * 100).round()}%',
|
||||
onChanged: (value) => setLocal(() => current = value),
|
||||
onChangeEnd:
|
||||
(value) => context.read<SettingsBloc>().add(
|
||||
SetTextScale(value),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEInkModeToggle(
|
||||
BuildContext context,
|
||||
SettingsState state,
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"me": "Ich",
|
||||
"discoverBooks": "Bücher entdecken",
|
||||
"search": "Suchen",
|
||||
"clearSearch": "Suche zurücksetzen",
|
||||
"searchBook": "Buch suchen",
|
||||
"enterTitleAuthorOrTags": "Titel, Autor oder Tags eingeben ...",
|
||||
"showReadBooks": "Gelesene Bücher",
|
||||
@@ -103,6 +104,8 @@
|
||||
"error": "Fehler",
|
||||
"errorLoadingBooks": "Fehler beim Laden der Bücher",
|
||||
"eInkMode": "E-Ink Modus",
|
||||
"fontSize": "Schriftgröße",
|
||||
"fontSizeDescription": "Textgröße in der gesamten App skalieren",
|
||||
"eInkModeDescription": "Deaktiviert Animationen für E-Ink Geräte",
|
||||
"deleteBook": "Buch löschen",
|
||||
"deleteBookConfirmation": "Bist du sicher, dass du dieses Buch löschen möchtest?",
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"me": "Me",
|
||||
"discoverBooks": "Discover books",
|
||||
"search": "Search",
|
||||
"clearSearch": "Clear search",
|
||||
"searchBook": "Search book",
|
||||
"enterTitleAuthorOrTags": "Enter title, author or tags ...",
|
||||
"showReadBooks": "Show read books",
|
||||
@@ -103,6 +104,8 @@
|
||||
"error": "Error",
|
||||
"errorLoadingBooks": "Error loading books",
|
||||
"eInkMode": "E-Ink Mode",
|
||||
"fontSize": "Font size",
|
||||
"fontSizeDescription": "Scale the text size across the whole app",
|
||||
"eInkModeDescription": "Disables animations for E-Ink devices",
|
||||
"deleteBook": "Delete book",
|
||||
"deleteBookConfirmation": "Are you sure you want to delete this book?",
|
||||
|
||||
+11
-1
@@ -166,7 +166,8 @@ class _MyAppState extends State<MyApp> {
|
||||
previous.themeSource != current.themeSource ||
|
||||
previous.selectedColorKey != current.selectedColorKey ||
|
||||
previous.languageCode != current.languageCode ||
|
||||
previous.isEInkMode != current.isEInkMode,
|
||||
previous.isEInkMode != current.isEInkMode ||
|
||||
previous.textScale != current.textScale,
|
||||
|
||||
builder: (context, settingsState) {
|
||||
return DynamicColorBuilder(
|
||||
@@ -226,6 +227,15 @@ class _MyAppState extends State<MyApp> {
|
||||
themeMode: settingsState.themeMode,
|
||||
navigatorKey: navigatorKey,
|
||||
navigatorObservers: [routeObserver],
|
||||
builder: (context, child) {
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
return MediaQuery(
|
||||
data: mediaQuery.copyWith(
|
||||
textScaler: TextScaler.linear(settingsState.textScale),
|
||||
),
|
||||
child: child ?? const SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
locale: Locale(settingsState.languageCode ?? 'en'),
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'package:calibre_web_companion/l10n/app_localizations.dart';
|
||||
import 'package:calibre_web_companion/features/book_view/bloc/book_view_bloc.dart';
|
||||
import 'package:calibre_web_companion/features/book_view/bloc/book_view_event.dart';
|
||||
import 'package:calibre_web_companion/features/book_view/bloc/book_view_state.dart';
|
||||
|
||||
class BookViewModeSelector extends StatelessWidget {
|
||||
const BookViewModeSelector({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localizations = AppLocalizations.of(context)!;
|
||||
return BlocBuilder<BookViewBloc, BookViewState>(
|
||||
buildWhen:
|
||||
(previous, current) =>
|
||||
previous.isListView != current.isListView ||
|
||||
previous.columnCount != current.columnCount,
|
||||
builder: (context, state) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return PopupMenuButton<dynamic>(
|
||||
icon: Icon(
|
||||
state.isListView
|
||||
? Icons.view_list_rounded
|
||||
: Icons.grid_view_rounded,
|
||||
),
|
||||
tooltip: localizations.columnsCount,
|
||||
onSelected: (dynamic value) {
|
||||
if (value == 'list') {
|
||||
context.read<BookViewBloc>().add(const SetViewMode(true));
|
||||
} else if (value is int) {
|
||||
context.read<BookViewBloc>().add(ChangeColumnCount(value));
|
||||
}
|
||||
},
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
PopupMenuItem(
|
||||
value: 'list',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.view_list,
|
||||
color: state.isListView ? scheme.primary : null,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(localizations.listView),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
for (int i = 1; i <= 5; i++)
|
||||
PopupMenuItem<int>(
|
||||
value: i,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
i == 1
|
||||
? Icons.looks_one
|
||||
: i == 2
|
||||
? Icons.looks_two
|
||||
: i == 3
|
||||
? Icons.looks_3
|
||||
: i == 4
|
||||
? Icons.looks_4
|
||||
: Icons.looks_5,
|
||||
color:
|
||||
!state.isListView && state.columnCount == i
|
||||
? scheme.primary
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text('$i ${localizations.columns}'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user