diff --git a/README.md b/README.md index f42c5dd..d323813 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,52 @@ # TabNews App -Aplicativo para o [TabNews](https://www.tabnews.com.br) feito em Flutter. +Aplicativo TabNews feito com muito ♥️ e Flutter, pela comunidade para o site [TabNews](https://www.tabnews.com.br). -### Features: +## Instalar e rodar o projeto -- [x] Tema escuro +### Dependências globais + +Você precisa ter o Flutter instalado e configurado na sua máquina: + +- [Flutter](https://docs.flutter.dev/get-started/install) 3.0 (ou qualquer versão **3** superior) + +### Dependências locais + +Então após baixar/clonar o repositório, não se esqueça de instalar as dependências locais do projeto: + +```bash +flutter pub get +``` + +### Rodar o projeto + +Para rodar o projeto localmente, basta rodar o comando abaixo: + +```bash +flutter run +``` + +Isto irá rodar o projeto no seu emulador/simulador ou dispositivo real conectado. + +**Lembrando que as vezes é necessário abrir o emulador/simulador antes de rodar o comando de run.** + + +## Features: + +- [x] Dark mode - [x] Leitura de conteúdos - [x] Pull To Refresh - [x] Infite Scroll -- [ ] Comentários das publicações (Em progresso) -- [ ] Login do usuário (Em progresso) +- [ ] Visualização de Comentários das publicações (Em progresso) +- [x] Login do usuário +- [x] Meus conteúdos - [ ] Resposta dos conteúdos - [ ] Postagens de conteúdos - [ ] Visualização do perfil - [ ] Favoritos (local database) - [ ] Opção ler mais taerde (local database) -### Imagens: +## Imagens: Home Home - Dark @@ -24,5 +54,6 @@ Aplicativo para o [TabNews](https://www.tabnews.com.br) feito em Flutter. Leitura Leitura - Dark -
-
+ +## Contribuidores + diff --git a/lib/main.dart b/lib/main.dart index 19965d8..c830bd8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,14 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:tabnews/src/app.dart'; -import 'package:tabnews/src/login_state.dart'; +import 'package:tabnews/src/preferences.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - final state = LoginState(await SharedPreferences.getInstance()); - state.checkLoggedIn(); + await Preferences.init(); - runApp(App(loginState: state)); + runApp(const App()); } diff --git a/lib/src/app.dart b/lib/src/app.dart index ca72bab..6b010f7 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -1,13 +1,12 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:tabnews/src/login_state.dart'; -import 'package:tabnews/src/routes.dart'; +import 'package:tabnews/src/providers/user.dart'; +import 'package:tabnews/src/ui/layouts/tab.dart'; +import 'package:tabnews/src/ui/pages/login.dart'; class App extends StatelessWidget { - final LoginState loginState; - - const App({super.key, required this.loginState}); + const App({super.key}); @override Widget build(BuildContext context) { @@ -15,28 +14,20 @@ class App extends StatelessWidget { providers: [ ChangeNotifierProvider( lazy: false, - create: (_) => loginState, - ), - Provider( - lazy: false, - create: (context) => Routes(loginState), + create: (_) => UserProvider(), ), ], - child: Builder( - builder: (context) { - final router = Provider.of(context, listen: false).router; - - return MaterialApp.router( - routeInformationParser: router.routeInformationParser, - routerDelegate: router.routerDelegate, - title: 'TabNews', - debugShowCheckedModeBanner: false, - darkTheme: ThemeData.dark(), - theme: ThemeData( - primaryColor: Colors.black, - ), - ); - }, + child: MaterialApp( + title: 'TabNews', + debugShowCheckedModeBanner: false, + darkTheme: ThemeData.dark(), + theme: ThemeData( + primaryColor: Colors.black, + ), + home: Consumer( + builder: (context, user, _) => + user.loggedIn ? const TabLayout() : const LoginPage(), + ), ), ); } diff --git a/lib/src/constants.dart b/lib/src/constants.dart index 2cb279f..9fc6627 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -1,18 +1,11 @@ import 'package:flutter/material.dart'; -const String rootRouteName = 'root'; -const String createAccountRouteName = 'create-account'; -const String detailsRouteName = 'details'; -const String homeRouteName = 'home'; -const String loggedInKey = 'LoggedIn'; -const String loginRouteName = 'login'; -const String moreInfoRouteName = 'moreInfo'; -const String personalRouteName = 'personal'; -const String profileMoreInfoRouteName = 'profile-moreInfo'; -const String profilePersonalRouteName = 'profile-personal'; -const String profileRouteName = 'profile'; -const String profileSigninInfoRouteName = 'profile-signin'; -const String postRouteName = 'post-view'; -const String signinInfoRouteName = 'signin'; +abstract class AppConstants { + static const String loggedInKey = 'LoggedIn'; + static const String authKey = 'AuthLogged'; + static const String userKey = 'UserLogged'; +} -const Color primaryColor = Color.fromRGBO(36, 41, 47, 1); +abstract class AppColors { + static const Color primaryColor = Color.fromRGBO(36, 41, 47, 1); +} diff --git a/lib/src/login_state.dart b/lib/src/login_state.dart deleted file mode 100644 index 1359ca9..0000000 --- a/lib/src/login_state.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter/material.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:tabnews/src/models/auth.dart'; - -import 'constants.dart'; - -class LoginState extends ChangeNotifier { - final SharedPreferences prefs; - bool _loggedIn = false; - Auth _session = Auth.fromJson({}); - - LoginState(this.prefs) { - loggedIn = prefs.getBool(loggedInKey) ?? false; - session = Auth.fromJson( - jsonDecode(prefs.getString('${loggedInKey}_auth') ?? '{}'), - ); - } - - bool get loggedIn => _loggedIn; - set loggedIn(bool value) { - _loggedIn = value; - prefs.setBool(loggedInKey, value); - notifyListeners(); - } - - Auth get session => _session; - set session(Auth auth) { - prefs.setString('${loggedInKey}_auth', jsonEncode(auth.toJson())); - notifyListeners(); - } - - void checkLoggedIn() { - loggedIn = prefs.getBool(loggedInKey) ?? false; - session = Auth.fromJson( - jsonDecode(prefs.getString('${loggedInKey}_auth') ?? '{}'), - ); - } -} diff --git a/lib/src/models/user.dart b/lib/src/models/user.dart new file mode 100644 index 0000000..1e80afc --- /dev/null +++ b/lib/src/models/user.dart @@ -0,0 +1,49 @@ +class User { + String? id; + String? username; + String? email; + bool? notifications; + List? features; + int? tabcoins; + int? tabcash; + String? createdAt; + String? updatedAt; + + User({ + this.id, + this.username, + this.email, + this.notifications, + this.features, + this.tabcoins, + this.tabcash, + this.createdAt, + this.updatedAt, + }); + + User.fromJson(Map json) { + id = json['id']; + username = json['username']; + email = json['email']; + notifications = json['notifications']; + features = json['features'].cast(); + tabcoins = json['tabcoins']; + tabcash = json['tabcash']; + createdAt = json['created_at']; + updatedAt = json['updated_at']; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['username'] = username; + data['email'] = email; + data['notifications'] = notifications; + data['features'] = features; + data['tabcoins'] = tabcoins; + data['tabcash'] = tabcash; + data['created_at'] = createdAt; + data['updated_at'] = updatedAt; + return data; + } +} diff --git a/lib/src/pages/error.dart b/lib/src/pages/error.dart deleted file mode 100644 index 6310189..0000000 --- a/lib/src/pages/error.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter/material.dart'; - -//ignore: must_be_immutable -class ErrorPage extends StatelessWidget { - final Exception? error; - late String message; - - ErrorPage({Key? key, this.error}) : super(key: key) { - if (error != null) { - message = error.toString(); - } else { - message = 'Error'; - } - } - - @override - Widget build(BuildContext context) { - return Scaffold(body: Center(child: Text(message))); - } -} diff --git a/lib/src/preferences.dart b/lib/src/preferences.dart new file mode 100644 index 0000000..cc70704 --- /dev/null +++ b/lib/src/preferences.dart @@ -0,0 +1,17 @@ +import 'dart:async' show Future; +import 'package:shared_preferences/shared_preferences.dart'; + +class Preferences { + static late final SharedPreferences _instance; + + static Future init() async => + _instance = await SharedPreferences.getInstance(); + + static bool? getBool(String key) => _instance.getBool(key); + static Future setBool(String key, bool value) => + _instance.setBool(key, value); + + static String? getString(String key) => _instance.getString(key); + static Future setString(String key, String value) => + _instance.setString(key, value); +} diff --git a/lib/src/providers/user.dart b/lib/src/providers/user.dart new file mode 100644 index 0000000..db7a70a --- /dev/null +++ b/lib/src/providers/user.dart @@ -0,0 +1,88 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; + +import 'package:tabnews/src/constants.dart'; +import 'package:tabnews/src/models/auth.dart'; +import 'package:tabnews/src/models/user.dart'; +import 'package:tabnews/src/preferences.dart'; +import 'package:tabnews/src/services/auth.dart'; + +class UserProvider extends ChangeNotifier { + static final UserProvider _instance = UserProvider._internal(); + + final api = ApiAuth(); + final String _loggedKey = AppConstants.loggedInKey; + final String _authKey = AppConstants.authKey; + final String _userKey = AppConstants.userKey; + + factory UserProvider() { + return _instance; + } + + UserProvider._internal() { + _loggedIn = Preferences.getBool(_loggedKey) ?? false; + + if (_loggedIn) { + _sessionId = _getSessionId(); + _user = _getUser(); + } + } + + late bool _loggedIn; + bool get loggedIn => _loggedIn; + + late String _sessionId; + String get sessionId => _sessionId; + + late User _user; + User get user => _user; + + void login(String email, String password) async { + var auth = await api.postLogin(email, password); + + if (auth.id!.isNotEmpty) { + var user = await api.fetchUser(auth.token!); + + Preferences.setString(_authKey, jsonEncode(auth.toJson())); + Preferences.setString(_userKey, jsonEncode(user.toJson())); + Preferences.setBool(_loggedKey, true); + _loggedIn = true; + } + + notifyListeners(); + } + + void logout() { + Preferences.setString(_authKey, ''); + Preferences.setString(_userKey, ''); + Preferences.setBool(_loggedKey, false); + _loggedIn = false; + + notifyListeners(); + } + + String _getSessionId() { + String pref = Preferences.getString(_authKey) ?? ''; + + if (pref.isNotEmpty) { + Auth auth = Auth.fromJson(jsonDecode(pref)); + + return auth.token!; + } + + return ''; + } + + User _getUser() { + String pref = Preferences.getString(_userKey) ?? ''; + + if (pref.isNotEmpty) { + User user = User.fromJson(jsonDecode(pref)); + + return user; + } + + return User.fromJson({}); + } +} diff --git a/lib/src/routes.dart b/lib/src/routes.dart deleted file mode 100644 index 63e17b3..0000000 --- a/lib/src/routes.dart +++ /dev/null @@ -1,80 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:tabnews/src/constants.dart'; -import 'package:tabnews/src/login_state.dart'; -import 'package:tabnews/src/pages/error.dart'; -import 'package:tabnews/src/pages/home.dart'; - -import 'package:tabnews/src/pages/login.dart'; -import 'package:tabnews/src/pages/post.dart'; - -class Routes { - final LoginState loginState; - - Routes(this.loginState); - - late final router = GoRouter( - refreshListenable: loginState, - debugLogDiagnostics: true, - routes: [ - GoRoute( - name: rootRouteName, - path: '/', - redirect: (context, state) => state.namedLocation(homeRouteName), - ), - GoRoute( - name: loginRouteName, - path: '/login', - pageBuilder: (context, state) => MaterialPage( - key: state.pageKey, - child: const LoginPage(), - ), - ), - GoRoute( - name: createAccountRouteName, - path: '/register', - pageBuilder: (context, state) => MaterialPage( - key: state.pageKey, - child: const LoginPage(), - ), - ), - GoRoute( - name: homeRouteName, - path: '/home', - pageBuilder: (context, state) => MaterialPage( - key: state.pageKey, - child: const HomePage(), - ), - ), - GoRoute( - name: postRouteName, - path: '/post/:username/:slug', - pageBuilder: (context, state) => MaterialPage( - key: state.pageKey, - child: PostPage( - username: state.params['username']!, - slug: state.params['slug']!, - ), - ), - ), - ], - errorPageBuilder: (context, state) => MaterialPage( - key: state.pageKey, - child: ErrorPage(error: state.error), - ), - redirect: (context, state) { - final loginLoc = state.namedLocation(loginRouteName); - final loggingIn = state.subloc == loginLoc; - - final createAccountLoc = state.namedLocation(createAccountRouteName); - final creatingAccount = state.subloc == createAccountLoc; - - final loggedIn = loginState.loggedIn; - final rootLoc = state.namedLocation(rootRouteName); - - if (!loggedIn && !loggingIn && !creatingAccount) return loginLoc; - if (loggedIn && (loggingIn || creatingAccount)) return rootLoc; - return null; - }, - ); -} diff --git a/lib/src/services/api.dart b/lib/src/services/api.dart index fa486ee..bf0f39d 100644 --- a/lib/src/services/api.dart +++ b/lib/src/services/api.dart @@ -70,4 +70,26 @@ class Api { throw Exception('Failed to load singular content children'); } } + + Future> fetchMyContents({ + int page = 1, + required String user, + }) async { + final response = await http.get( + Uri.parse('$apiUrl/$user?strategy=new&page=$page'), + ); + + if (response.statusCode == 200) { + var dataJson = jsonDecode(response.body); + List contents = []; + + dataJson.forEach((item) { + contents.add(Content.fromJson(item)); + }); + + return contents; + } else { + throw Exception('Failed to load my contents'); + } + } } diff --git a/lib/src/services/auth.dart b/lib/src/services/auth.dart index 3741977..6164d88 100644 --- a/lib/src/services/auth.dart +++ b/lib/src/services/auth.dart @@ -1,7 +1,8 @@ import 'dart:convert'; - import 'package:http/http.dart' as http; + import 'package:tabnews/src/models/auth.dart'; +import 'package:tabnews/src/models/user.dart'; class ApiAuth { final apiUrl = 'https://www.tabnews.com.br/api/v1'; @@ -24,4 +25,21 @@ class ApiAuth { throw Exception('Failed to load singular content children'); } } + + Future fetchUser(String token) async { + final response = await http.get( + Uri.parse('$apiUrl/user'), + headers: { + 'Set-Cookie': 'session_id=$token', + 'Cookie': 'session_id=$token', + 'Content-Type': 'application/json; charset=UTF-8', + }, + ); + + if (response.statusCode == 200) { + return User.fromJson(jsonDecode(response.body)); + } else { + throw Exception('Failed to load singular content'); + } + } } diff --git a/lib/src/ui/layouts/page.dart b/lib/src/ui/layouts/page.dart new file mode 100644 index 0000000..fafd1c2 --- /dev/null +++ b/lib/src/ui/layouts/page.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +import 'package:tabnews/src/extensions/dark_mode.dart'; +import 'package:tabnews/src/ui/widgets/progress_indicator.dart'; +import 'package:tabnews/src/ui/widgets/top_bar.dart'; + +class PageLayout extends StatelessWidget { + final Widget body; + final bool isLoading; + + const PageLayout({ + super.key, + required this.body, + this.isLoading = false, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: const AppTopBar(), + body: RefreshIndicator( + color: context.isDarkMode ? Colors.white : Colors.black, + onRefresh: () async {}, + child: isLoading ? const AppProgressIndicator() : body, + ), + ); + } +} diff --git a/lib/src/ui/layouts/tab.dart b/lib/src/ui/layouts/tab.dart new file mode 100644 index 0000000..7e43ec1 --- /dev/null +++ b/lib/src/ui/layouts/tab.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; + +import 'package:tabnews/src/extensions/dark_mode.dart'; +import 'package:tabnews/src/ui/pages/favorites.dart'; +import 'package:tabnews/src/ui/pages/home.dart'; +import 'package:tabnews/src/ui/pages/profile.dart'; +import 'package:tabnews/src/ui/pages/recents.dart'; +import 'package:tabnews/src/ui/widgets/bottom_bar.dart'; +import 'package:tabnews/src/ui/widgets/top_bar.dart'; + +class TabLayout extends StatefulWidget { + const TabLayout({super.key}); + + @override + State createState() => _TabLayoutState(); +} + +class _TabLayoutState extends State { + int _currentPage = 0; + static const List _pages = [ + HomePage(), + RecentsPage(), + FavoritesPage(), + ProfilePage(), + ]; + + void _onItemTapped(int index) { + setState(() { + _currentPage = index; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + extendBody: true, + appBar: const AppTopBar(), + body: RefreshIndicator( + color: context.isDarkMode ? Colors.white : Colors.black, + onRefresh: () async {}, + child: IndexedStack( + index: _currentPage, + children: _pages, + ), + ), + bottomNavigationBar: AppBottomBar( + onTap: _onItemTapped, + currentPage: _currentPage, + ), + ); + } +} diff --git a/lib/src/pages/post.dart b/lib/src/ui/pages/content.dart similarity index 75% rename from lib/src/pages/post.dart rename to lib/src/ui/pages/content.dart index 54ab73b..d7245aa 100644 --- a/lib/src/pages/post.dart +++ b/lib/src/ui/pages/content.dart @@ -1,29 +1,29 @@ import 'package:flutter/material.dart'; -import 'package:tabnews/src/widgets/separator.dart'; import 'package:timeago/timeago.dart' as timeago; -import 'package:tabnews/src/widgets/comments.dart'; import 'package:tabnews/src/extensions/dark_mode.dart'; import 'package:tabnews/src/models/content.dart'; import 'package:tabnews/src/services/api.dart'; -import 'package:tabnews/src/widgets/layout.dart'; -import 'package:tabnews/src/widgets/markdown.dart'; +import 'package:tabnews/src/ui/widgets/markdown.dart'; +import 'package:tabnews/src/ui/widgets/comments.dart'; +import 'package:tabnews/src/ui/layouts/page.dart'; +import 'package:tabnews/src/ui/widgets/progress_indicator.dart'; -class PostPage extends StatefulWidget { +class ContentPage extends StatefulWidget { final String username; final String slug; - const PostPage({ + const ContentPage({ super.key, required this.username, required this.slug, }); @override - State createState() => _PostPageState(); + State createState() => _ContentPageState(); } -class _PostPageState extends State { +class _ContentPageState extends State { Content content = Content.fromJson({}); final api = Api(); final ScrollController _controller = ScrollController(); @@ -37,8 +37,9 @@ class _PostPageState extends State { } Future _getContent() async { - var contentResp = - await api.fetchContent('${widget.username}/${widget.slug}'); + var contentResp = await api.fetchContent( + '${widget.username}/${widget.slug}', + ); setState(() { content = contentResp; @@ -50,11 +51,9 @@ class _PostPageState extends State { Widget build(BuildContext context) { timeago.setLocaleMessages('pt-BR', timeago.PtBrMessages()); - return LayoutApp( - pullToRefresh: _getContent, - isLoading: isLoading, + return PageLayout( body: isLoading - ? const SizedBox() + ? const AppProgressIndicator() : SingleChildScrollView( controller: _controller, padding: const EdgeInsets.all(10.0), @@ -71,7 +70,9 @@ class _PostPageState extends State { ), const SizedBox(height: 10.0), Text( - '${content.title}', + content.parentId != null + ? 'Em resposta a...' + : '${content.title}', style: const TextStyle().copyWith( fontSize: 18.0, fontWeight: FontWeight.w700, @@ -82,7 +83,7 @@ class _PostPageState extends State { controller: _controller, ), const SizedBox(height: 30.0), - const AppSeparator(), + const Divider(), const SizedBox(height: 30.0), CommentsWidget( slug: '${widget.username}/${widget.slug}', diff --git a/lib/src/ui/pages/favorites.dart b/lib/src/ui/pages/favorites.dart new file mode 100644 index 0000000..a78e2bc --- /dev/null +++ b/lib/src/ui/pages/favorites.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class FavoritesPage extends StatefulWidget { + const FavoritesPage({super.key}); + + @override + State createState() => _FavoritesPageState(); +} + +class _FavoritesPageState extends State { + @override + Widget build(BuildContext context) { + return const Center( + child: Text('Favoritos ainda em construção :3'), + ); + } +} diff --git a/lib/src/ui/pages/home.dart b/lib/src/ui/pages/home.dart new file mode 100644 index 0000000..fdb1ed7 --- /dev/null +++ b/lib/src/ui/pages/home.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; + +import 'package:tabnews/src/models/content.dart'; +import 'package:tabnews/src/services/api.dart'; +import 'package:tabnews/src/ui/widgets/item_content.dart'; +import 'package:tabnews/src/ui/widgets/progress_indicator.dart'; + +class HomePage extends StatefulWidget { + const HomePage({super.key}); + + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State { + late List contents; + final api = Api(); + bool isLoading = true; + static const _perPage = 30; + + final PagingController _pagingController = + PagingController(firstPageKey: 1); + + @override + void initState() { + super.initState(); + + _pagingController.addPageRequestListener((pageKey) { + _getContents(pageKey); + }); + } + + @override + void dispose() { + _pagingController.dispose(); + + super.dispose(); + } + + Future _getContents(int page) async { + final content = await api.fetchContents(page: page); + + final isLastPage = content.length < _perPage; + + if (isLastPage) { + _pagingController.appendLastPage(content); + } else { + final nextPage = page + 1; + + _pagingController.appendPage(content, nextPage); + } + } + + @override + Widget build(BuildContext context) { + /*pullToRefresh: () => Future.sync( + () => _pagingController.refresh(), + )*/ + return PagedListView( + padding: const EdgeInsets.all(10.0), + pagingController: _pagingController, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) { + return ItemContent(index: index, content: item); + }, + firstPageProgressIndicatorBuilder: (_) => const AppProgressIndicator(), + newPageProgressIndicatorBuilder: (_) => const AppProgressIndicator(), + ), + ); + } +} diff --git a/lib/src/pages/login.dart b/lib/src/ui/pages/login.dart similarity index 72% rename from lib/src/pages/login.dart rename to lib/src/ui/pages/login.dart index a71e941..bb94149 100644 --- a/lib/src/pages/login.dart +++ b/lib/src/ui/pages/login.dart @@ -1,11 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; + import 'package:tabnews/src/constants.dart'; -import 'package:tabnews/src/login_state.dart'; -import 'package:tabnews/src/models/auth.dart'; -import 'package:tabnews/src/services/auth.dart'; +import 'package:tabnews/src/providers/user.dart'; +import 'package:tabnews/src/ui/widgets/top_bar.dart'; class LoginPage extends StatefulWidget { const LoginPage({super.key}); @@ -18,40 +16,11 @@ class _LoginPageState extends State { TextEditingController emailTextController = TextEditingController(); TextEditingController passwordTextController = TextEditingController(); final _formKey = GlobalKey(); - final api = ApiAuth(); - - void saveLoginState(BuildContext context) async { - var auth = await api.postLogin( - emailTextController.text, - passwordTextController.text, - ); - - if (auth.id!.isNotEmpty) { - Provider.of(context, listen: false).loggedIn = true; - Provider.of(context, listen: false).session = auth; - - GoRouter.of(context).replaceNamed(loginRouteName); - } - } @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - centerTitle: false, - title: Row( - children: [ - SvgPicture.asset( - 'lib/assets/logo.svg', - semanticsLabel: 'TabNews', - ), - const SizedBox(width: 10.0), - const Text('TabNews'), - ], - ), - elevation: 0, - backgroundColor: primaryColor, - ), + appBar: const AppTopBar(), body: Center( child: Form( key: _formKey, @@ -95,7 +64,7 @@ class _LoginPageState extends State { style: ButtonStyle( elevation: MaterialStateProperty.all(0.0), backgroundColor: MaterialStateProperty.all( - primaryColor, + AppColors.primaryColor, ), foregroundColor: MaterialStateProperty.all( Colors.white, @@ -107,7 +76,10 @@ class _LoginPageState extends State { ), ), onPressed: () { - saveLoginState(context); + Provider.of(context, listen: false).login( + emailTextController.text, + passwordTextController.text, + ); }, child: Text( 'Login', diff --git a/lib/src/pages/home.dart b/lib/src/ui/pages/my_contents.dart similarity index 64% rename from lib/src/pages/home.dart rename to lib/src/ui/pages/my_contents.dart index 488fd22..9b04c23 100644 --- a/lib/src/pages/home.dart +++ b/lib/src/ui/pages/my_contents.dart @@ -1,22 +1,21 @@ import 'package:flutter/material.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:tabnews/src/login_state.dart'; import 'package:tabnews/src/models/content.dart'; +import 'package:tabnews/src/providers/user.dart'; import 'package:tabnews/src/services/api.dart'; -import 'package:tabnews/src/widgets/item_card.dart'; -import 'package:tabnews/src/widgets/layout.dart'; -import 'package:tabnews/src/widgets/progress_indicator.dart'; +import 'package:tabnews/src/ui/layouts/page.dart'; +import 'package:tabnews/src/ui/widgets/item_content.dart'; +import 'package:tabnews/src/ui/widgets/progress_indicator.dart'; -class HomePage extends StatefulWidget { - const HomePage({super.key}); +class MyContentsPage extends StatefulWidget { + const MyContentsPage({super.key}); @override - State createState() => _HomePageState(); + State createState() => _MyContentsPageState(); } -class _HomePageState extends State { +class _MyContentsPageState extends State { late List contents; final api = Api(); bool isLoading = true; @@ -29,17 +28,11 @@ class _HomePageState extends State { void initState() { super.initState(); - test(); - _pagingController.addPageRequestListener((pageKey) { _getContents(pageKey); }); } - test() async { - print(LoginState(await SharedPreferences.getInstance()).session.id); - } - @override void dispose() { _pagingController.dispose(); @@ -48,7 +41,10 @@ class _HomePageState extends State { } Future _getContents(int page) async { - final content = await api.fetchContents(page: page); + final content = await api.fetchMyContents( + page: page, + user: UserProvider().user.username!, + ); final isLastPage = content.length < _perPage; @@ -63,23 +59,22 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { - return LayoutApp( - pullToRefresh: () => Future.sync( - () => _pagingController.refresh(), - ), + return PageLayout( body: PagedListView( padding: const EdgeInsets.all(10.0), pagingController: _pagingController, builderDelegate: PagedChildBuilderDelegate( itemBuilder: (context, item, index) { - return ItemCard(index: index, content: item); + return ItemContent( + index: index, + content: item, + ); }, firstPageProgressIndicatorBuilder: (_) => const AppProgressIndicator(), newPageProgressIndicatorBuilder: (_) => const AppProgressIndicator(), ), ), - isLoading: false, ); } } diff --git a/lib/src/ui/pages/profile.dart b/lib/src/ui/pages/profile.dart new file mode 100644 index 0000000..cccae7b --- /dev/null +++ b/lib/src/ui/pages/profile.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'package:tabnews/src/providers/user.dart'; +import 'package:tabnews/src/ui/pages/my_contents.dart'; +import 'package:tabnews/src/utils/navigation.dart'; + +class ProfilePage extends StatefulWidget { + const ProfilePage({super.key}); + + @override + State createState() => _ProfilePageState(); +} + +class _ProfilePageState extends State { + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric( + vertical: 10.0, + horizontal: 10.0, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '${UserProvider().user.username}', + style: const TextStyle().copyWith( + fontSize: 28.0, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 80.0), + ListTile( + onTap: () => Navigation.push(context, const MyContentsPage()), + title: const Text('Meu conteúdo'), + ), + const Divider(color: Colors.grey), + const ListTile( + onTap: null, + title: Text('Publicar novo conteúdo'), + ), + const ListTile( + onTap: null, + title: Text('Editar perfil'), + ), + const Divider(color: Colors.grey), + ListTile( + onTap: () => + Provider.of(context, listen: false).logout(), + title: Text( + 'Deslogar', + style: const TextStyle().copyWith( + color: Colors.red, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/src/ui/pages/recents.dart b/lib/src/ui/pages/recents.dart new file mode 100644 index 0000000..9328c8d --- /dev/null +++ b/lib/src/ui/pages/recents.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; + +import 'package:tabnews/src/models/content.dart'; +import 'package:tabnews/src/services/api.dart'; +import 'package:tabnews/src/ui/widgets/item_content.dart'; +import 'package:tabnews/src/ui/widgets/progress_indicator.dart'; + +class RecentsPage extends StatefulWidget { + const RecentsPage({super.key}); + + @override + State createState() => _RecentsPageState(); +} + +class _RecentsPageState extends State { + late List contents; + final api = Api(); + bool isLoading = true; + static const _perPage = 30; + + final PagingController _pagingController = + PagingController(firstPageKey: 1); + + @override + void initState() { + super.initState(); + + _pagingController.addPageRequestListener((pageKey) { + _getContents(pageKey); + }); + } + + @override + void dispose() { + _pagingController.dispose(); + + super.dispose(); + } + + Future _getContents(int page) async { + final content = await api.fetchContentsNew(page: page); + + final isLastPage = content.length < _perPage; + + if (isLastPage) { + _pagingController.appendLastPage(content); + } else { + final nextPage = page + 1; + + _pagingController.appendPage(content, nextPage); + } + } + + @override + Widget build(BuildContext context) { + /*pullToRefresh: () => Future.sync( + () => _pagingController.refresh(), + )*/ + return PagedListView( + padding: const EdgeInsets.all(10.0), + pagingController: _pagingController, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) { + return ItemContent(index: index, content: item); + }, + firstPageProgressIndicatorBuilder: (_) => const AppProgressIndicator(), + newPageProgressIndicatorBuilder: (_) => const AppProgressIndicator(), + ), + ); + } +} diff --git a/lib/src/ui/widgets/bottom_bar.dart b/lib/src/ui/widgets/bottom_bar.dart new file mode 100644 index 0000000..1610c3c --- /dev/null +++ b/lib/src/ui/widgets/bottom_bar.dart @@ -0,0 +1,52 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:tabnews/src/constants.dart'; + +class AppBottomBar extends StatelessWidget { + final int currentPage; + final void Function(int) onTap; + + const AppBottomBar({ + super.key, + required this.onTap, + required this.currentPage, + }); + + @override + Widget build(BuildContext context) { + return ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), + child: BottomNavigationBar( + currentIndex: currentPage, + selectedItemColor: Colors.white, + unselectedItemColor: Colors.grey.shade400, + type: BottomNavigationBarType.fixed, + backgroundColor: AppColors.primaryColor, + showSelectedLabels: false, + showUnselectedLabels: false, + onTap: onTap, + items: const [ + BottomNavigationBarItem( + icon: Icon(Icons.home), + label: 'Home', + ), + BottomNavigationBarItem( + icon: Icon(Icons.feed), + label: 'Recentes', + ), + BottomNavigationBarItem( + icon: Icon(Icons.favorite), + label: 'Favoritos', + ), + BottomNavigationBarItem( + icon: Icon(Icons.account_circle), + label: 'Perfil', + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/widgets/comments.dart b/lib/src/ui/widgets/comments.dart similarity index 87% rename from lib/src/widgets/comments.dart rename to lib/src/ui/widgets/comments.dart index 5a59adb..07a4298 100644 --- a/lib/src/widgets/comments.dart +++ b/lib/src/ui/widgets/comments.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:tabnews/src/widgets/item_comment.dart'; -import 'package:tabnews/src/widgets/separator.dart'; import 'package:timeago/timeago.dart' as timeago; import 'package:tabnews/src/models/comment.dart'; import 'package:tabnews/src/services/api.dart'; +import 'package:tabnews/src/ui/widgets/item_comment.dart'; class CommentsWidget extends StatefulWidget { final String slug; @@ -47,7 +46,7 @@ class _CommentsWidgetState extends State { shrinkWrap: true, controller: widget.controller, itemCount: comments.length, - separatorBuilder: (context, index) => const AppSeparator(), + separatorBuilder: (context, index) => const Divider(), itemBuilder: (context, index) => ItemComment( comment: comments[index], controller: widget.controller, diff --git a/lib/src/widgets/item_comment.dart b/lib/src/ui/widgets/item_comment.dart similarity index 95% rename from lib/src/widgets/item_comment.dart rename to lib/src/ui/widgets/item_comment.dart index 75eff13..7b745eb 100644 --- a/lib/src/widgets/item_comment.dart +++ b/lib/src/ui/widgets/item_comment.dart @@ -3,7 +3,7 @@ import 'package:timeago/timeago.dart' as timeago; import 'package:tabnews/src/extensions/dark_mode.dart'; import 'package:tabnews/src/models/comment.dart'; -import 'package:tabnews/src/widgets/markdown.dart'; +import 'package:tabnews/src/ui/widgets/markdown.dart'; class ItemComment extends StatelessWidget { final Comment comment; diff --git a/lib/src/widgets/item_card.dart b/lib/src/ui/widgets/item_content.dart similarity index 80% rename from lib/src/widgets/item_card.dart rename to lib/src/ui/widgets/item_content.dart index 9a4ad4c..ed954cb 100644 --- a/lib/src/widgets/item_card.dart +++ b/lib/src/ui/widgets/item_content.dart @@ -1,15 +1,16 @@ import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; import 'package:timeago/timeago.dart' as timeago; +import 'package:tabnews/src/ui/pages/content.dart'; +import 'package:tabnews/src/utils/navigation.dart'; import 'package:tabnews/src/extensions/dark_mode.dart'; import 'package:tabnews/src/models/content.dart'; -class ItemCard extends StatelessWidget { +class ItemContent extends StatelessWidget { final int index; final Content content; - const ItemCard({ + const ItemContent({ super.key, required this.index, required this.content, @@ -19,11 +20,20 @@ class ItemCard extends StatelessWidget { Widget build(BuildContext context) { timeago.setLocaleMessages('pt-BR', timeago.PtBrMessages()); var date = DateTime.parse(content.publishedAt!); + bool isComment = false; - var slug = '${content.ownerUsername}/${content.slug}'; + if (content.parentId != null) { + isComment = true; + } return InkWell( - onTap: () => GoRouter.of(context).push('/post/$slug'), + onTap: () => Navigation.push( + context, + ContentPage( + username: content.ownerUsername!, + slug: content.slug!, + ), + ), child: Container( margin: const EdgeInsets.only(bottom: 15.0), padding: const EdgeInsets.all(15.0), @@ -47,7 +57,7 @@ class ItemCard extends StatelessWidget { const SizedBox(width: 10.0), Flexible( child: Text( - '${content.title}', + isComment ? '${content.body}' : '${content.title}', style: const TextStyle().copyWith( fontSize: 15.0, fontWeight: FontWeight.w700, diff --git a/lib/src/widgets/markdown.dart b/lib/src/ui/widgets/markdown.dart similarity index 100% rename from lib/src/widgets/markdown.dart rename to lib/src/ui/widgets/markdown.dart diff --git a/lib/src/widgets/progress_indicator.dart b/lib/src/ui/widgets/progress_indicator.dart similarity index 100% rename from lib/src/widgets/progress_indicator.dart rename to lib/src/ui/widgets/progress_indicator.dart diff --git a/lib/src/ui/widgets/top_bar.dart b/lib/src/ui/widgets/top_bar.dart new file mode 100644 index 0000000..62f4bfe --- /dev/null +++ b/lib/src/ui/widgets/top_bar.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +import 'package:tabnews/src/constants.dart'; + +class AppTopBar extends StatelessWidget with PreferredSizeWidget { + const AppTopBar({super.key}); + + @override + Widget build(BuildContext context) { + return AppBar( + centerTitle: false, + title: Row( + children: [ + SvgPicture.asset( + 'lib/assets/logo.svg', + semanticsLabel: 'TabNews', + ), + const SizedBox(width: 10.0), + const Text('TabNews'), + ], + ), + elevation: 0, + backgroundColor: AppColors.primaryColor, + ); + } + + @override + Size get preferredSize => AppBar().preferredSize; +} diff --git a/lib/src/utils/navigation.dart b/lib/src/utils/navigation.dart new file mode 100644 index 0000000..d1e333c --- /dev/null +++ b/lib/src/utils/navigation.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; + +abstract class Navigation { + static push( + BuildContext context, + Widget page, + ) { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => page), + ); + } + + static pushModal( + BuildContext context, + Widget page, + ) { + Navigator.push( + context, + MaterialPageRoute( + fullscreenDialog: true, + builder: (_) => page, + ), + ); + } + + static pushReplacement( + BuildContext context, + Widget page, + ) { + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => page), + ); + } + + static pushReplacementModal( + BuildContext context, + Widget page, + ) { + Navigator.pushReplacement( + context, + MaterialPageRoute( + fullscreenDialog: true, + builder: (_) => page, + ), + ); + } + + static pushAndRemoveUntil( + BuildContext context, + Widget page, + Function predicate, + ) { + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (_) => page), + (route) => false, + ); + } + + static pushAndRemoveUntilModal( + BuildContext context, + Widget page, + Function predicate, + ) { + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + fullscreenDialog: true, + builder: (_) => page, + ), + (route) => false, + ); + } + + static pop(BuildContext context) { + Navigator.pop(context); + } + + static popUntil(BuildContext context) { + Navigator.popUntil( + context, + (route) => route.isFirst, + ); + } +} diff --git a/lib/src/widgets/layout.dart b/lib/src/widgets/layout.dart deleted file mode 100644 index c228e95..0000000 --- a/lib/src/widgets/layout.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; - -import 'package:tabnews/src/extensions/dark_mode.dart'; - -class LayoutApp extends StatelessWidget { - final Future Function() pullToRefresh; - final Widget body; - final bool isLoading; - - const LayoutApp({ - super.key, - required this.body, - required this.pullToRefresh, - required this.isLoading, - }); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - centerTitle: false, - title: Row( - children: [ - SvgPicture.asset( - 'lib/assets/logo.svg', - semanticsLabel: 'TabNews', - ), - const SizedBox(width: 10.0), - const Text('TabNews'), - ], - ), - elevation: 0, - backgroundColor: const Color.fromRGBO(36, 41, 47, 1), - ), - body: RefreshIndicator( - color: context.isDarkMode ? Colors.white : Colors.black, - onRefresh: pullToRefresh, - child: isLoading - ? Center( - child: CircularProgressIndicator( - color: context.isDarkMode ? Colors.white : Colors.black, - ), - ) - : body, - ), - ); - } -} diff --git a/lib/src/widgets/separator.dart b/lib/src/widgets/separator.dart deleted file mode 100644 index aa2fd59..0000000 --- a/lib/src/widgets/separator.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:tabnews/src/extensions/dark_mode.dart'; - -class AppSeparator extends StatelessWidget { - const AppSeparator({super.key}); - - @override - Widget build(BuildContext context) { - return Container( - color: context.isDarkMode ? Colors.grey.shade700 : Colors.grey.shade200, - height: 3.0, - margin: const EdgeInsets.all(15.0), - ); - } -}