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:
@@ -24,5 +54,6 @@ Aplicativo para o [TabNews](https://www.tabnews.com.br) feito em Flutter.
-
-
+
+## 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),
- );
- }
-}