chore: bootstrap flutter reader app skeleton
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../core/theme/app_theme.dart';
|
||||
import 'router/app_router.dart';
|
||||
|
||||
class ReaderApp extends ConsumerWidget {
|
||||
const ReaderApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final router = ref.watch(appRouterProvider);
|
||||
|
||||
return MaterialApp.router(
|
||||
title: 'Reader App',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: AppTheme.lightTheme,
|
||||
routerConfig: router,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../features/auth/presentation/login_screen.dart';
|
||||
import '../../features/bookshelf/presentation/bookshelf_screen.dart';
|
||||
import '../../features/comments/presentation/comments_screen.dart';
|
||||
import '../../features/genres/presentation/genres_screen.dart';
|
||||
import '../../features/home/presentation/home_screen.dart';
|
||||
import '../../features/novel/presentation/novel_detail_screen.dart';
|
||||
import '../../features/profile/presentation/profile_screen.dart';
|
||||
import '../../features/reader/presentation/reader_screen.dart';
|
||||
import '../../features/search/presentation/search_screen.dart';
|
||||
import '../../features/settings/presentation/settings_screen.dart';
|
||||
import '../../features/splash/presentation/splash_screen.dart';
|
||||
import '../../shared/widgets/app_shell.dart';
|
||||
import 'route_names.dart';
|
||||
|
||||
final appRouterProvider = Provider<GoRouter>((ref) {
|
||||
return GoRouter(
|
||||
initialLocation: RouteNames.splash,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: RouteNames.splash,
|
||||
builder: (context, state) => const SplashScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: RouteNames.login,
|
||||
builder: (context, state) => const LoginScreen(),
|
||||
),
|
||||
ShellRoute(
|
||||
builder: (context, state, child) => AppShell(child: child),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: RouteNames.home,
|
||||
builder: (context, state) => const HomeScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: RouteNames.search,
|
||||
builder: (context, state) => const SearchScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: RouteNames.genres,
|
||||
builder: (context, state) => const GenresScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: RouteNames.bookshelf,
|
||||
builder: (context, state) => const BookshelfScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: RouteNames.profile,
|
||||
builder: (context, state) => const ProfileScreen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: RouteNames.novelDetail,
|
||||
builder: (_, state) => NovelDetailScreen(
|
||||
novelId: state.uri.queryParameters['id'] ?? '',
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: RouteNames.reader,
|
||||
builder: (_, state) => ReaderScreen(
|
||||
chapterId: state.uri.queryParameters['chapterId'] ?? '',
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: RouteNames.comments,
|
||||
builder: (_, state) => CommentsScreen(
|
||||
novelId: state.uri.queryParameters['novelId'] ?? '',
|
||||
chapterId: state.uri.queryParameters['chapterId'],
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: RouteNames.settings,
|
||||
builder: (context, state) => const SettingsScreen(),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
class RouteNames {
|
||||
RouteNames._();
|
||||
|
||||
static const splash = '/';
|
||||
static const home = '/home';
|
||||
static const login = '/login';
|
||||
static const search = '/search';
|
||||
static const genres = '/genres';
|
||||
static const novelDetail = '/novel';
|
||||
static const reader = '/reader';
|
||||
static const bookshelf = '/bookshelf';
|
||||
static const comments = '/comments';
|
||||
static const profile = '/profile';
|
||||
static const settings = '/settings';
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../storage/secure_store.dart';
|
||||
|
||||
class ApiClient {
|
||||
ApiClient({
|
||||
required String baseUrl,
|
||||
required SecureStore secureStore,
|
||||
}) : _secureStore = secureStore,
|
||||
dio = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: baseUrl,
|
||||
connectTimeout: const Duration(seconds: 20),
|
||||
receiveTimeout: const Duration(seconds: 20),
|
||||
headers: const {'Content-Type': 'application/json'},
|
||||
),
|
||||
) {
|
||||
dio.interceptors.add(
|
||||
InterceptorsWrapper(
|
||||
onRequest: (options, handler) async {
|
||||
final token = await _secureStore.getAccessToken();
|
||||
if (token != null && token.isNotEmpty) {
|
||||
options.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
handler.next(options);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final Dio dio;
|
||||
final SecureStore _secureStore;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class LocalStore {
|
||||
static const _kFontSize = 'reader_font_size';
|
||||
static const _kLineHeight = 'reader_line_height';
|
||||
static const _kLetterSpacing = 'reader_letter_spacing';
|
||||
static const _kFontFamily = 'reader_font_family';
|
||||
|
||||
Future<void> saveReadingSettings({
|
||||
required double fontSize,
|
||||
required double lineHeight,
|
||||
required double letterSpacing,
|
||||
required String fontFamily,
|
||||
}) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setDouble(_kFontSize, fontSize);
|
||||
await prefs.setDouble(_kLineHeight, lineHeight);
|
||||
await prefs.setDouble(_kLetterSpacing, letterSpacing);
|
||||
await prefs.setString(_kFontFamily, fontFamily);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getReadingSettings() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return {
|
||||
'fontSize': prefs.getDouble(_kFontSize) ?? 18,
|
||||
'lineHeight': prefs.getDouble(_kLineHeight) ?? 1.8,
|
||||
'letterSpacing': prefs.getDouble(_kLetterSpacing) ?? 0,
|
||||
'fontFamily': prefs.getString(_kFontFamily) ?? 'serif',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
class SecureStore {
|
||||
SecureStore() : _storage = const FlutterSecureStorage();
|
||||
|
||||
static const _kAccessToken = 'access_token';
|
||||
static const _kRefreshToken = 'refresh_token';
|
||||
|
||||
final FlutterSecureStorage _storage;
|
||||
|
||||
Future<void> setAccessToken(String token) =>
|
||||
_storage.write(key: _kAccessToken, value: token);
|
||||
|
||||
Future<String?> getAccessToken() => _storage.read(key: _kAccessToken);
|
||||
|
||||
Future<void> setRefreshToken(String token) =>
|
||||
_storage.write(key: _kRefreshToken, value: token);
|
||||
|
||||
Future<String?> getRefreshToken() => _storage.read(key: _kRefreshToken);
|
||||
|
||||
Future<void> clear() => _storage.deleteAll();
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppTheme {
|
||||
AppTheme._();
|
||||
|
||||
static final ThemeData lightTheme = ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: const Color(0xFF155DFC),
|
||||
brightness: Brightness.light,
|
||||
),
|
||||
scaffoldBackgroundColor: const Color(0xFFF7F9FC),
|
||||
appBarTheme: const AppBarTheme(
|
||||
centerTitle: false,
|
||||
backgroundColor: Color(0xFFF7F9FC),
|
||||
foregroundColor: Color(0xFF121826),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../shared/widgets/feature_placeholder.dart';
|
||||
|
||||
class LoginScreen extends StatelessWidget {
|
||||
const LoginScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Dang nhap')),
|
||||
body: const FeaturePlaceholder(
|
||||
title: 'Google Login',
|
||||
description:
|
||||
'Khung dang nhap Google OAuth cho mobile auth endpoint. Se bo sung token refresh va secure storage trong phase tiep theo.',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../shared/widgets/feature_placeholder.dart';
|
||||
|
||||
class BookshelfScreen extends StatelessWidget {
|
||||
const BookshelfScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTabController(
|
||||
length: 4,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Tu sach'),
|
||||
bottom: const TabBar(
|
||||
isScrollable: true,
|
||||
tabs: [
|
||||
Tab(text: 'Dang doc'),
|
||||
Tab(text: 'Danh dau'),
|
||||
Tab(text: 'Da doc'),
|
||||
Tab(text: 'De cu'),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: const TabBarView(
|
||||
children: [
|
||||
FeaturePlaceholder(
|
||||
title: 'Dang doc',
|
||||
description: 'Danh sach truyện dang doc theo progress sync.',
|
||||
),
|
||||
FeaturePlaceholder(
|
||||
title: 'Danh dau',
|
||||
description: 'Tat ca truyện da bookmark cua user.',
|
||||
),
|
||||
FeaturePlaceholder(
|
||||
title: 'Da doc',
|
||||
description: 'Danh sach truyện da hoan thanh.',
|
||||
),
|
||||
FeaturePlaceholder(
|
||||
title: 'De cu',
|
||||
description: 'Danh sach truyện user da de cu.',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CommentsScreen extends StatelessWidget {
|
||||
const CommentsScreen({
|
||||
super.key,
|
||||
required this.novelId,
|
||||
this.chapterId,
|
||||
});
|
||||
|
||||
final String novelId;
|
||||
final String? chapterId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Binh luan')),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
children: [
|
||||
Text('Novel ID: ${novelId.isEmpty ? '(missing)' : novelId}'),
|
||||
Text('Chapter ID: ${chapterId ?? '(all novel comments)'}'),
|
||||
const SizedBox(height: 12),
|
||||
const Text('Khung danh sach binh luan + form gui comment + phan trang.'),
|
||||
const SizedBox(height: 20),
|
||||
TextField(
|
||||
maxLines: 4,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Viet binh luan cua ban...',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
FilledButton(
|
||||
onPressed: () {},
|
||||
child: const Text('Gui binh luan'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../shared/widgets/feature_placeholder.dart';
|
||||
|
||||
class GenresScreen extends StatelessWidget {
|
||||
const GenresScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('The loai')),
|
||||
body: const FeaturePlaceholder(
|
||||
title: 'Genre Discovery',
|
||||
description:
|
||||
'Khung danh sach the loai va man hinh truyện theo the loai slug de dong bo hanh vi voi web.',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../app/router/route_names.dart';
|
||||
import '../../../shared/widgets/feature_placeholder.dart';
|
||||
|
||||
class HomeScreen extends StatelessWidget {
|
||||
const HomeScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Trang chu')),
|
||||
body: FeaturePlaceholder(
|
||||
title: 'Home Feed',
|
||||
description:
|
||||
'Khung trang chu cho carousel hot, random grid, bang de cu, bang xep hang, truyện moi cap nhat va comments gan day.',
|
||||
actions: [
|
||||
FilledButton(
|
||||
onPressed: () => context.go(RouteNames.search),
|
||||
child: const Text('Mo tim kiem'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../app/router/route_names.dart';
|
||||
|
||||
class NovelDetailScreen extends StatelessWidget {
|
||||
const NovelDetailScreen({super.key, required this.novelId});
|
||||
|
||||
final String novelId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Chi tiet truyện')),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
children: [
|
||||
Text('Novel ID: ${novelId.isEmpty ? '(missing)' : novelId}'),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'Khung chi tiet truyện: metadata, series, chapter list, rating, bookmark, recommendation, comments.',
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Wrap(
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
children: [
|
||||
FilledButton(
|
||||
onPressed: () => context.push('${RouteNames.reader}?chapterId=1'),
|
||||
child: const Text('Doc chuong 1'),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: () =>
|
||||
context.push('${RouteNames.comments}?novelId=$novelId'),
|
||||
child: const Text('Xem binh luan'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../app/router/route_names.dart';
|
||||
import '../../../shared/widgets/feature_placeholder.dart';
|
||||
|
||||
class ProfileScreen extends StatelessWidget {
|
||||
const ProfileScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Tai khoan')),
|
||||
body: FeaturePlaceholder(
|
||||
title: 'User Profile',
|
||||
description:
|
||||
'Khung profile user, thong tin session, thong ke bookmark/de cu va cac cai dat doc dong bo.',
|
||||
actions: [
|
||||
FilledButton(
|
||||
onPressed: () => context.push(RouteNames.settings),
|
||||
child: const Text('Mo cai dat doc'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../app/router/route_names.dart';
|
||||
|
||||
class ReaderScreen extends StatefulWidget {
|
||||
const ReaderScreen({super.key, required this.chapterId});
|
||||
|
||||
final String chapterId;
|
||||
|
||||
@override
|
||||
State<ReaderScreen> createState() => _ReaderScreenState();
|
||||
}
|
||||
|
||||
class _ReaderScreenState extends State<ReaderScreen> {
|
||||
double fontSize = 18;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Doc chuong ${widget.chapterId.isEmpty ? '?' : widget.chapterId}'),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () => context.push(RouteNames.settings),
|
||||
icon: const Icon(Icons.tune),
|
||||
tooltip: 'Cai dat doc',
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Reader body placeholder with TOC, TTS, offline marker.'),
|
||||
const SizedBox(height: 12),
|
||||
Text('Co chu hien tai: ${fontSize.toStringAsFixed(0)}'),
|
||||
Slider(
|
||||
min: 14,
|
||||
max: 26,
|
||||
value: fontSize,
|
||||
onChanged: (v) => setState(() => fontSize = v),
|
||||
),
|
||||
const Spacer(),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () {},
|
||||
child: const Text('Chuong truoc'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: FilledButton(
|
||||
onPressed: () {},
|
||||
child: const Text('Chuong sau'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.small(
|
||||
onPressed: () {},
|
||||
child: const Icon(Icons.record_voice_over),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../shared/widgets/feature_placeholder.dart';
|
||||
|
||||
class SearchScreen extends StatelessWidget {
|
||||
const SearchScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Tim kiem')),
|
||||
body: const FeaturePlaceholder(
|
||||
title: 'Search + Filters',
|
||||
description:
|
||||
'Khung tim kiem truyện voi goi y theo tu khoa, loc theo the loai/trang thai va sap xep theo views-rating-latest.',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SettingsScreen extends StatefulWidget {
|
||||
const SettingsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<SettingsScreen> createState() => _SettingsScreenState();
|
||||
}
|
||||
|
||||
class _SettingsScreenState extends State<SettingsScreen> {
|
||||
double fontSize = 18;
|
||||
double lineHeight = 1.8;
|
||||
double letterSpacing = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Cai dat doc')),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
children: [
|
||||
Text('Co chu: ${fontSize.toStringAsFixed(0)}'),
|
||||
Slider(
|
||||
min: 14,
|
||||
max: 26,
|
||||
value: fontSize,
|
||||
onChanged: (v) => setState(() => fontSize = v),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text('Line-height: ${lineHeight.toStringAsFixed(1)}'),
|
||||
Slider(
|
||||
min: 1.2,
|
||||
max: 2.4,
|
||||
value: lineHeight,
|
||||
onChanged: (v) => setState(() => lineHeight = v),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text('Letter-spacing: ${letterSpacing.toStringAsFixed(1)}'),
|
||||
Slider(
|
||||
min: -0.5,
|
||||
max: 2,
|
||||
value: letterSpacing,
|
||||
onChanged: (v) => setState(() => letterSpacing = v),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FilledButton(
|
||||
onPressed: () {},
|
||||
child: const Text('Luu va dong bo'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../app/router/route_names.dart';
|
||||
|
||||
class SplashScreen extends StatefulWidget {
|
||||
const SplashScreen({super.key});
|
||||
|
||||
@override
|
||||
State<SplashScreen> createState() => _SplashScreenState();
|
||||
}
|
||||
|
||||
class _SplashScreenState extends State<SplashScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Future<void>.delayed(const Duration(milliseconds: 700), () {
|
||||
if (!mounted) return;
|
||||
context.go(RouteNames.home);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Scaffold(
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.menu_book_rounded, size: 48),
|
||||
SizedBox(height: 12),
|
||||
Text('Reader App'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'app/app.dart';
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
runApp(const ProviderScope(child: ReaderApp()));
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../app/router/route_names.dart';
|
||||
|
||||
class AppShell extends StatelessWidget {
|
||||
const AppShell({super.key, required this.child});
|
||||
|
||||
final Widget child;
|
||||
|
||||
int _indexForLocation(String location) {
|
||||
if (location.startsWith(RouteNames.search)) return 1;
|
||||
if (location.startsWith(RouteNames.bookshelf)) return 2;
|
||||
if (location.startsWith(RouteNames.genres)) return 3;
|
||||
if (location.startsWith(RouteNames.profile)) return 4;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final location = GoRouterState.of(context).uri.path;
|
||||
final selectedIndex = _indexForLocation(location);
|
||||
|
||||
return Scaffold(
|
||||
body: child,
|
||||
bottomNavigationBar: NavigationBar(
|
||||
selectedIndex: selectedIndex,
|
||||
onDestinationSelected: (index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
context.go(RouteNames.home);
|
||||
case 1:
|
||||
context.go(RouteNames.search);
|
||||
case 2:
|
||||
context.go(RouteNames.bookshelf);
|
||||
case 3:
|
||||
context.go(RouteNames.genres);
|
||||
case 4:
|
||||
context.go(RouteNames.profile);
|
||||
}
|
||||
},
|
||||
destinations: const [
|
||||
NavigationDestination(icon: Icon(Icons.home_outlined), label: 'Home'),
|
||||
NavigationDestination(icon: Icon(Icons.search), label: 'Tim kiem'),
|
||||
NavigationDestination(icon: Icon(Icons.bookmark_border), label: 'Tu sach'),
|
||||
NavigationDestination(icon: Icon(Icons.category_outlined), label: 'The loai'),
|
||||
NavigationDestination(icon: Icon(Icons.person_outline), label: 'Tai khoan'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FeaturePlaceholder extends StatelessWidget {
|
||||
const FeaturePlaceholder({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.description,
|
||||
this.actions = const <Widget>[],
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String description;
|
||||
final List<Widget> actions;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: theme.textTheme.headlineSmall),
|
||||
const SizedBox(height: 10),
|
||||
Text(description, style: theme.textTheme.bodyLarge),
|
||||
if (actions.isNotEmpty) ...[
|
||||
const SizedBox(height: 18),
|
||||
Wrap(spacing: 10, runSpacing: 10, children: actions),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user