diff --git a/lib/features/authentication/bloc/authentication_bloc.dart b/lib/features/authentication/bloc/authentication_bloc.dart index fd7855cc..8514a5f8 100644 --- a/lib/features/authentication/bloc/authentication_bloc.dart +++ b/lib/features/authentication/bloc/authentication_bloc.dart @@ -10,16 +10,22 @@ import 'package:tsdm_client/utils/debug.dart'; part 'authentication_event.dart'; part 'authentication_state.dart'; +/// Emitter typedef AuthenticationEmitter = Emitter; +/// Bloc the authentication, including login and logout. +/// +/// This bloc should be used as a global long-live bloc. class AuthenticationBloc extends Bloc { - AuthenticationBloc( - {required AuthenticationRepository authenticationRepository}) - : _authenticationRepository = authenticationRepository, + /// Constructor + AuthenticationBloc({ + required AuthenticationRepository authenticationRepository, + }) : _authenticationRepository = authenticationRepository, super(const AuthenticationState()) { on( - _onAuthenticationFetchLoginHashRequested); + _onAuthenticationFetchLoginHashRequested, + ); on(_onAuthenticationLoginRequested); } @@ -32,15 +38,23 @@ class AuthenticationBloc emit(state.copyWith(status: AuthenticationStatus.fetchingHash)); try { final loginHash = await _authenticationRepository.fetchHash(); - emit(state.copyWith( - status: AuthenticationStatus.gotHash, loginHash: loginHash)); + emit( + state.copyWith( + status: AuthenticationStatus.gotHash, + loginHash: loginHash, + ), + ); } on HttpRequestFailedException catch (e) { debug('failed to fetch login hash: $e'); emit(state.copyWith(status: AuthenticationStatus.failed)); } on LoginException catch (e) { debug('failed to fetch login hash: $e'); - emit(state.copyWith( - status: AuthenticationStatus.failed, loginException: e)); + emit( + state.copyWith( + status: AuthenticationStatus.failed, + loginException: e, + ), + ); } } @@ -57,8 +71,12 @@ class AuthenticationBloc emit(state.copyWith(status: AuthenticationStatus.failed)); } on LoginException catch (e) { debug('failed to login: $e'); - emit(state.copyWith( - status: AuthenticationStatus.failed, loginException: e)); + emit( + state.copyWith( + status: AuthenticationStatus.failed, + loginException: e, + ), + ); } } } diff --git a/lib/features/authentication/bloc/authentication_event.dart b/lib/features/authentication/bloc/authentication_event.dart index 735d3eed..aa3ea1e0 100644 --- a/lib/features/authentication/bloc/authentication_event.dart +++ b/lib/features/authentication/bloc/authentication_event.dart @@ -1,5 +1,6 @@ part of 'authentication_bloc.dart'; +/// Event of authentication. sealed class AuthenticationEvent extends Equatable { const AuthenticationEvent(); @@ -10,7 +11,11 @@ sealed class AuthenticationEvent extends Equatable { /// Call this event to fetch hash data required in login process before login. final class AuthenticationFetchLoginHashRequested extends AuthenticationEvent {} +/// User request to login with user credential. final class AuthenticationLoginRequested extends AuthenticationEvent { + /// Constructor. const AuthenticationLoginRequested(this.userCredential) : super(); + + /// User credential. final UserCredential userCredential; } diff --git a/lib/features/authentication/bloc/authentication_state.dart b/lib/features/authentication/bloc/authentication_state.dart index df444394..6cf46eff 100644 --- a/lib/features/authentication/bloc/authentication_state.dart +++ b/lib/features/authentication/bloc/authentication_state.dart @@ -1,12 +1,14 @@ part of 'authentication_bloc.dart'; +/// Status of authentication. enum AuthenticationStatus { + /// Initial state initial, /// Fetching hash data that need to use in login process. fetchingHash, - // + /// After got the form hash. gotHash, /// Polling login request. @@ -19,19 +21,29 @@ enum AuthenticationStatus { failed, } +/// State of authentication. +/// +/// Carrying all current logged user info and login status. final class AuthenticationState extends Equatable { + /// Constructor. const AuthenticationState({ this.status = AuthenticationStatus.initial, this.loginHash, this.loginException, }); + /// Status of authentication. final AuthenticationStatus status; + /// The login hash used to login. + /// + /// Useless unless is going to login. final LoginHash? loginHash; + /// Exception happened in login. final LoginException? loginException; + /// Copy with. AuthenticationState copyWith({ AuthenticationStatus? status, LoginHash? loginHash, diff --git a/lib/features/authentication/repository/authentication_repository.dart b/lib/features/authentication/repository/authentication_repository.dart index f124b5e5..9b06c48d 100644 --- a/lib/features/authentication/repository/authentication_repository.dart +++ b/lib/features/authentication/repository/authentication_repository.dart @@ -38,8 +38,15 @@ import 'package:universal_html/parsing.dart'; /// Status of authentication. enum AuthenticationStatus { + /// Unknown state. + /// + /// Same with [unauthenticated]. unknown, + + /// Have user logged. authenticated, + + /// No one logged. unauthenticated, } @@ -49,6 +56,7 @@ enum AuthenticationStatus { /// /// **Need to call dispose.** class AuthenticationRepository { + /// Constructor. AuthenticationRepository({User? user}) : _authedUser = user; static const _checkAuthUrl = '$baseUrl/home.php?mod=spacecp'; @@ -74,10 +82,13 @@ class AuthenticationRepository { User? _authedUser; + /// The current logged user. User? get currentUser => _authedUser; + /// Authentication status stream. Stream get status => _controller.asBroadcastStream(); + /// Dispose the resources. void dispose() { _controller.close(); } @@ -88,7 +99,8 @@ class AuthenticationRepository { /// /// * **HttpRequestFailedException** when http request failed. /// * **LoginFormHashNotFoundException** when form hash not found. - /// * **LoginInvalidFormHashException** when form hash found but not in the expected format. + /// * **LoginInvalidFormHashException** when form hash found but not in the + /// expected format. Future fetchHash() async { // TODO: Parse CDATA. // 返回的data是xml: @@ -120,10 +132,6 @@ class AuthenticationRepository { return LoginHash(formHash: formHash, loginHash: loginHash); } - Future loginWithCookie(Map cookieMap) async { - throw UnimplementedError(); - } - /// Login with password and other parameters in [credential]. /// /// Will not change authentication status if failed to login. diff --git a/lib/features/authentication/repository/exceptions/exceptions.dart b/lib/features/authentication/repository/exceptions/exceptions.dart index 7e114d5a..5b02b5ad 100644 --- a/lib/features/authentication/repository/exceptions/exceptions.dart +++ b/lib/features/authentication/repository/exceptions/exceptions.dart @@ -1,30 +1,53 @@ +/// Basic exception class of login sealed class LoginException implements Exception {} +/// The form hash used in login progress is not found. final class LoginFormHashNotFoundException implements LoginException {} /// Found form hash, but it's not in the expect format. final class LoginInvalidFormHashException implements LoginException {} +/// The login result message of login progress is not found. +/// +/// Indicating that we do not know whether we logged in successful or not. final class LoginMessageNotFoundException implements LoginException {} +/// The captcha user texted is incorrect. final class LoginIncorrectCaptchaException implements LoginException {} +/// Incorrect password or account. final class LoginInvalidCredentialException implements LoginException {} +/// Security question or its answer is incorrect. final class LoginIncorrectSecurityQuestionException implements LoginException {} +/// Reached the limit of login attempt. +/// +/// Maybe locked in 20 minutes. final class LoginAttemptLimitException implements LoginException {} +/// User info not found when try to login after login seems success. +/// +/// Now we should update the logged user info but this exception means we can +/// not found the logged user info. final class LoginUserInfoNotFoundException implements LoginException {} +/// Some other exception that not recognized. final class LoginOtherErrorException implements LoginException { + /// Constructor. LoginOtherErrorException(this.message); + /// Message to describe the error. final String message; } +/// Basic exception class of logout. sealed class LogoutException implements Exception {} +/// The form hash used to logout is not found. final class LogoutFormHashNotFoundException implements LogoutException {} +/// Failed to logout. +/// +/// Nearly impossible to happen. final class LogoutFailedException implements LogoutException {} diff --git a/lib/features/authentication/repository/internal/login_result.dart b/lib/features/authentication/repository/internal/login_result.dart index fee3e0be..8a112ad8 100644 --- a/lib/features/authentication/repository/internal/login_result.dart +++ b/lib/features/authentication/repository/internal/login_result.dart @@ -51,7 +51,8 @@ enum LoginResult { return LoginResult.success; } - // Impossible unless server response page updated and changed these messages. + // Impossible unless server response page updated and changed these + // messages. debug( 'login result check passed but message check maybe outdated: $message', ); @@ -65,7 +66,8 @@ enum LoginResult { // Other unrecognized error. debug( - 'login result check not passed: alert_info class with unknown message: $message', + 'login result check not passed: ' + 'alert_info class with unknown message: $message', ); return LoginResult.otherError; } @@ -85,7 +87,8 @@ enum LoginResult { // Other unrecognized error. debug( - 'login result check not passed: alert_error with unknown message: $message', + 'login result check not passed: ' + 'alert_error with unknown message: $message', ); return LoginResult.otherError; } diff --git a/lib/features/authentication/repository/internal/user_info.dart b/lib/features/authentication/repository/internal/user_info.dart index af44c303..a29c25e8 100644 --- a/lib/features/authentication/repository/internal/user_info.dart +++ b/lib/features/authentication/repository/internal/user_info.dart @@ -1,8 +1,12 @@ /// User info used in repo. class UserInfo { + /// Constructor. const UserInfo({required this.uid, required this.username}); + /// User id. final String uid; + + /// User name. final String username; @override diff --git a/lib/features/authentication/repository/models/hash.dart b/lib/features/authentication/repository/models/hash.dart index 535272f4..b2f878b0 100644 --- a/lib/features/authentication/repository/models/hash.dart +++ b/lib/features/authentication/repository/models/hash.dart @@ -1,12 +1,19 @@ import 'package:equatable/equatable.dart'; +/// A group of login hash used in login or logout progress. class LoginHash extends Equatable { + /// Constructor. const LoginHash({ required this.formHash, required this.loginHash, }); + /// Form hash. final String formHash; + + /// Login hash. + /// + /// Seems not used. final String loginHash; @override diff --git a/lib/features/authentication/repository/models/user.dart b/lib/features/authentication/repository/models/user.dart index 85493d9a..5d02ff5f 100644 --- a/lib/features/authentication/repository/models/user.dart +++ b/lib/features/authentication/repository/models/user.dart @@ -1,7 +1,15 @@ import 'package:equatable/equatable.dart'; /// Authenticated user. +/// +/// [username], [uid] and [email] should have the same priority in identifying +/// the user. +/// +/// * Though we may not know the all info above when trying to login. +/// * All the info above MUST be provided before save logged info info local +/// storage. class User extends Equatable { + /// Constructor. const User({ this.username, this.uid, @@ -9,9 +17,18 @@ class User extends Equatable { this.email, }); + /// Username. final String? username; + + /// Uid. final String? uid; + + /// Password. + /// + /// Never save this to local store. final String? password; + + /// Email address. final String? email; @override diff --git a/lib/features/authentication/repository/models/user_credential.dart b/lib/features/authentication/repository/models/user_credential.dart index 64dce829..9beb9998 100644 --- a/lib/features/authentication/repository/models/user_credential.dart +++ b/lib/features/authentication/repository/models/user_credential.dart @@ -2,8 +2,13 @@ import 'package:tsdm_client/constants/url.dart'; /// User name field type, pair with password. enum LoginField { + /// Username username, + + /// Email address. email, + + /// Uid uid; @override @@ -18,17 +23,22 @@ enum LoginField { /// Additional security question. class SecurityQuestion { + /// Constructor. const SecurityQuestion({ required this.questionId, required this.answer, }); + /// The question id of security question chose by user. final String questionId; + + /// The answer text that user texted. final String answer; } /// Login credential. class UserCredential { + /// Constructor. const UserCredential({ required this.loginField, required this.loginFieldValue, @@ -76,6 +86,7 @@ class UserCredential { /// Can be null. final SecurityQuestion? securityQuestion; + /// Method to convert to json. Map toJson() { final m = { 'loginfield': loginField.toString(), diff --git a/lib/features/authentication/view/login_page.dart b/lib/features/authentication/view/login_page.dart index 62bb847b..ad19256d 100644 --- a/lib/features/authentication/view/login_page.dart +++ b/lib/features/authentication/view/login_page.dart @@ -6,9 +6,12 @@ import 'package:tsdm_client/features/authentication/repository/exceptions/except import 'package:tsdm_client/features/authentication/widgets/login_form.dart'; import 'package:tsdm_client/generated/i18n/strings.g.dart'; +/// Page of user to login. class LoginPage extends StatefulWidget { + /// Constructor. const LoginPage({this.redirectBackState, super.key}); + /// The redirect back route that navigator will push when logged in succeed. final GoRouterState? redirectBackState; @override @@ -24,8 +27,8 @@ class _LoginPageState extends State { ), body: BlocProvider( create: (context) => AuthenticationBloc( - authenticationRepository: RepositoryProvider.of(context)) - ..add(AuthenticationFetchLoginHashRequested()), + authenticationRepository: RepositoryProvider.of(context), + )..add(AuthenticationFetchLoginHashRequested()), child: BlocListener( listener: (context, state) { if (state.status == AuthenticationStatus.failed) { diff --git a/lib/features/authentication/widgets/captcha_image.dart b/lib/features/authentication/widgets/captcha_image.dart index c3bb3434..4e80847f 100644 --- a/lib/features/authentication/widgets/captcha_image.dart +++ b/lib/features/authentication/widgets/captcha_image.dart @@ -15,7 +15,9 @@ const _captchaImageHeight = 150; /// So indicator should use (150 / 60) * 320 width. const _indicatorBoxWidth = (60 / _captchaImageHeight) * _captchaImageWidth; +/// The captcha image used in login form. class CaptchaImage extends StatefulWidget { + /// Constructor. const CaptchaImage({super.key}); static final Uri _fakeFormVerifyUri = @@ -78,10 +80,11 @@ class _VerityImageState extends State { return Image.memory(bytes, height: 60); } return const SizedBox( - width: _indicatorBoxWidth, - child: Center( - child: CircularProgressIndicator(), - )); + width: _indicatorBoxWidth, + child: Center( + child: CircularProgressIndicator(), + ), + ); }, ), ); diff --git a/lib/features/authentication/widgets/login_form.dart b/lib/features/authentication/widgets/login_form.dart index 28641331..cf95bbf3 100644 --- a/lib/features/authentication/widgets/login_form.dart +++ b/lib/features/authentication/widgets/login_form.dart @@ -18,10 +18,12 @@ final _loginQuestions = [ '您其中一位老师的名字', '您个人计算机的型号', '您最喜欢的餐馆名称', - '驾驶执照的最后四位数字' + '驾驶执照的最后四位数字', ]; +/// Form for user to fill login info. class LoginForm extends StatefulWidget { + /// Constructor. const LoginForm({ this.redirectPath, this.redirectPathParameters, @@ -29,8 +31,13 @@ class LoginForm extends StatefulWidget { super.key, }); + /// The url path to redirect back once login succeed. final String? redirectPath; + + /// The path parameters of url to redirect back. final Map? redirectPathParameters; + + /// The extra object of url to redirect back. final Object? redirectExtra; @override @@ -212,7 +219,9 @@ class _LoginFormState extends State { Navigator.of(context).pop(); } else { debug( - 'login success, redirect back to: path=${widget.redirectPath} with parameters=${widget.redirectPathParameters}, extra=${widget.redirectExtra}', + 'login success, redirect back to: path=${widget.redirectPath} ' + 'with parameters=${widget.redirectPathParameters}, ' + 'extra=${widget.redirectExtra}', ); context.pushReplacementNamed( widget.redirectPath!, diff --git a/lib/features/forum/bloc/forum_bloc.dart b/lib/features/forum/bloc/forum_bloc.dart index 294fb3f4..cf513246 100644 --- a/lib/features/forum/bloc/forum_bloc.dart +++ b/lib/features/forum/bloc/forum_bloc.dart @@ -12,16 +12,17 @@ import 'package:universal_html/html.dart' as uh; part 'forum_event.dart'; part 'forum_state.dart'; +/// Emitter typedef ForumEmitter = Emitter; /// Bloc of forum page. class ForumBloc extends Bloc { + /// Constructor. ForumBloc({ required String fid, required ForumRepository forumRepository, }) : _forumRepository = forumRepository, super(ForumState(fid: fid)) { - // on(_onForumLoadMoreRequested); on(_onForumRefreshRequested); on(_onForumJumpPageRequested); @@ -40,8 +41,8 @@ class ForumBloc extends Bloc { ); emit(await _parseFromDocument(document, event.pageNumber)); } on HttpRequestFailedException catch (e) { - debug( - 'failed to load forum page: fid=${state.fid}, pageNumber=${event.pageNumber}: $e'); + debug('failed to load forum page: fid=${state.fid}, ' + 'pageNumber=${event.pageNumber}: $e'); emit(state.copyWith(status: ForumStatus.failed)); } } @@ -52,10 +53,11 @@ class ForumBloc extends Bloc { ) async { emit( state.copyWith( - status: ForumStatus.loading, - stickThreadList: [], - normalThreadList: [], - subredditList: []), + status: ForumStatus.loading, + stickThreadList: [], + normalThreadList: [], + subredditList: [], + ), ); try { @@ -95,13 +97,20 @@ class ForumBloc extends Bloc { List? stickThreadList; List? subredditList; final normalThreadList = _buildThreadList( - document, 'tsdm_normalthread', NormalThread.fromTBody); + document, + 'tsdm_normalthread', + NormalThread.fromTBody, + ); - // When jump to other pages, pinned thread and subreddits should be reserved in state. + // When jump to other pages, pinned thread and subreddits should be + // reserved in state. // Only the first page has pinned threads and subreddits. if (pageNumber == 1) { stickThreadList = _buildThreadList( - document, 'tsdm_stickthread', StickThread.fromTBody); + document, + 'tsdm_stickthread', + StickThread.fromTBody, + ); subredditList = _buildForumList(document, state.fid); } diff --git a/lib/features/forum/bloc/forum_event.dart b/lib/features/forum/bloc/forum_event.dart index d2897bd7..b1d7e0db 100644 --- a/lib/features/forum/bloc/forum_event.dart +++ b/lib/features/forum/bloc/forum_event.dart @@ -1,5 +1,6 @@ part of 'forum_bloc.dart'; +/// Forum event. sealed class ForumEvent extends Equatable { const ForumEvent(); @@ -7,17 +8,23 @@ sealed class ForumEvent extends Equatable { List get props => []; } +/// User request to refresh the forum page. final class ForumRefreshRequested extends ForumEvent {} /// User requested to load page [pageNumber]. final class ForumLoadMoreRequested extends ForumEvent { + /// Constructor. const ForumLoadMoreRequested(this.pageNumber) : super(); + /// Page number to load. final int pageNumber; } /// User request to jump to another page. final class ForumJumpPageRequested extends ForumEvent { + /// Constructor. const ForumJumpPageRequested(this.pageNumber) : super(); + + /// Page number to jump to. final int pageNumber; } diff --git a/lib/features/forum/bloc/forum_state.dart b/lib/features/forum/bloc/forum_state.dart index 0f17a74e..a8d7e9fb 100644 --- a/lib/features/forum/bloc/forum_state.dart +++ b/lib/features/forum/bloc/forum_state.dart @@ -2,14 +2,22 @@ part of 'forum_bloc.dart'; /// Page status. enum ForumStatus { + /// Initial. initial, + + /// Loading. loading, + + /// Load succeed. success, + + /// Failed to load. failed, } /// State of forum page of the app. class ForumState extends Equatable { + /// Constructor. const ForumState({ required this.fid, this.title, @@ -40,9 +48,11 @@ class ForumState extends Equatable { /// Pinned thread in this forum. /// - /// Only load in the first page and never update because other numbers of pages do not have pinned threads at all. + /// Only load in the first page and never update because other numbers of + /// pages do not have pinned threads at all. final List stickThreadList; + /// All normal thread list. final List normalThreadList; /// All subreddits in this forum. @@ -56,11 +66,13 @@ class ForumState extends Equatable { /// Current pageNumber final int currentPage; + /// How many pages in this forum final int totalPages; /// Flag indicating current user has permission to see this page or not. /// - /// Only works with logged user. If no user logged in, use [needLogin] flag instead. + /// Only works with logged user. If no user logged in, use [needLogin] flag + /// instead. final bool havePermission; /// Message showed from server when have no permission. @@ -71,6 +83,7 @@ class ForumState extends Equatable { /// Only works when no user logged. final bool needLogin; + /// Copy with ForumState copyWith({ ForumStatus? status, String? fid, diff --git a/lib/features/forum/view/forum_page.dart b/lib/features/forum/view/forum_page.dart index 6db2f68b..676ec427 100644 --- a/lib/features/forum/view/forum_page.dart +++ b/lib/features/forum/view/forum_page.dart @@ -27,12 +27,16 @@ const _pinnedTabIndex = 0; const _threadTabIndex = 1; const _subredditTabIndex = 2; +/// Page to show all forum status. class ForumPage extends StatefulWidget { + /// Constructor. const ForumPage({required this.fid, this.title, super.key}) : forumUrl = '$baseUrl/forum.php?mod=forumdisplay&fid=$fid'; /// Forum ID. final String fid; + + /// Forum title. final String? title; /// The url is used to provide features like "open in external browser". @@ -188,7 +192,8 @@ class _ForumPageState extends State } else if (!state.havePermission) { if (state.permissionDeniedMessage != null) { return Center( - child: munchElement(context, state.permissionDeniedMessage!)); + child: munchElement(context, state.permissionDeniedMessage!), + ); } else { return Center(child: Text(context.t.general.noPermission)); } @@ -348,15 +353,18 @@ class _ForumPageState extends State ) : null, onSearch: () async { - await context.pushNamed(ScreenPaths.search, - queryParameters: {'fid': widget.fid}); + await context.pushNamed( + ScreenPaths.search, + queryParameters: {'fid': widget.fid}, + ); }, onJumpPage: (pageNumber) async { if (!mounted) { return; } // Mark loading here. - // Mark state will be removed when loading finishes (next build). + // Mark state will be removed when + // loading finishes (next build). context.read().markLoading(); context .read() @@ -388,14 +396,18 @@ class _ForumPageState extends State if (!context.mounted) { return; } - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text( - context.t.aboutPage.copiedToClipboard, + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + context.t.aboutPage.copiedToClipboard, + ), ), - )); + ); case MenuActions.openInBrowser: - await context.dispatchAsUrl(widget.forumUrl, - external: true); + await context.dispatchAsUrl( + widget.forumUrl, + external: true, + ); case MenuActions.backToTop: await _threadScrollController.animateTo( 0, diff --git a/lib/features/home/cubit/home_cubit.dart b/lib/features/home/cubit/home_cubit.dart index fae3a39e..31b25849 100644 --- a/lib/features/home/cubit/home_cubit.dart +++ b/lib/features/home/cubit/home_cubit.dart @@ -10,6 +10,7 @@ part 'home_state.dart'; /// /// Not the home tab nor the homepage of website. class HomeCubit extends Cubit { + /// Constructor. HomeCubit() : super(const HomeState()); /// Change the current home page state. diff --git a/lib/features/home/cubit/home_state.dart b/lib/features/home/cubit/home_state.dart index 6c2b176b..28b50cab 100644 --- a/lib/features/home/cubit/home_state.dart +++ b/lib/features/home/cubit/home_state.dart @@ -25,6 +25,7 @@ enum HomeTab { /// State of the homepage of the app. final class HomeState extends Equatable { + /// Constructor. const HomeState({this.tab = HomeTab.home}); /// Current tab. @@ -34,8 +35,10 @@ final class HomeState extends Equatable { List get props => [tab]; } +/// Bar item in app navigator. class NavigationBarItem { - NavigationBarItem({ + /// Constructor. + const NavigationBarItem({ required this.icon, required this.selectedIcon, required this.label, @@ -43,13 +46,27 @@ class NavigationBarItem { required this.tab, }); + /// Item icon. + /// + /// Use outline style icons. final Icon icon; + + /// Item icon when selected. + /// + /// Use normal style icons. final Icon selectedIcon; + + /// Name of the item. final String label; + + /// Screen path of the item. final String targetPath; + + /// Tab index. final HomeTab tab; } +/// All navigation bar items. final barItems = [ NavigationBarItem( icon: const Icon(Icons.home_outlined), diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index 469f6a2c..7b427735 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -7,6 +7,7 @@ import 'package:tsdm_client/shared/repositories/forum_home_repository/forum_home /// Page of the homepage of the app. class HomePage extends StatelessWidget { + /// Constructor. const HomePage({ required ForumHomeRepository forumHomeRepository, required this.showNavigationBar, @@ -14,8 +15,12 @@ class HomePage extends StatelessWidget { super.key, }) : _forumHomeRepository = forumHomeRepository; + /// Control to show the app level navigation bar or not. + /// + /// Only show in top pages. final bool showNavigationBar; + /// Child widget, or call it the body widget. final Widget child; final ForumHomeRepository _forumHomeRepository; @@ -27,13 +32,15 @@ class HomePage extends StatelessWidget { create: (_) => HomeCubit(), child: RepositoryProvider.value( value: _forumHomeRepository, - child: Builder(builder: (context) { - return Scaffold( - body: child, - bottomNavigationBar: - showNavigationBar ? const HomeNavigationBar() : null, - ); - }), + child: Builder( + builder: (context) { + return Scaffold( + body: child, + bottomNavigationBar: + showNavigationBar ? const HomeNavigationBar() : null, + ); + }, + ), ), ); } diff --git a/lib/features/home/widgets/home_navigation_bar.dart b/lib/features/home/widgets/home_navigation_bar.dart index 5aed828f..057f8558 100644 --- a/lib/features/home/widgets/home_navigation_bar.dart +++ b/lib/features/home/widgets/home_navigation_bar.dart @@ -4,7 +4,11 @@ import 'package:go_router/go_router.dart'; import 'package:tsdm_client/features/home/cubit/home_cubit.dart'; import 'package:tsdm_client/generated/i18n/strings.g.dart'; +/// Navigation bar or rail of the home page. +/// +/// Root page. class HomeNavigationBar extends StatefulWidget { + /// Constructor. const HomeNavigationBar({super.key}); @override @@ -19,8 +23,13 @@ class _HomeNavigationBarState extends State { return NavigationBar( destinations: barItems - .map((e) => NavigationDestination( - icon: e.icon, selectedIcon: e.selectedIcon, label: e.label)) + .map( + (e) => NavigationDestination( + icon: e.icon, + selectedIcon: e.selectedIcon, + label: e.label, + ), + ) .toList(), selectedIndex: context.select((cubit) => cubit.state.tab).index, diff --git a/lib/features/homepage/bloc/homepage_bloc.dart b/lib/features/homepage/bloc/homepage_bloc.dart index 8843d47b..8e8c5cd9 100644 --- a/lib/features/homepage/bloc/homepage_bloc.dart +++ b/lib/features/homepage/bloc/homepage_bloc.dart @@ -16,7 +16,9 @@ import 'package:universal_html/html.dart' as uh; part 'homepage_event.dart'; part 'homepage_state.dart'; +/// Extension on [uh.Document] to extract user info. extension ExtractProfileAvatar on uh.Document { + /// Extract the user avatar url. String? extractAvatar() { return querySelector('div#wp.wp div#ct.ct2 div.sd div.hm > p > a > img') ?.imageUrl(); @@ -25,6 +27,7 @@ extension ExtractProfileAvatar on uh.Document { /// Bloc for the homepage of the app. class HomepageBloc extends Bloc { + /// Constructor. HomepageBloc({ required ForumHomeRepository forumHomeRepository, required ProfileRepository profileRepository, @@ -32,20 +35,26 @@ class HomepageBloc extends Bloc { }) : _forumHomeRepository = forumHomeRepository, _profileRepository = profileRepository, _authenticationRepository = authenticationRepository, - super(forumHomeRepository.hasCache() - ? _parseStateFromDocument( - forumHomeRepository.getCache()!, - authenticationRepository.currentUser?.username, - avatarUrl: profileRepository.getCache()?.extractAvatar(), - ) - : const HomepageState()) { + super( + forumHomeRepository.hasCache() + ? _parseStateFromDocument( + forumHomeRepository.getCache()!, + authenticationRepository.currentUser?.username, + avatarUrl: profileRepository.getCache()?.extractAvatar(), + ) + : const HomepageState(), + ) { on(_onHomepageLoadRequested); on(_onHomepageRefreshRequested); on<_HomepageAuthChanged>(_onHomepageAuthChanged); - _authStatusSub = _authenticationRepository.status.listen((status) => add( + _authStatusSub = _authenticationRepository.status.listen( + (status) => add( _HomepageAuthChanged( - isLogged: status == AuthenticationStatus.authenticated))); + isLogged: status == AuthenticationStatus.authenticated, + ), + ), + ); } final ForumHomeRepository _forumHomeRepository; @@ -60,19 +69,23 @@ class HomepageBloc extends Bloc { Emitter emit, ) async { if (_forumHomeRepository.hasCache()) { - final s = _parseStateFromDocument(_forumHomeRepository.getCache()!, - _authenticationRepository.currentUser?.username, - avatarUrl: state.loggedUserInfo?.avatarUrl); + final s = _parseStateFromDocument( + _forumHomeRepository.getCache()!, + _authenticationRepository.currentUser?.username, + avatarUrl: state.loggedUserInfo?.avatarUrl, + ); emit(s); return; } // Clear data. emit(const HomepageState(status: HomepageStatus.loading)); try { - final documentList = (await Future.wait([ - _forumHomeRepository.fetchHomePage(), - _profileRepository.fetchProfile() - ])) + final documentList = (await Future.wait( + [ + _forumHomeRepository.fetchHomePage(), + _profileRepository.fetchProfile(), + ], + )) .whereType() .toList(); if (documentList.length != 2) { @@ -127,8 +140,10 @@ class HomepageBloc extends Bloc { ?.imageUrl(); // Parse data and change state. final s = _parseStateFromDocument( - document, _authenticationRepository.currentUser?.username, - avatarUrl: avatarUrl); + document, + _authenticationRepository.currentUser?.username, + avatarUrl: avatarUrl, + ); emit(s); } on HttpHandshakeFailedException catch (e) { debug('failed to fetch dom: $e'); @@ -251,8 +266,8 @@ class HomepageBloc extends Bloc { pinnedThreadGroupList.add(group); } - // The sort on server side is not as displayed, fix the sort to keep the same - // with website appearance. + // The sort on server side is not as displayed, fix the sort to keep the + // same with website appearance. if (pinnedThreadGroupList.length >= 7) { pinnedThreadGroupList ..swap(4, 5) @@ -301,12 +316,14 @@ List _buildKahrpbaPicHrefList(uh.Element? scriptNode) { .innerHtmlEx() .split('\n') .where((e) => e.contains("window.location='")) - .map((e) => e - .split("window.location='") - .lastOrNull - ?.split("'") - .firstOrNull - ?.replaceFirst('&', '&')) + .map( + (e) => e + .split("window.location='") + .lastOrNull + ?.split("'") + .firstOrNull + ?.replaceFirst('&', '&'), + ) .toList(); } diff --git a/lib/features/homepage/bloc/homepage_event.dart b/lib/features/homepage/bloc/homepage_event.dart index 5dedf659..7606540c 100644 --- a/lib/features/homepage/bloc/homepage_event.dart +++ b/lib/features/homepage/bloc/homepage_event.dart @@ -8,9 +8,14 @@ sealed class HomepageEvent extends Equatable { List get props => []; } +/// User request to load the homepage. +/// +/// This will load from cache if available. final class HomepageLoadRequested extends HomepageEvent {} /// User requests to refresh homepage. +/// +/// Directly load homepage from server. final class HomepageRefreshRequested extends HomepageEvent {} /// User requests to login. diff --git a/lib/features/homepage/bloc/homepage_state.dart b/lib/features/homepage/bloc/homepage_state.dart index 6a147da8..cf28be91 100644 --- a/lib/features/homepage/bloc/homepage_state.dart +++ b/lib/features/homepage/bloc/homepage_state.dart @@ -19,19 +19,25 @@ enum HomepageStatus { /// Failed to load data. failed; + /// Is [initial]? bool get isInitial => this == HomepageStatus.initial; + /// Is [needLogin]? bool get isNeedLogin => this == HomepageStatus.needLogin; + /// Is [loading]? bool get isLoading => this == HomepageStatus.loading; + /// Is [success]? bool get isSuccess => this == HomepageStatus.success; + /// Is [failed]? bool get isFailed => this == HomepageStatus.failed; } /// State of homepage. final class HomepageState extends Equatable { + /// Constructor. const HomepageState({ this.status = HomepageStatus.initial, this.forumStatus = const ForumStatus.empty(), @@ -57,6 +63,7 @@ final class HomepageState extends Equatable { /// Swiper urls in the homepage. final List swiperUrlList; + /// Copy with HomepageState copyWith({ HomepageStatus? status, ForumStatus? forumStatus, diff --git a/lib/features/homepage/models/forum_status.dart b/lib/features/homepage/models/forum_status.dart index faaf1c4e..6074134c 100644 --- a/lib/features/homepage/models/forum_status.dart +++ b/lib/features/homepage/models/forum_status.dart @@ -4,12 +4,14 @@ import 'package:equatable/equatable.dart'; /// /// Including threads/replies in today and yesterday, and member count. final class ForumStatus extends Equatable { + /// Constructor. const ForumStatus({ required this.todayCount, required this.yesterdayCount, required this.threadCount, }); + /// Construct an empty forum status. const ForumStatus.empty() : todayCount = '0', yesterdayCount = '0', diff --git a/lib/features/homepage/models/logged_user_info.dart b/lib/features/homepage/models/logged_user_info.dart index 654a30b7..5d339287 100644 --- a/lib/features/homepage/models/logged_user_info.dart +++ b/lib/features/homepage/models/logged_user_info.dart @@ -6,6 +6,7 @@ import 'package:equatable/equatable.dart'; /// * The logged user may have an avatar url or not, depending on the theme of /// website. final class LoggedUserInfo extends Equatable { + /// Constructor. const LoggedUserInfo({ required this.username, required this.relatedLinkPairList, diff --git a/lib/features/homepage/models/pinned_thread.dart b/lib/features/homepage/models/pinned_thread.dart index cead7192..ce4ec0f4 100644 --- a/lib/features/homepage/models/pinned_thread.dart +++ b/lib/features/homepage/models/pinned_thread.dart @@ -4,6 +4,7 @@ import 'package:equatable/equatable.dart'; /// /// Contains thread info and the author's info. final class PinnedThread extends Equatable { + /// Constructor. const PinnedThread({ required this.threadUrl, required this.threadTitle, diff --git a/lib/features/homepage/models/pinned_thread_group.dart b/lib/features/homepage/models/pinned_thread_group.dart index 655c184a..d9dc9b5c 100644 --- a/lib/features/homepage/models/pinned_thread_group.dart +++ b/lib/features/homepage/models/pinned_thread_group.dart @@ -3,6 +3,7 @@ import 'package:tsdm_client/features/homepage/models/pinned_thread.dart'; /// A list of recommended thread with grouped name in the website homepage. final class PinnedThreadGroup extends Equatable { + /// Constructor. const PinnedThreadGroup({required this.title, required this.threadList}); /// Title of this thread group. diff --git a/lib/features/homepage/models/swiper_url.dart b/lib/features/homepage/models/swiper_url.dart index 3f930aa3..079edc49 100644 --- a/lib/features/homepage/models/swiper_url.dart +++ b/lib/features/homepage/models/swiper_url.dart @@ -2,6 +2,7 @@ import 'package:equatable/equatable.dart'; /// A pair of url used in swiper in homepage. final class SwiperUrl extends Equatable { + /// Constructor. const SwiperUrl({required this.coverUrl, required this.linkUrl}); /// Url of the Cover image. diff --git a/lib/features/homepage/repository/homepage_repository.dart b/lib/features/homepage/repository/homepage_repository.dart deleted file mode 100644 index e756e3b9..00000000 --- a/lib/features/homepage/repository/homepage_repository.dart +++ /dev/null @@ -1,3 +0,0 @@ -class HomepageRepository { - // final authen -} diff --git a/lib/features/homepage/view/homepage_page.dart b/lib/features/homepage/view/homepage_page.dart index 6b9c149c..9ae34f3d 100644 --- a/lib/features/homepage/view/homepage_page.dart +++ b/lib/features/homepage/view/homepage_page.dart @@ -20,6 +20,7 @@ import 'package:tsdm_client/utils/retry_button.dart'; /// /// This page is in the Homepage of the app, already wrapped in a [Scaffold]. class HomepagePage extends StatefulWidget { + /// Constructor. const HomepagePage({super.key}); @override @@ -51,7 +52,12 @@ class _HomepagePageState extends State { listener: (context, state) { if (state.status == HomepageStatus.failed) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.t.general.failedToLoad))); + SnackBar( + content: Text( + context.t.general.failedToLoad, + ), + ), + ); } }, child: BlocBuilder( @@ -107,7 +113,7 @@ class _HomepagePageState extends State { onPressed: () async { await context.pushNamed(ScreenPaths.search); }, - ) + ), ], ), body: body, diff --git a/lib/features/homepage/widgets/pin_section.dart b/lib/features/homepage/widgets/pin_section.dart index 0a329a30..2eda7463 100644 --- a/lib/features/homepage/widgets/pin_section.dart +++ b/lib/features/homepage/widgets/pin_section.dart @@ -11,12 +11,16 @@ import 'package:tsdm_client/widgets/single_line_text.dart'; /// /// Threads are separated into different groups. class PinSection extends StatelessWidget { + /// Constructor. const PinSection(this.pinnedThreadGroup, {super.key}); + /// All pinned thread gathered in groups. final List pinnedThreadGroup; Widget _sectionThreadBuilder( - BuildContext context, PinnedThread pinnedThread) { + BuildContext context, + PinnedThread pinnedThread, + ) { return ListTile( title: SingleLineText( pinnedThread.threadTitle, @@ -64,23 +68,25 @@ class PinSection extends StatelessWidget { final sectionName = pinnedThreadGroup[i].title; final threadWidgetList = _buildSectionThreads(context, pinnedThreadGroup[i].threadList); - ret.add(Card( - clipBehavior: Clip.hardEdge, - margin: EdgeInsets.zero, - child: Padding( - padding: edgeInsetsT10, - child: Column( - children: [ - Text( - sectionName, - style: Theme.of(context).textTheme.titleLarge, - ), - sizedBoxW10H10, - threadWidgetList, - ], + ret.add( + Card( + clipBehavior: Clip.hardEdge, + margin: EdgeInsets.zero, + child: Padding( + padding: edgeInsetsT10, + child: Column( + children: [ + Text( + sectionName, + style: Theme.of(context).textTheme.titleLarge, + ), + sizedBoxW10H10, + threadWidgetList, + ], + ), ), ), - )); + ); } return GridView( @@ -89,7 +95,8 @@ class PinSection extends StatelessWidget { // TODO: Not hardcode these Extent sizes. gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 800, - // Set to at least 552 to ensure not overflow when scaling window size down. + // Set to at least 552 to ensure not overflow when scaling window + // size down. mainAxisSpacing: 5, mainAxisExtent: 552, crossAxisSpacing: 5, diff --git a/lib/features/homepage/widgets/welcome_section.dart b/lib/features/homepage/widgets/welcome_section.dart index 15271de0..902d42aa 100644 --- a/lib/features/homepage/widgets/welcome_section.dart +++ b/lib/features/homepage/widgets/welcome_section.dart @@ -16,7 +16,9 @@ import 'package:tsdm_client/widgets/cached_image/cached_image_provider.dart'; import 'package:tsdm_client/widgets/checkin_button/checkin_button.dart'; import 'package:tsdm_client/widgets/single_line_text.dart'; +/// A section of homepage, contains swiper and user info. class WelcomeSection extends StatelessWidget { + /// Constructor. const WelcomeSection({ required this.forumStatus, required this.loggedUserInfo, @@ -24,15 +26,24 @@ class WelcomeSection extends StatelessWidget { super.key, }); + /// Forum status. final ForumStatus forumStatus; + + /// Current logged user info. + /// + /// Null if no one logged. final LoggedUserInfo? loggedUserInfo; + + /// All urls used in swiper. final List swiperUrlList; static const double _kahrpbaPicWidth = 300; static const double _kahrpbaPicHeight = 218; Widget _buildKahrpbaSwiper( - BuildContext context, List swiperUrlList) { + BuildContext context, + List swiperUrlList, + ) { return Card( margin: EdgeInsets.zero, clipBehavior: Clip.antiAlias, diff --git a/lib/features/jump_page/cubit/jump_page_cubit.dart b/lib/features/jump_page/cubit/jump_page_cubit.dart index 0802d01e..76ab4bd3 100644 --- a/lib/features/jump_page/cubit/jump_page_cubit.dart +++ b/lib/features/jump_page/cubit/jump_page_cubit.dart @@ -5,12 +5,16 @@ part 'jump_page_state.dart'; /// Cubit of jump page feature. /// -/// This cubit is used inside other pages that can "jump to another page" logically. +/// This cubit is used inside other pages that can "jump to another page" +/// logically. /// -/// Provides the logic that need to handle in other widgets when jump page actions triggered.. +/// Provides the logic that need to handle in other widgets when jump page +/// actions triggered.. class JumpPageCubit extends Cubit { + /// Constructor. JumpPageCubit() : super(const JumpPageState()); + /// Jump to another [page]. void jumpTo(int page) => emit( state.copyWith( status: JumpPageStatus.success, @@ -18,12 +22,15 @@ class JumpPageCubit extends Cubit { ), ); + /// Mark the current page is loading, disable jump page. void markLoading() => emit(state.copyWith(status: JumpPageStatus.loading, canJumpPage: false)); + /// Mark the current page finished loading, enable jump page. void markSuccess() => emit(state.copyWith(status: JumpPageStatus.success, canJumpPage: true)); + /// Set current page status and total page status. void setPageInfo({int? currentPage, int? totalPages}) => emit(state.copyWith(currentPage: currentPage, totalPages: totalPages)); } diff --git a/lib/features/jump_page/cubit/jump_page_state.dart b/lib/features/jump_page/cubit/jump_page_state.dart index 5a970c42..60ad9a15 100644 --- a/lib/features/jump_page/cubit/jump_page_state.dart +++ b/lib/features/jump_page/cubit/jump_page_state.dart @@ -1,12 +1,20 @@ part of 'jump_page_cubit.dart'; +/// status of jumping page. enum JumpPageStatus { + /// Initial. initial, + + /// Loading the new page. loading, + + /// Load succeed. success, } +/// State of jumping page. final class JumpPageState extends Equatable { + /// Constructor. const JumpPageState({ this.status = JumpPageStatus.initial, this.currentPage = 1, @@ -14,6 +22,7 @@ final class JumpPageState extends Equatable { this.canJumpPage = true, }); + /// Status of jumping. final JumpPageStatus status; /// Current page number @@ -21,8 +30,11 @@ final class JumpPageState extends Equatable { /// Total page number. final int totalPages; + + /// Flag indicates can jump page or not. final bool canJumpPage; + /// Copy with. JumpPageState copyWith({ JumpPageStatus? status, int? currentPage, diff --git a/lib/features/jump_page/widgets/jump_page_dialog.dart b/lib/features/jump_page/widgets/jump_page_dialog.dart index bf689271..4753ffac 100644 --- a/lib/features/jump_page/widgets/jump_page_dialog.dart +++ b/lib/features/jump_page/widgets/jump_page_dialog.dart @@ -3,7 +3,9 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:tsdm_client/generated/i18n/strings.g.dart'; +/// A dialog to ask jump page info from user before jump page. class JumpPageDialog extends StatelessWidget { + /// Constructor. const JumpPageDialog({ required this.current, required this.max, @@ -14,8 +16,13 @@ class JumpPageDialog extends StatelessWidget { assert(min <= current, 'current should larger than min'), assert(current <= max, 'current should no more than max'); + /// Current page number. final int current; + + /// Minimum page number. final int min; + + /// Maximum page number. final int max; @override @@ -29,24 +36,28 @@ class JumpPageDialog extends StatelessWidget { ).toList(); return AlertDialog( title: Text(context.t.jumpDialog.title), - // FIXME: Here should handle better when both large mount or small mount of choices. - // Issue is that Column will junk is choices are too many and ListView fills all height even there are only few choices. + // FIXME: Here should handle better when both large mount or small mount + // of choices. + // Issue is that Column will junk is choices are too many and ListView + // fills all height even there are only few choices. content: choicesList.length <= 30 ? SingleChildScrollView( child: Column( children: choicesList - .map((e) => RadioListTile( - title: Text('$e'), - value: e, - groupValue: v, - onChanged: (value) { - if (value == null) { - return; - } - v = value; - Navigator.pop(context, v); - }, - )) + .map( + (e) => RadioListTile( + title: Text('$e'), + value: e, + groupValue: v, + onChanged: (value) { + if (value == null) { + return; + } + v = value; + Navigator.pop(context, v); + }, + ), + ) .toList(), ), ) diff --git a/lib/features/latest_thread/bloc/latest_thread_bloc.dart b/lib/features/latest_thread/bloc/latest_thread_bloc.dart index 63a88dd2..3b99beca 100644 --- a/lib/features/latest_thread/bloc/latest_thread_bloc.dart +++ b/lib/features/latest_thread/bloc/latest_thread_bloc.dart @@ -11,10 +11,13 @@ import 'package:universal_html/html.dart' as uh; part 'latest_thread_event.dart'; part 'latest_thread_state.dart'; +/// Emitter typedef LatestThreadEmitter = Emitter; +/// Bloc the the latest thread feature. final class LatestThreadBloc extends Bloc { + /// Constructor. LatestThreadBloc({required LatestThreadRepository latestThreadRepository}) : _latestThreadRepository = latestThreadRepository, super(const LatestThreadState()) { @@ -37,12 +40,14 @@ final class LatestThreadBloc final document = await _latestThreadRepository.fetchDocument(state.nextPageUrl!); final (threadList, nextPageUrl) = _parseThreadList(document); - emit(state.copyWith( - status: LatestThreadStatus.success, - threadList: [...state.threadList, ...?threadList], - pageNumber: state.pageNumber + 1, - nextPageUrl: nextPageUrl, - )); + emit( + state.copyWith( + status: LatestThreadStatus.success, + threadList: [...state.threadList, ...?threadList], + pageNumber: state.pageNumber + 1, + nextPageUrl: nextPageUrl, + ), + ); } on HttpRequestFailedException catch (e) { debug('failed to load latest thread next page: $e'); emit(state.copyWith(status: LatestThreadStatus.failed)); @@ -57,12 +62,14 @@ final class LatestThreadBloc try { final document = await _latestThreadRepository.fetchDocument(event.url); final (threadList, nextPageUrl) = _parseThreadList(document); - emit(state.copyWith( - status: LatestThreadStatus.success, - threadList: threadList, - pageNumber: 1, - nextPageUrl: nextPageUrl, - )); + emit( + state.copyWith( + status: LatestThreadStatus.success, + threadList: threadList, + pageNumber: 1, + nextPageUrl: nextPageUrl, + ), + ); } on HttpRequestFailedException catch (e) { debug('failed to load latest thread page: $e'); emit(state.copyWith(status: LatestThreadStatus.failed)); @@ -70,7 +77,8 @@ final class LatestThreadBloc } (List?, String? nextPageUrl) _parseThreadList( - uh.Document document) { + uh.Document document, + ) { final data = document .querySelector('div#threadlist > ul') ?.querySelectorAll('li') diff --git a/lib/features/latest_thread/bloc/latest_thread_event.dart b/lib/features/latest_thread/bloc/latest_thread_event.dart index ab1d93d6..9e2c34d7 100644 --- a/lib/features/latest_thread/bloc/latest_thread_event.dart +++ b/lib/features/latest_thread/bloc/latest_thread_event.dart @@ -1,5 +1,6 @@ part of 'latest_thread_bloc.dart'; +/// Events of latest thread feature. sealed class LatestThreadEvent extends Equatable { const LatestThreadEvent(); @@ -7,9 +8,14 @@ sealed class LatestThreadEvent extends Equatable { List get props => []; } +/// No more page to load. final class LatestThreadLoadMoreRequested extends LatestThreadEvent {} +/// User request to refresh. final class LatestThreadRefreshRequested extends LatestThreadEvent { + /// Constructor. const LatestThreadRefreshRequested(this.url) : super(); + + /// Url to load page. final String url; } diff --git a/lib/features/latest_thread/bloc/latest_thread_state.dart b/lib/features/latest_thread/bloc/latest_thread_state.dart index 0f4363db..5997e7e9 100644 --- a/lib/features/latest_thread/bloc/latest_thread_state.dart +++ b/lib/features/latest_thread/bloc/latest_thread_state.dart @@ -1,13 +1,23 @@ part of 'latest_thread_bloc.dart'; +/// Status of the latest thread feature. enum LatestThreadStatus { + /// Initial. initial, + + /// Loading. loading, + + /// Success. success, + + /// Failed to load. failed, } +/// State of the latest thread feature. final class LatestThreadState extends Equatable { + /// Constructor. const LatestThreadState({ this.status = LatestThreadStatus.initial, this.threadList = const [], @@ -15,11 +25,19 @@ final class LatestThreadState extends Equatable { this.nextPageUrl, }); + /// Status. final LatestThreadStatus status; + + /// All thread to display. final List threadList; + + /// Current page number. final int pageNumber; + + /// Url to fetch the next page. final String? nextPageUrl; + /// Copy with LatestThreadState copyWith({ LatestThreadStatus? status, List? threadList, diff --git a/lib/features/latest_thread/models/latest_thread.dart b/lib/features/latest_thread/models/latest_thread.dart index 5fe22ec8..b7e83aac 100644 --- a/lib/features/latest_thread/models/latest_thread.dart +++ b/lib/features/latest_thread/models/latest_thread.dart @@ -1,11 +1,9 @@ -import 'package:flutter/foundation.dart'; import 'package:tsdm_client/extensions/string.dart'; import 'package:tsdm_client/extensions/universal_html.dart'; import 'package:tsdm_client/shared/models/user.dart'; import 'package:tsdm_client/utils/debug.dart'; import 'package:universal_html/html.dart' as uh; -@immutable class _LatestThreadInfo { const _LatestThreadInfo({ required this.title, @@ -60,31 +58,43 @@ class _LatestThreadInfo { final String? quotedMessage; } +/// Latest thread model. class LatestThread { + /// Build from
  • node. LatestThread.fromLi(uh.Element element) : _info = _buildFromLiNode(element); final _LatestThreadInfo? _info; static final _re = RegExp(r'(?\d+)'); + /// Thread title. String? get title => _info?.title; + /// Thread url. String? get url => _info?.url; + /// Thread id. String? get threadID => _info?.threadID; + /// Forum name the thread belongs to. String? get forumName => _info?.forumName; + /// Forum url the thread belongs to. String? get forumUrl => _info?.forumUrl; + /// The user info of latest replied user. User? get latestReplyAuthor => _info?.latestReplyAuthor; + /// Time of latest reply. DateTime? get latestReplyTime => _info?.latestReplyTime; + /// Total replies count. int? get replyCount => _info?.replyCount; + /// View times count. int? get viewCount => _info?.viewCount; + /// Quoted message to show. String? get quotedMessage => _info?.quotedMessage; ///
    @@ -179,6 +189,7 @@ failed to parse LatestThread node: { ); } + /// Is valid or not. bool isValid() { return _info != null; } diff --git a/lib/features/latest_thread/repository/latest_thread_repository.dart b/lib/features/latest_thread/repository/latest_thread_repository.dart index 1c41ff9e..f4479d98 100644 --- a/lib/features/latest_thread/repository/latest_thread_repository.dart +++ b/lib/features/latest_thread/repository/latest_thread_repository.dart @@ -7,6 +7,7 @@ import 'package:tsdm_client/shared/providers/server_time_provider/sevrer_time_pr import 'package:universal_html/html.dart' as uh; import 'package:universal_html/parsing.dart'; +/// Repository of the latest thread feature. class LatestThreadRepository { /// Fetch html document from [url]. /// diff --git a/lib/features/latest_thread/view/latest_thread_page.dart b/lib/features/latest_thread/view/latest_thread_page.dart index b8bfcb40..72c6e563 100644 --- a/lib/features/latest_thread/view/latest_thread_page.dart +++ b/lib/features/latest_thread/view/latest_thread_page.dart @@ -8,9 +8,12 @@ import 'package:tsdm_client/generated/i18n/strings.g.dart'; import 'package:tsdm_client/utils/retry_button.dart'; import 'package:tsdm_client/widgets/card/thread_card.dart'; +/// Page to show info about latest thread page. class LatestThreadPage extends StatefulWidget { + /// Constructor. const LatestThreadPage({required this.url, super.key}); + /// Url the the page. final String url; @override @@ -58,7 +61,9 @@ class _LatestThreadPageState extends State { void initState() { super.initState(); _refreshController = EasyRefreshController( - controlFinishLoad: true, controlFinishRefresh: true); + controlFinishLoad: true, + controlFinishRefresh: true, + ); } @override @@ -76,15 +81,18 @@ class _LatestThreadPageState extends State { ), BlocProvider( create: (context) => LatestThreadBloc( - latestThreadRepository: RepositoryProvider.of(context)) - ..add(LatestThreadRefreshRequested(widget.url)), - ) + latestThreadRepository: RepositoryProvider.of(context), + )..add(LatestThreadRefreshRequested(widget.url)), + ), ], child: BlocListener( listener: (context, state) { if (state.status == LatestThreadStatus.failed) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.t.general.failedToLoad))); + SnackBar( + content: Text(context.t.general.failedToLoad), + ), + ); } }, child: BlocBuilder( diff --git a/lib/widgets/list_app_bar.dart b/lib/widgets/list_app_bar.dart index d4545df5..3fefcee6 100644 --- a/lib/widgets/list_app_bar.dart +++ b/lib/widgets/list_app_bar.dart @@ -13,7 +13,7 @@ enum MenuActions { backToTop, } -class ListAppBar extends StatelessWidget implements PreferredSizeWidget { +class ListAppBar extends StatelessWidget implements PreferredSizeWidget { const ListAppBar({ required this.onSearch, this.title,