From c81e7a7931bd0d1588bfb5386f2b0b24fa0705dd Mon Sep 17 00:00:00 2001 From: Friyn Date: Mon, 11 Aug 2025 09:16:45 +0700 Subject: [PATCH 01/13] v2 first commit --- devtools_options.yaml | 3 ++ lib/main.dart | 64 ++++++++++++++++++++----------------------- lib/page/login.dart | 22 +++++++++++++++ lib/page/user.dart | 2 ++ pubspec.yaml | 2 ++ 5 files changed, 58 insertions(+), 35 deletions(-) create mode 100644 devtools_options.yaml create mode 100644 lib/page/login.dart create mode 100644 lib/page/user.dart diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/lib/main.dart b/lib/main.dart index ae5bcab..8eb54a4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,8 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'dart:convert'; +// import 'package:tlist/page/login.dart'; + void main() { runApp(const MyApp()); } @@ -283,13 +285,24 @@ class _MainScreenState extends State { final TextEditingController _searchController = TextEditingController(); String _searchQuery = ''; - final List _titles = ['To-Do List', 'My Notes', 'Keuangan']; + final List _titles = ['Tasks', 'Notes', 'Keuangan']; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(_titles[_currentIndex]), + actions: [ + IconButton( + icon: const Icon(Icons.person), + onPressed: () { + // Navigator.push( + // context, + // MaterialPageRoute(builder: (context) => const LoginPage()), + // ); + }, + ), + ], bottom: PreferredSize( preferredSize: const Size.fromHeight(60), child: Padding( @@ -338,7 +351,7 @@ class _MainScreenState extends State { bottomNavigationBar: BottomNavigationBar( currentIndex: _currentIndex, type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.blue, + selectedItemColor: const Color(0xFF128C7E), unselectedItemColor: Colors.grey, onTap: (index) { setState(() { @@ -351,7 +364,7 @@ class _MainScreenState extends State { BottomNavigationBarItem( icon: Icon(Icons.check_circle_outline), activeIcon: Icon(Icons.check_circle), - label: 'To-Do', + label: 'Tasks', ), BottomNavigationBarItem( icon: Icon(Icons.note_outlined), @@ -869,38 +882,19 @@ class _AddEditTransactionDialogState extends State { mainAxisSize: MainAxisSize.min, children: [ if (widget.transaction == null) - Row( - children: [ - Expanded( - child: RadioListTile( - dense: true, - title: const Text('Pemasukan'), - value: 'income', - groupValue: _transactionType, - onChanged: (value) { - setState(() { - _transactionType = value!; - _selectedCategory = (value == 'income' ? widget.incomeCategories : widget.expenseCategories).first; - }); - }, - ), - ), - Expanded( - child: RadioListTile( - dense: true, - title: const Text('Pengeluaran'), - value: 'expense', - groupValue: _transactionType, - onChanged: (value) { - setState(() { - _transactionType = value!; - _selectedCategory = (value == 'income' ? widget.incomeCategories : widget.expenseCategories).first; - }); - }, - ), - ), - ], - ), + DropdownButtonFormField( + value: _transactionType, + items: const [ + DropdownMenuItem(child: Text('Pemasukan'), value: 'income'), + DropdownMenuItem(child: Text('Pengeluaran'), value: 'expense'), + ], + onChanged: (value) { + setState(() { + _transactionType = value!; + _selectedCategory = (value == 'income' ? widget.incomeCategories : widget.expenseCategories).first; + }); + }, + ), const SizedBox(height: 16), TextField( controller: _titleController, diff --git a/lib/page/login.dart b/lib/page/login.dart new file mode 100644 index 0000000..a782f5b --- /dev/null +++ b/lib/page/login.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +class LoginPage extends StatefulWidget { + const LoginPage({super.key}); + + @override + State createState() => _LoginPageState(); +} + +class _LoginPageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Login'), + ), + body: const Center( + child: Text('Login Page'), + ), + ); + } +} \ No newline at end of file diff --git a/lib/page/user.dart b/lib/page/user.dart new file mode 100644 index 0000000..196afaa --- /dev/null +++ b/lib/page/user.dart @@ -0,0 +1,2 @@ +import 'package:flutter/material.dart'; + diff --git a/pubspec.yaml b/pubspec.yaml index 72568d1..e0b3009 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,6 +29,8 @@ dev_dependencies: flutter: uses-material-design: true + assets: + - assets/ flutter_icons: android: true From a2a95ef124211c4ecbaf748734a6636bec36c1fb Mon Sep 17 00:00:00 2001 From: Friyn Date: Mon, 11 Aug 2025 11:39:29 +0700 Subject: [PATCH 02/13] Add login, register and user page --- .firebaserc | 5 + .github/workflows/firebase-hosting-merge.yml | 20 ++ .../firebase-hosting-pull-request.yml | 21 +++ .gitignore | 4 + lib/main.dart | 10 +- lib/page/login.dart | 120 +++++++++++- lib/page/register.dart | 172 ++++++++++++++++++ lib/page/user.dart | 63 +++++++ 8 files changed, 408 insertions(+), 7 deletions(-) create mode 100644 .firebaserc create mode 100644 .github/workflows/firebase-hosting-merge.yml create mode 100644 .github/workflows/firebase-hosting-pull-request.yml create mode 100644 lib/page/register.dart diff --git a/.firebaserc b/.firebaserc new file mode 100644 index 0000000..996aa9f --- /dev/null +++ b/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "tlistserver" + } +} diff --git a/.github/workflows/firebase-hosting-merge.yml b/.github/workflows/firebase-hosting-merge.yml new file mode 100644 index 0000000..26fc5b9 --- /dev/null +++ b/.github/workflows/firebase-hosting-merge.yml @@ -0,0 +1,20 @@ +# This file was auto-generated by the Firebase CLI +# https://github.com/firebase/firebase-tools + +name: Deploy to Firebase Hosting on merge +on: + push: + branches: + - main +jobs: + build_and_deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: flutter build web + - uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: ${{ secrets.GITHUB_TOKEN }} + firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_TLISTSERVER }} + channelId: live + projectId: tlistserver diff --git a/.github/workflows/firebase-hosting-pull-request.yml b/.github/workflows/firebase-hosting-pull-request.yml new file mode 100644 index 0000000..003088f --- /dev/null +++ b/.github/workflows/firebase-hosting-pull-request.yml @@ -0,0 +1,21 @@ +# This file was auto-generated by the Firebase CLI +# https://github.com/firebase/firebase-tools + +name: Deploy to Firebase Hosting on PR +on: pull_request +permissions: + checks: write + contents: read + pull-requests: write +jobs: + build_and_preview: + if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: flutter build web + - uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: ${{ secrets.GITHUB_TOKEN }} + firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_TLISTSERVER }} + projectId: tlistserver diff --git a/.gitignore b/.gitignore index 918e6cc..2bbfb7f 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,7 @@ app.*.map.json /android/app/profile /android/app/release /android/build/reports + +# Firebase +.Firebase +firebase.json \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 8eb54a4..ef918e9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'dart:convert'; -// import 'package:tlist/page/login.dart'; +import 'package:tlist/page/user.dart'; void main() { runApp(const MyApp()); @@ -296,10 +296,10 @@ class _MainScreenState extends State { IconButton( icon: const Icon(Icons.person), onPressed: () { - // Navigator.push( - // context, - // MaterialPageRoute(builder: (context) => const LoginPage()), - // ); + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const UserPage()), + ); }, ), ], diff --git a/lib/page/login.dart b/lib/page/login.dart index a782f5b..dd67d4c 100644 --- a/lib/page/login.dart +++ b/lib/page/login.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:tlist/page/register.dart'; class LoginPage extends StatefulWidget { const LoginPage({super.key}); @@ -8,14 +9,129 @@ class LoginPage extends StatefulWidget { } class _LoginPageState extends State { + final _formKey = GlobalKey(); + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + bool _obscure = true; + + void _submit() { + if (_formKey.currentState?.validate() ?? false) { + FocusScope.of(context).unfocus(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Login berhasil')), + ); + // TODO: Integrasikan dengan auth/routing bila diperlukan. + } + } + + @override + void dispose() { + _emailController.dispose(); + _passwordController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Login'), ), - body: const Center( - child: Text('Login Page'), + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 24), + Center( + child: Image.asset( + 'assets/logo.png', + height: 80, + ), + ), + const SizedBox(height: 12), + Text( + 'Welcome to TList!', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 24), + TextFormField( + controller: _emailController, + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.next, + decoration: const InputDecoration( + labelText: 'Email', + hintText: 'you@example.com', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.email_outlined), + ), + validator: (value) { + final v = value?.trim() ?? ''; + if (v.isEmpty) return 'Email tidak boleh kosong'; + final emailRegex = RegExp(r'^.+@.+\..+$'); + if (!emailRegex.hasMatch(v)) return 'Format email tidak valid'; + return null; + }, + ), + const SizedBox(height: 16), + TextFormField( + controller: _passwordController, + obscureText: _obscure, + textInputAction: TextInputAction.done, + onFieldSubmitted: (_) => _submit(), + decoration: InputDecoration( + labelText: 'Password', + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.lock_outline), + suffixIcon: IconButton( + onPressed: () => setState(() => _obscure = !_obscure), + icon: Icon(_obscure ? Icons.visibility : Icons.visibility_off), + tooltip: _obscure ? 'Tampilkan' : 'Sembunyikan', + ), + ), + validator: (value) { + final v = value ?? ''; + if (v.isEmpty) return 'Password tidak boleh kosong'; + if (v.length < 6) return 'Minimal 6 karakter'; + return null; + }, + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: _submit, + style: ElevatedButton.styleFrom( + minimumSize: const Size.fromHeight(48), + ), + child: const Text('Login'), + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Belum punya akun?'), + TextButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const RegisterPage(), + ), + ); + }, + child: const Text('Daftar'), + ), + ], + ), + ], + ), + ), + ), ), ); } diff --git a/lib/page/register.dart b/lib/page/register.dart new file mode 100644 index 0000000..e60e63e --- /dev/null +++ b/lib/page/register.dart @@ -0,0 +1,172 @@ +import 'package:flutter/material.dart'; + +class RegisterPage extends StatefulWidget { + const RegisterPage({super.key}); + + @override + State createState() => _RegisterPageState(); +} + +class _RegisterPageState extends State { + final _formKey = GlobalKey(); + final _nameController = TextEditingController(); + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + final _confirmController = TextEditingController(); + + bool _obscurePass = true; + bool _obscureConfirm = true; + + void _submit() { + if (!(_formKey.currentState?.validate() ?? false)) return; + FocusScope.of(context).unfocus(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Registrasi berhasil')), + ); + // TODO: Integrasikan ke backend/auth jika dibutuhkan + Navigator.pop(context); // kembali ke login + } + + @override + void dispose() { + _nameController.dispose(); + _emailController.dispose(); + _passwordController.dispose(); + _confirmController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Daftar')), + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 24), + Center( + child: Image.asset( + 'assets/logo.png', + height: 80, + ), + ), + const SizedBox(height: 12), + Text( + 'Buat Akun TList', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 24), + TextFormField( + controller: _nameController, + textInputAction: TextInputAction.next, + decoration: const InputDecoration( + labelText: 'Nama Lengkap', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.person_outline), + ), + validator: (value) { + final v = (value ?? '').trim(); + if (v.isEmpty) return 'Nama tidak boleh kosong'; + if (v.length < 3) return 'Nama terlalu pendek'; + return null; + }, + ), + const SizedBox(height: 16), + TextFormField( + controller: _emailController, + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.next, + decoration: const InputDecoration( + labelText: 'Email', + hintText: 'you@example.com', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.email_outlined), + ), + validator: (value) { + final v = (value ?? '').trim(); + if (v.isEmpty) return 'Email tidak boleh kosong'; + final emailRegex = RegExp(r'^.+@.+\..+$'); + if (!emailRegex.hasMatch(v)) return 'Format email tidak valid'; + return null; + }, + ), + const SizedBox(height: 16), + TextFormField( + controller: _passwordController, + obscureText: _obscurePass, + textInputAction: TextInputAction.next, + decoration: InputDecoration( + labelText: 'Password', + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.lock_outline), + suffixIcon: IconButton( + onPressed: () => setState(() => _obscurePass = !_obscurePass), + icon: Icon(_obscurePass ? Icons.visibility : Icons.visibility_off), + tooltip: _obscurePass ? 'Tampilkan' : 'Sembunyikan', + ), + ), + validator: (value) { + final v = value ?? ''; + if (v.isEmpty) return 'Password tidak boleh kosong'; + if (v.length < 6) return 'Minimal 6 karakter'; + return null; + }, + ), + const SizedBox(height: 16), + TextFormField( + controller: _confirmController, + obscureText: _obscureConfirm, + textInputAction: TextInputAction.done, + onFieldSubmitted: (_) => _submit(), + decoration: InputDecoration( + labelText: 'Konfirmasi Password', + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.lock_outline), + suffixIcon: IconButton( + onPressed: () => setState(() => _obscureConfirm = !_obscureConfirm), + icon: Icon(_obscureConfirm ? Icons.visibility : Icons.visibility_off), + tooltip: _obscureConfirm ? 'Tampilkan' : 'Sembunyikan', + ), + ), + validator: (value) { + final v = value ?? ''; + if (v.isEmpty) return 'Konfirmasi password tidak boleh kosong'; + if (v != _passwordController.text) return 'Password tidak cocok'; + return null; + }, + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: _submit, + style: ElevatedButton.styleFrom( + minimumSize: const Size.fromHeight(48), + ), + child: const Text('Daftar'), + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Sudah punya akun?'), + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Login'), + ), + ], + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/page/user.dart b/lib/page/user.dart index 196afaa..9794a70 100644 --- a/lib/page/user.dart +++ b/lib/page/user.dart @@ -1,2 +1,65 @@ import 'package:flutter/material.dart'; +import 'package:tlist/page/login.dart'; +import 'package:tlist/page/register.dart'; + +class UserPage extends StatelessWidget { + const UserPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('User')), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: _buildSignedOut(context), + ), + ), + ); + } + + Widget _buildSignedOut(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 24), + Center( + child: Image.asset( + 'assets/logo.png', + height: 80, + ), + ), + const SizedBox(height: 16), + const Text( + 'You are not sign in', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const LoginPage()), + ); + }, + style: ElevatedButton.styleFrom(minimumSize: const Size.fromHeight(48)), + child: const Text('Login'), + ), + const SizedBox(height: 12), + OutlinedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const RegisterPage()), + ); + }, + style: OutlinedButton.styleFrom(minimumSize: const Size.fromHeight(48)), + child: const Text('Daftar'), + ), + ], + ); + } + +} From 4f89d1adcd282da45bc54d31a931d75bcad2d8c6 Mon Sep 17 00:00:00 2001 From: Friyn Date: Mon, 11 Aug 2025 12:46:15 +0700 Subject: [PATCH 03/13] Functioning the thing --- .gitignore | 8 +- android/app/build.gradle.kts | 3 + android/app/google-services.json | 29 ++++ android/settings.gradle.kts | 3 + firebase | 0 lib/firebase_options.dart | 76 +++++++++ lib/main.dart | 146 ++++++++++++++---- lib/page/login.dart | 41 ++++- lib/page/register.dart | 49 +++++- lib/page/user.dart | 44 +++++- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 24 +++ pubspec.yaml | 1 + web/favicon.png | Bin 917 -> 15274 bytes web/faviconn.png | Bin 0 -> 917 bytes web/index.html | 2 +- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 18 files changed, 391 insertions(+), 41 deletions(-) create mode 100644 android/app/google-services.json create mode 100644 firebase create mode 100644 lib/firebase_options.dart create mode 100644 web/faviconn.png diff --git a/.gitignore b/.gitignore index 2bbfb7f..b220634 100644 --- a/.gitignore +++ b/.gitignore @@ -46,5 +46,9 @@ app.*.map.json /android/build/reports # Firebase -.Firebase -firebase.json \ No newline at end of file +.firebaserc +firebase.json +firebase-debug.log +.firebase +.github +firebaseconfig.js \ No newline at end of file diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index aa26736..8a482bf 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -1,5 +1,8 @@ plugins { id("com.android.application") + // START: FlutterFire Configuration + id("com.google.gms.google-services") + // END: FlutterFire Configuration id("kotlin-android") // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id("dev.flutter.flutter-gradle-plugin") diff --git a/android/app/google-services.json b/android/app/google-services.json new file mode 100644 index 0000000..e7ffc53 --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "582262425557", + "project_id": "tlistserver", + "storage_bucket": "tlistserver.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:582262425557:android:81685a50fe61a2ad029f5c", + "android_client_info": { + "package_name": "com.friyn.tlist" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyAWYT98BF7VQr3y_qThu5wae0vG308nF6k" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index ab39a10..bd7522f 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -19,6 +19,9 @@ pluginManagement { plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("com.android.application") version "8.7.3" apply false + // START: FlutterFire Configuration + id("com.google.gms.google-services") version("4.3.15") apply false + // END: FlutterFire Configuration id("org.jetbrains.kotlin.android") version "2.1.0" apply false } diff --git a/firebase b/firebase new file mode 100644 index 0000000..e69de29 diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart new file mode 100644 index 0000000..758f391 --- /dev/null +++ b/lib/firebase_options.dart @@ -0,0 +1,76 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: type=lint +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for ios - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.macOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for macos - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.windows: + return windows; + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions web = FirebaseOptions( + apiKey: 'AIzaSyCE6Rw02MDEKHIe-aqYEoGuk9GYkj2eHG0', + appId: '1:582262425557:web:bb5480d783f97b58029f5c', + messagingSenderId: '582262425557', + projectId: 'tlistserver', + authDomain: 'tlistserver.firebaseapp.com', + storageBucket: 'tlistserver.firebasestorage.app', + measurementId: 'G-GL9ZWVCBD5', + ); + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyAWYT98BF7VQr3y_qThu5wae0vG308nF6k', + appId: '1:582262425557:android:81685a50fe61a2ad029f5c', + messagingSenderId: '582262425557', + projectId: 'tlistserver', + storageBucket: 'tlistserver.firebasestorage.app', + ); + + static const FirebaseOptions windows = FirebaseOptions( + apiKey: 'AIzaSyCE6Rw02MDEKHIe-aqYEoGuk9GYkj2eHG0', + appId: '1:582262425557:web:a2fb9a11a3eb69e6029f5c', + messagingSenderId: '582262425557', + projectId: 'tlistserver', + authDomain: 'tlistserver.firebaseapp.com', + storageBucket: 'tlistserver.firebasestorage.app', + measurementId: 'G-K1HVP6VG7G', + ); +} diff --git a/lib/main.dart b/lib/main.dart index ef918e9..f8ff434 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,10 +2,18 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'dart:convert'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'firebase_options.dart'; import 'package:tlist/page/user.dart'; -void main() { +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); runApp(const MyApp()); } @@ -204,50 +212,132 @@ class DataService { // Tasks (tetap sama) static Future> loadTasks() async { - final prefs = await SharedPreferences.getInstance(); - final String? tasksJson = prefs.getString(_tasksKey); - if (tasksJson == null) return []; - - final List tasksList = json.decode(tasksJson); - return tasksList.map((task) => Task.fromJson(task)).toList(); + final user = FirebaseAuth.instance.currentUser; + if (user != null) { + final snap = await FirebaseFirestore.instance + .collection('users') + .doc(user.uid) + .collection('tasks') + .get(); + return snap.docs.map((d) => Task.fromJson(d.data())).toList(); + } else { + final prefs = await SharedPreferences.getInstance(); + final String? tasksJson = prefs.getString(_tasksKey); + if (tasksJson == null) return []; + final List tasksList = json.decode(tasksJson); + return tasksList.map((task) => Task.fromJson(task)).toList(); + } } static Future saveTasks(List tasks) async { - final prefs = await SharedPreferences.getInstance(); - final String tasksJson = json.encode(tasks.map((task) => task.toJson()).toList()); - await prefs.setString(_tasksKey, tasksJson); + final user = FirebaseAuth.instance.currentUser; + if (user != null) { + final col = FirebaseFirestore.instance + .collection('users') + .doc(user.uid) + .collection('tasks'); + final batch = FirebaseFirestore.instance.batch(); + // clear existing by fetching and deleting, then re-add + final existing = await col.get(); + for (final doc in existing.docs) { + batch.delete(doc.reference); + } + for (final t in tasks) { + final ref = col.doc(t.id); + batch.set(ref, t.toJson()); + } + await batch.commit(); + } else { + final prefs = await SharedPreferences.getInstance(); + final String tasksJson = json.encode(tasks.map((task) => task.toJson()).toList()); + await prefs.setString(_tasksKey, tasksJson); + } } // Notes (tetap sama) static Future> loadNotes() async { - final prefs = await SharedPreferences.getInstance(); - final String? notesJson = prefs.getString(_notesKey); - if (notesJson == null) return []; - - final List notesList = json.decode(notesJson); - return notesList.map((note) => Note.fromJson(note)).toList(); + final user = FirebaseAuth.instance.currentUser; + if (user != null) { + final snap = await FirebaseFirestore.instance + .collection('users') + .doc(user.uid) + .collection('notes') + .get(); + return snap.docs.map((d) => Note.fromJson(d.data())).toList(); + } else { + final prefs = await SharedPreferences.getInstance(); + final String? notesJson = prefs.getString(_notesKey); + if (notesJson == null) return []; + final List notesList = json.decode(notesJson); + return notesList.map((note) => Note.fromJson(note)).toList(); + } } static Future saveNotes(List notes) async { - final prefs = await SharedPreferences.getInstance(); - final String notesJson = json.encode(notes.map((note) => note.toJson()).toList()); - await prefs.setString(_notesKey, notesJson); + final user = FirebaseAuth.instance.currentUser; + if (user != null) { + final col = FirebaseFirestore.instance + .collection('users') + .doc(user.uid) + .collection('notes'); + final batch = FirebaseFirestore.instance.batch(); + final existing = await col.get(); + for (final doc in existing.docs) { + batch.delete(doc.reference); + } + for (final n in notes) { + final ref = col.doc(n.id); + batch.set(ref, n.toJson()); + } + await batch.commit(); + } else { + final prefs = await SharedPreferences.getInstance(); + final String notesJson = json.encode(notes.map((note) => note.toJson()).toList()); + await prefs.setString(_notesKey, notesJson); + } } // Transactions (BARU) static Future> loadTransactions() async { - final prefs = await SharedPreferences.getInstance(); - final String? transactionsJson = prefs.getString(_transactionsKey); - if (transactionsJson == null) return []; - - final List transactionsList = json.decode(transactionsJson); - return transactionsList.map((transaction) => Transaction.fromJson(transaction)).toList(); + final user = FirebaseAuth.instance.currentUser; + if (user != null) { + final snap = await FirebaseFirestore.instance + .collection('users') + .doc(user.uid) + .collection('transactions') + .get(); + return snap.docs.map((d) => Transaction.fromJson(d.data())).toList(); + } else { + final prefs = await SharedPreferences.getInstance(); + final String? transactionsJson = prefs.getString(_transactionsKey); + if (transactionsJson == null) return []; + final List transactionsList = json.decode(transactionsJson); + return transactionsList.map((transaction) => Transaction.fromJson(transaction)).toList(); + } } static Future saveTransactions(List transactions) async { - final prefs = await SharedPreferences.getInstance(); - final String transactionsJson = json.encode(transactions.map((transaction) => transaction.toJson()).toList()); - await prefs.setString(_transactionsKey, transactionsJson); + final user = FirebaseAuth.instance.currentUser; + if (user != null) { + final col = FirebaseFirestore.instance + .collection('users') + .doc(user.uid) + .collection('transactions'); + final batch = FirebaseFirestore.instance.batch(); + final existing = await col.get(); + for (final doc in existing.docs) { + batch.delete(doc.reference); + } + for (final tr in transactions) { + final ref = col.doc(tr.id); + batch.set(ref, tr.toJson()); + } + await batch.commit(); + } else { + final prefs = await SharedPreferences.getInstance(); + final String transactionsJson = json.encode(transactions.map((transaction) => transaction.toJson()).toList()); + await prefs.setString(_transactionsKey, transactionsJson); + } } // Categories diff --git a/lib/page/login.dart b/lib/page/login.dart index dd67d4c..92e90e8 100644 --- a/lib/page/login.dart +++ b/lib/page/login.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:tlist/page/register.dart'; +import 'package:firebase_auth/firebase_auth.dart'; class LoginPage extends StatefulWidget { const LoginPage({super.key}); @@ -14,13 +15,45 @@ class _LoginPageState extends State { final _passwordController = TextEditingController(); bool _obscure = true; - void _submit() { - if (_formKey.currentState?.validate() ?? false) { - FocusScope.of(context).unfocus(); + Future _submit() async { + if (!(_formKey.currentState?.validate() ?? false)) return; + FocusScope.of(context).unfocus(); + final email = _emailController.text.trim(); + final pass = _passwordController.text; + try { + await FirebaseAuth.instance.signInWithEmailAndPassword( + email: email, + password: pass, + ); + if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Login berhasil')), ); - // TODO: Integrasikan dengan auth/routing bila diperlukan. + Navigator.pop(context); + } on FirebaseAuthException catch (e) { + final msg = _humanizeAuthError(e.code); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(msg)), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Terjadi kesalahan: $e')), + ); + } + } + + String _humanizeAuthError(String code) { + switch (code) { + case 'invalid-email': + return 'Email tidak valid'; + case 'user-disabled': + return 'Akun dinonaktifkan'; + case 'user-not-found': + return 'Pengguna tidak ditemukan'; + case 'wrong-password': + return 'Password salah'; + default: + return 'Login gagal ($code)'; } } diff --git a/lib/page/register.dart b/lib/page/register.dart index e60e63e..fad5378 100644 --- a/lib/page/register.dart +++ b/lib/page/register.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:firebase_auth/firebase_auth.dart'; class RegisterPage extends StatefulWidget { const RegisterPage({super.key}); @@ -20,11 +21,49 @@ class _RegisterPageState extends State { void _submit() { if (!(_formKey.currentState?.validate() ?? false)) return; FocusScope.of(context).unfocus(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Registrasi berhasil')), - ); - // TODO: Integrasikan ke backend/auth jika dibutuhkan - Navigator.pop(context); // kembali ke login + _register(); + } + + Future _register() async { + final name = _nameController.text.trim(); + final email = _emailController.text.trim(); + final pass = _passwordController.text; + try { + final cred = await FirebaseAuth.instance.createUserWithEmailAndPassword( + email: email, + password: pass, + ); + await cred.user?.updateDisplayName(name); + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Registrasi berhasil')), + ); + Navigator.pop(context); + } on FirebaseAuthException catch (e) { + final msg = _humanizeAuthError(e.code); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(msg)), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Terjadi kesalahan: $e')), + ); + } + } + + String _humanizeAuthError(String code) { + switch (code) { + case 'email-already-in-use': + return 'Email sudah terdaftar'; + case 'invalid-email': + return 'Email tidak valid'; + case 'operation-not-allowed': + return 'Operasi tidak diizinkan'; + case 'weak-password': + return 'Password terlalu lemah'; + default: + return 'Registrasi gagal ($code)'; + } } @override diff --git a/lib/page/user.dart b/lib/page/user.dart index 9794a70..1e752c6 100644 --- a/lib/page/user.dart +++ b/lib/page/user.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:tlist/page/login.dart'; import 'package:tlist/page/register.dart'; +import 'package:firebase_auth/firebase_auth.dart'; class UserPage extends StatelessWidget { const UserPage({super.key}); @@ -12,7 +13,19 @@ class UserPage extends StatelessWidget { body: SafeArea( child: Padding( padding: const EdgeInsets.all(16.0), - child: _buildSignedOut(context), + child: StreamBuilder( + stream: FirebaseAuth.instance.authStateChanges(), + builder: (context, snapshot) { + final user = snapshot.data; + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + if (user == null) { + return _buildSignedOut(context); + } + return _buildSignedIn(context, user); + }, + ), ), ), ); @@ -61,5 +74,34 @@ class UserPage extends StatelessWidget { ); } + Widget _buildSignedIn(BuildContext context, User user) { + final displayName = user.displayName ?? 'Pengguna'; + final email = user.email ?? '-'; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + ListTile( + leading: const CircleAvatar(child: Icon(Icons.person)), + title: Text(displayName), + subtitle: Text(email), + ), + const Divider(), + ListTile( + leading: const Icon(Icons.logout), + title: const Text('Keluar'), + onTap: () async { + await FirebaseAuth.instance.signOut(); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Signed out')), + ); + } + }, + ), + ], + ); + } + } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 1fb4f92..3ba4b5e 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,11 +5,13 @@ import FlutterMacOS import Foundation +import cloud_firestore import firebase_auth import firebase_core import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 30d245f..087fedd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -89,6 +89,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + cloud_firestore: + dependency: "direct main" + description: + name: cloud_firestore + sha256: "78d22987093d89e875102753cba0335f13b1255086925941dacb96cd0b57866e" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + cloud_firestore_platform_interface: + dependency: transitive + description: + name: cloud_firestore_platform_interface + sha256: a9ce2edd81c3578d22a35933af2f56742e628a09dcb923750d2525d3152a823d + url: "https://pub.dev" + source: hosted + version: "7.0.0" + cloud_firestore_web: + dependency: transitive + description: + name: cloud_firestore_web + sha256: b751751ca29eb8ceff95c4f6c2d21bc895de968118e98235f5ffe45f0173ae24 + url: "https://pub.dev" + source: hosted + version: "5.0.0" collection: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e0b3009..7d6dbe3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: shared_preferences: ^2.5.3 firebase_core: ^4.0.0 firebase_auth: ^6.0.0 + cloud_firestore: ^6.0.0 dev_dependencies: flutter_test: diff --git a/web/favicon.png b/web/favicon.png index 8aaa46ac1ae21512746f852a42ba87e4165dfdd1..c3ea8bb0e7ba4f713faa63b98958cfa8d7709ccc 100644 GIT binary patch literal 15274 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 diff --git a/web/faviconn.png b/web/faviconn.png new file mode 100644 index 0000000000000000000000000000000000000000..8aaa46ac1ae21512746f852a42ba87e4165dfdd1 GIT binary patch literal 917 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|I14-?iy0X7 zltGxWVyS%@P(fs7NJL45ua8x7ey(0(N`6wRUPW#JP&EUCO@$SZnVVXYs8ErclUHn2 zVXFjIVFhG^g!Ppaz)DK8ZIvQ?0~DO|i&7O#^-S~(l1AfjnEK zjFOT9D}DX)@^Za$W4-*MbbUihOG|wNBYh(yU7!lx;>x^|#0uTKVr7USFmqf|i<65o z3raHc^AtelCMM;Vme?vOfh>Xph&xL%(-1c06+^uR^q@XSM&D4+Kp$>4P^%3{)XKjo zGZknv$b36P8?Z_gF{nK@`XI}Z90TzwSQO}0J1!f2c(B=V`5aP@1P1a|PZ!4!3&Gl8 zTYqUsf!gYFyJnXpu0!n&N*SYAX-%d(5gVjrHJWqXQshj@!Zm{!01WsQrH~9=kTxW#6SvuapgMqt>$=j#%eyGrQzr zP{L-3gsMA^$I1&gsBAEL+vxi1*Igl=8#8`5?A-T5=z-sk46WA1IUT)AIZHx1rdUrf zVJrJn<74DDw`j)Ki#gt}mIT-Q`XRa2-jQXQoI%w`nb|XblvzK${ZzlV)m-XcwC(od z71_OEC5Bt9GEXosOXaPTYOia#R4ID2TiU~`zVMl08TV_C%DnU4^+HE>9(CE4D6?Fz oujB08i7adh9xk7*FX66dWH6F5TM;?E2b5PlUHx3vIVCg!0Dx9vYXATM literal 0 HcmV?d00001 diff --git a/web/index.html b/web/index.html index 942ca66..e775e31 100644 --- a/web/index.html +++ b/web/index.html @@ -27,7 +27,7 @@ - tlist + TList diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index d141b74..bf6d21a 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,10 +6,13 @@ #include "generated_plugin_registrant.h" +#include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + CloudFirestorePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("CloudFirestorePluginCApi")); FirebaseAuthPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi")); FirebaseCorePluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 29944d5..b83b40a 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + cloud_firestore firebase_auth firebase_core ) From 61d5df3e9f1ad093ff869ddccb031de916571f26 Mon Sep 17 00:00:00 2001 From: Friyn Date: Tue, 12 Aug 2025 00:42:29 +0700 Subject: [PATCH 04/13] Update User section --- .gitignore | 3 +- lib/main.dart | 250 ++++++++++-------- lib/page/login.dart | 91 ++++++- lib/page/register.dart | 10 +- lib/page/user.dart | 177 +++++++++++++ macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 24 ++ pubspec.yaml | 1 + 8 files changed, 446 insertions(+), 112 deletions(-) diff --git a/.gitignore b/.gitignore index b220634..51c0e79 100644 --- a/.gitignore +++ b/.gitignore @@ -51,4 +51,5 @@ firebase.json firebase-debug.log .firebase .github -firebaseconfig.js \ No newline at end of file +firebaseconfig.js +lib/firebase_options.dart \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index f8ff434..e26bf03 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -701,39 +701,50 @@ class _FinanceScreenState extends State { // Transactions List Expanded( - child: filteredTransactions.isEmpty - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, + child: RefreshIndicator( + onRefresh: _loadData, + child: filteredTransactions.isEmpty + ? ListView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(16.0), children: [ - Icon(Icons.receipt_outlined, size: 64, color: Colors.grey[400]), - const SizedBox(height: 16), - Text( - widget.searchQuery.isNotEmpty - ? 'Tidak ada transaksi yang cocok' - : 'Belum ada transaksi', - style: TextStyle(fontSize: 18, color: Colors.grey[600]), - ), - if (widget.searchQuery.isEmpty) - Text( - 'Tap + untuk menambah transaksi baru', - style: TextStyle(color: Colors.grey[500]), + const SizedBox(height: 120), + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.receipt_outlined, size: 64, color: Colors.grey[400]), + const SizedBox(height: 16), + Text( + widget.searchQuery.isNotEmpty + ? 'Tidak ada transaksi yang cocok' + : 'Belum ada transaksi', + style: TextStyle(fontSize: 18, color: Colors.grey[600]), + ), + if (widget.searchQuery.isEmpty) + Text( + 'Tap + untuk menambah transaksi baru', + style: TextStyle(color: Colors.grey[500]), + ), + ], ), + ), ], + ) + : ListView.builder( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(16.0), + itemCount: filteredTransactions.length, + itemBuilder: (context, index) { + final transaction = filteredTransactions[index]; + return TransactionCard( + transaction: transaction, + onEdit: () => _editTransaction(transaction), + onDelete: () => _deleteTransaction(transaction), + ); + }, ), - ) - : ListView.builder( - padding: const EdgeInsets.all(16.0), - itemCount: filteredTransactions.length, - itemBuilder: (context, index) { - final transaction = filteredTransactions[index]; - return TransactionCard( - transaction: transaction, - onEdit: () => _editTransaction(transaction), - onDelete: () => _deleteTransaction(transaction), - ); - }, - ), + ), ), ], ), @@ -1233,41 +1244,52 @@ class _TodoListScreenState extends State { ), ), Expanded( - child: filteredTasks.isEmpty - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, + child: RefreshIndicator( + onRefresh: _loadData, + child: filteredTasks.isEmpty + ? ListView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(16.0), children: [ - Icon(Icons.task_alt, size: 64, color: Colors.grey[400]), - const SizedBox(height: 16), - Text( - widget.searchQuery.isNotEmpty - ? 'Tidak ada task yang cocok' - : 'Belum ada task', - style: TextStyle(fontSize: 18, color: Colors.grey[600]), - ), - if (widget.searchQuery.isEmpty) - Text( - 'Tap + untuk menambah task baru', - style: TextStyle(color: Colors.grey[500]), + const SizedBox(height: 120), + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.task_alt, size: 64, color: Colors.grey[400]), + const SizedBox(height: 16), + Text( + widget.searchQuery.isNotEmpty + ? 'Tidak ada task yang cocok' + : 'Belum ada task', + style: TextStyle(fontSize: 18, color: Colors.grey[600]), + ), + if (widget.searchQuery.isEmpty) + Text( + 'Tap + untuk menambah task baru', + style: TextStyle(color: Colors.grey[500]), + ), + ], ), + ), ], + ) + : ListView.builder( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(8.0), + itemCount: filteredTasks.length, + itemBuilder: (context, index) { + final task = filteredTasks[index]; + return TaskCard( + task: task, + onToggleComplete: () => _toggleTaskComplete(task), + onToggleSubTaskComplete: (subTask) => _toggleSubTaskComplete(task, subTask), + onEdit: () => _editTask(task), + onDelete: () => _deleteTask(task), + ); + }, ), - ) - : ListView.builder( - padding: const EdgeInsets.all(8.0), - itemCount: filteredTasks.length, - itemBuilder: (context, index) { - final task = filteredTasks[index]; - return TaskCard( - task: task, - onToggleComplete: () => _toggleTaskComplete(task), - onToggleSubTaskComplete: (subTask) => _toggleSubTaskComplete(task, subTask), - onEdit: () => _editTask(task), - onDelete: () => _deleteTask(task), - ); - }, - ), + ), ), ], ), @@ -1436,58 +1458,70 @@ class _NotesScreenState extends State { ), ), Expanded( - child: filteredNotes.isEmpty - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, + child: RefreshIndicator( + onRefresh: _loadData, + child: filteredNotes.isEmpty + ? ListView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(16.0), children: [ - Icon(Icons.note_outlined, size: 64, color: Colors.grey[400]), - const SizedBox(height: 16), - Text( - widget.searchQuery.isNotEmpty - ? 'Tidak ada note yang cocok' - : 'Belum ada note', - style: TextStyle(fontSize: 18, color: Colors.grey[600]), - ), - if (widget.searchQuery.isEmpty) - Text( - 'Tap + untuk menambah note baru', - style: TextStyle(color: Colors.grey[500]), + const SizedBox(height: 120), + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.note_outlined, size: 64, color: Colors.grey[400]), + const SizedBox(height: 16), + Text( + widget.searchQuery.isNotEmpty + ? 'Tidak ada note yang cocok' + : 'Belum ada note', + style: TextStyle(fontSize: 18, color: Colors.grey[600]), + ), + if (widget.searchQuery.isEmpty) + Text( + 'Tap + untuk menambah note baru', + style: TextStyle(color: Colors.grey[500]), + ), + ], ), + ), ], - ), - ) - : isGridView - ? GridView.builder( - padding: const EdgeInsets.all(8.0), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - childAspectRatio: 0.8, - crossAxisSpacing: 8, - mainAxisSpacing: 8, + ) + : isGridView + ? GridView.builder( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(8.0), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 0.8, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + ), + itemCount: filteredNotes.length, + itemBuilder: (context, index) { + final note = filteredNotes[index]; + return NoteGridCard( + note: note, + onTap: () => _editNote(note), + onDelete: () => _deleteNote(note), + ); + }, + ) + : ListView.builder( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(8.0), + itemCount: filteredNotes.length, + itemBuilder: (context, index) { + final note = filteredNotes[index]; + return NoteListCard( + note: note, + onTap: () => _editNote(note), + onDelete: () => _deleteNote(note), + ); + }, ), - itemCount: filteredNotes.length, - itemBuilder: (context, index) { - final note = filteredNotes[index]; - return NoteGridCard( - note: note, - onTap: () => _editNote(note), - onDelete: () => _deleteNote(note), - ); - }, - ) - : ListView.builder( - padding: const EdgeInsets.all(8.0), - itemCount: filteredNotes.length, - itemBuilder: (context, index) { - final note = filteredNotes[index]; - return NoteListCard( - note: note, - onTap: () => _editNote(note), - onDelete: () => _deleteNote(note), - ); - }, - ), + ), ), ], ), diff --git a/lib/page/login.dart b/lib/page/login.dart index 92e90e8..3bd227e 100644 --- a/lib/page/login.dart +++ b/lib/page/login.dart @@ -25,6 +25,20 @@ class _LoginPageState extends State { email: email, password: pass, ); + final user = FirebaseAuth.instance.currentUser; + await user?.reload(); + if (!(user?.emailVerified ?? false)) { + await user?.sendEmailVerification(); + await FirebaseAuth.instance.signOut(); + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Email belum terverifikasi. Link verifikasi telah dikirim ulang.'), + duration: Duration(seconds: 4), + ), + ); + return; + } if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Login berhasil')), @@ -57,6 +71,76 @@ class _LoginPageState extends State { } } + Future _forgotPassword() async { + final emailController = TextEditingController(text: _emailController.text.trim()); + final formKey = GlobalKey(); + await showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Reset Password'), + content: Form( + key: formKey, + child: TextFormField( + controller: emailController, + keyboardType: TextInputType.emailAddress, + decoration: const InputDecoration( + labelText: 'Email terdaftar', + border: OutlineInputBorder(), + ), + validator: (value) { + final v = value?.trim() ?? ''; + if (v.isEmpty) return 'Email tidak boleh kosong'; + final emailRegex = RegExp(r'^.+@.+\..+$'); + if (!emailRegex.hasMatch(v)) return 'Format email tidak valid'; + return null; + }, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Batal'), + ), + ElevatedButton( + onPressed: () async { + if (!(formKey.currentState?.validate() ?? false)) return; + final email = emailController.text.trim(); + try { + await FirebaseAuth.instance.sendPasswordResetEmail(email: email); + if (mounted) { + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Link reset password telah dikirim ke email.')), + ); + } + } on FirebaseAuthException catch (e) { + final msg = e.code == 'user-not-found' + ? 'Email tidak terdaftar' + : e.code == 'invalid-email' + ? 'Email tidak valid' + : 'Gagal mengirim email reset: ${e.code}'; + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(msg)), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Terjadi kesalahan: $e')), + ); + } + } + }, + child: const Text('Kirim Link'), + ), + ], + ); + }, + ); + } + @override void dispose() { _emailController.dispose(); @@ -147,7 +231,7 @@ class _LoginPageState extends State { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text('Belum punya akun?'), + const Text('Belum punya akun? '), TextButton( onPressed: () { Navigator.push( @@ -159,6 +243,11 @@ class _LoginPageState extends State { }, child: const Text('Daftar'), ), + const Text(' | '), + TextButton( + onPressed: _forgotPassword, + child: const Text('Lupa password?'), + ), ], ), ], diff --git a/lib/page/register.dart b/lib/page/register.dart index fad5378..a33056b 100644 --- a/lib/page/register.dart +++ b/lib/page/register.dart @@ -34,11 +34,17 @@ class _RegisterPageState extends State { password: pass, ); await cred.user?.updateDisplayName(name); + await cred.user?.sendEmailVerification(); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Registrasi berhasil')), + const SnackBar( + content: Text('Registrasi berhasil. Cek email untuk verifikasi sebelum login.'), + duration: Duration(seconds: 4), + ), ); - Navigator.pop(context); + // Paksa sign out sampai email terverifikasi + await FirebaseAuth.instance.signOut(); + Navigator.pop(context); // kembali ke login } on FirebaseAuthException catch (e) { final msg = _humanizeAuthError(e.code); ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/page/user.dart b/lib/page/user.dart index 1e752c6..3e174cf 100644 --- a/lib/page/user.dart +++ b/lib/page/user.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:tlist/page/login.dart'; import 'package:tlist/page/register.dart'; import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/foundation.dart'; class UserPage extends StatelessWidget { const UserPage({super.key}); @@ -87,6 +88,148 @@ class UserPage extends StatelessWidget { subtitle: Text(email), ), const Divider(), + if (!(user.emailVerified)) + ListTile( + leading: const Icon(Icons.mark_email_unread_outlined), + title: const Text('Kirim ulang verifikasi email'), + onTap: () async { + try { + final acs = kIsWeb + ? ActionCodeSettings( + url: '${Uri.base.origin}/verified', + handleCodeInApp: false, + ) + : null; + if (acs != null) { + await user.sendEmailVerification(acs); + } else { + await user.sendEmailVerification(); + } + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Email verifikasi dikirim')), + ); + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Gagal kirim verifikasi: $e')), + ); + } + } + }, + ), + ListTile( + leading: const Icon(Icons.person_outline), + title: const Text('Ganti nama tampil'), + onTap: () async { + final name = await _prompt(context, title: 'Nama tampil baru', hint: 'Nama lengkap'); + if (name == null || name.trim().isEmpty) return; + try { + await user.updateDisplayName(name.trim()); + await user.reload(); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Nama tampil diperbarui')), + ); + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Gagal ganti nama: $e')), + ); + } + } + }, + ), + ListTile( + leading: const Icon(Icons.alternate_email), + title: const Text('Ganti email'), + subtitle: const Text('Butuh verifikasi ulang, Anda akan keluar'), + onTap: () async { + final currentPass = await _prompt(context, title: 'Konfirmasi Password', hint: 'Password saat ini', obscure: true); + if (currentPass == null || currentPass.isEmpty) return; + final newEmail = await _prompt(context, title: 'Email baru', hint: 'nama@domain.com'); + if (newEmail == null || newEmail.trim().isEmpty) return; + try { + final emailNow = user.email; + if (emailNow == null) throw Exception('Email akun tidak tersedia'); + final cred = EmailAuthProvider.credential(email: emailNow, password: currentPass); + await user.reauthenticateWithCredential(cred); + final newAddr = newEmail.trim(); + final acs = kIsWeb + ? ActionCodeSettings( + url: '${Uri.base.origin}/verified', + handleCodeInApp: false, + ) + : null; + if (acs != null) { + await user.verifyBeforeUpdateEmail(newAddr, acs); + } else { + await user.verifyBeforeUpdateEmail(newAddr); + } + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Verifikasi telah dikirim ke email baru. Selesaikan verifikasi lalu login kembali.')), + ); + } + await FirebaseAuth.instance.signOut(); + } on FirebaseAuthException catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Gagal ganti email: ${e.code}')), + ); + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Gagal ganti email: $e')), + ); + } + } + }, + ), + ListTile( + leading: const Icon(Icons.lock_outline), + title: const Text('Ganti password'), + onTap: () async { + final currentPass = await _prompt(context, title: 'Password saat ini', hint: 'Password', obscure: true); + if (currentPass == null || currentPass.isEmpty) return; + final newPass = await _prompt(context, title: 'Password baru', hint: 'Minimal 6 karakter', obscure: true); + if (newPass == null || newPass.length < 6) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Password baru minimal 6 karakter')), + ); + } + return; + } + try { + final emailNow = user.email; + if (emailNow == null) throw Exception('Email akun tidak tersedia'); + final cred = EmailAuthProvider.credential(email: emailNow, password: currentPass); + await user.reauthenticateWithCredential(cred); + await user.updatePassword(newPass); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Password berhasil diubah')), + ); + } + } on FirebaseAuthException catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Gagal ganti password: ${e.code}')), + ); + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Gagal ganti password: $e')), + ); + } + } + }, + ), ListTile( leading: const Icon(Icons.logout), title: const Text('Keluar'), @@ -103,5 +246,39 @@ class UserPage extends StatelessWidget { ); } + Future _prompt( + BuildContext context, { + required String title, + required String hint, + bool obscure = false, + }) async { + final controller = TextEditingController(); + final result = await showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text(title), + content: TextField( + controller: controller, + obscureText: obscure, + decoration: InputDecoration(hintText: hint), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Batal'), + ), + ElevatedButton( + onPressed: () => Navigator.pop(context, controller.text.trim()), + child: const Text('OK'), + ), + ], + ); + }, + ); + controller.dispose(); + return result; + } + } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 3ba4b5e..65628e3 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,11 +8,13 @@ import Foundation import cloud_firestore import firebase_auth import firebase_core +import firebase_database import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + FLTFirebaseDatabasePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseDatabasePlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 087fedd..aadf063 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -217,6 +217,30 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + firebase_database: + dependency: "direct main" + description: + name: firebase_database + sha256: "4d8eb973f46af4dd481e89fec4f431805ff7415c8bf45ac895a302594c94506e" + url: "https://pub.dev" + source: hosted + version: "12.0.0" + firebase_database_platform_interface: + dependency: transitive + description: + name: firebase_database_platform_interface + sha256: "4f06fec2f58e6a33554c1509d63773c71b41ea3237643f17e72f7036d91f9f04" + url: "https://pub.dev" + source: hosted + version: "0.2.6+11" + firebase_database_web: + dependency: transitive + description: + name: firebase_database_web + sha256: faf6c2059b014d60ab574c1ce5afedc708ccb6bdad4d991c313c0705bba59c56 + url: "https://pub.dev" + source: hosted + version: "0.2.6+17" flutter: dependency: "direct main" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 7d6dbe3..6f252d2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,6 +18,7 @@ dependencies: firebase_core: ^4.0.0 firebase_auth: ^6.0.0 cloud_firestore: ^6.0.0 + firebase_database: ^12.0.0 dev_dependencies: flutter_test: From 4b90d39cd28d457508f0fa804c7bfb0fc19ba833 Mon Sep 17 00:00:00 2001 From: Friyn Date: Tue, 12 Aug 2025 01:56:38 +0700 Subject: [PATCH 05/13] Update Refresh System --- lib/main.dart | 140 +++++++++++++++++++++++++++++++++++++++++--- lib/page/login.dart | 2 +- lib/page/user.dart | 2 +- 3 files changed, 133 insertions(+), 11 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index e26bf03..dc96d3d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'dart:convert'; +import 'dart:async'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; @@ -374,15 +375,123 @@ class _MainScreenState extends State { int _currentIndex = 0; final TextEditingController _searchController = TextEditingController(); String _searchQuery = ''; + bool _loginPromptShown = false; final List _titles = ['Tasks', 'Notes', 'Keuangan']; + // Auth subscription for auto refresh on login/logout/register + StreamSubscription? _authSub; + + // Global keys to control refresh on each tab + final GlobalKey<_TodoListScreenState> _todoKey = GlobalKey<_TodoListScreenState>(); + final GlobalKey<_NotesScreenState> _notesKey = GlobalKey<_NotesScreenState>(); + final GlobalKey<_FinanceScreenState> _financeKey = GlobalKey<_FinanceScreenState>(); + + // Centralized refresh for all tabs + Future _refreshAll() async { + await Future.wait([ + _todoKey.currentState?.refresh() ?? Future.value(), + _notesKey.currentState?.refresh() ?? Future.value(), + _financeKey.currentState?.refresh() ?? Future.value(), + ]); + } + + @override + void initState() { + super.initState(); + // Listen to auth state changes and trigger global refresh + _authSub = FirebaseAuth.instance.authStateChanges().listen((_) { + if (mounted) _refreshAll(); + }); + // Tampilkan popup benefit login setelah frame pertama agar context siap + WidgetsBinding.instance.addPostFrameCallback((_) { + _maybeShowLoginPrompt(); + }); + } + + Future _maybeShowLoginPrompt() async { + if (_loginPromptShown) return; + final user = FirebaseAuth.instance.currentUser; + if (user != null) return; // sudah login, tidak perlu prompt + final prefs = await SharedPreferences.getInstance(); + final seen = prefs.getBool('seen_login_benefit_prompt') ?? false; + if (seen) { + _loginPromptShown = true; + return; + } + _loginPromptShown = true; + if (!mounted) return; + bool doNotShowAgain = false; + showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) { + return StatefulBuilder( + builder: (context, setStateDialog) { + return AlertDialog( + title: const Text('Login ke TList'), + content: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Dengan login kamu bisa'), + const SizedBox(height: 8), + const Text('- Sinkronisasi data ke cloud'), + const Text('- Akses data di banyak perangkat'), + const Text('- Cadangan otomatis'), + const Text(''), + const SizedBox(height: 12), + CheckboxListTile( + value: doNotShowAgain, + onChanged: (v) { + setStateDialog(() { + doNotShowAgain = v ?? false; + }); + }, + title: const Text('Jangan tampilkan lagi'), + contentPadding: EdgeInsets.zero, + controlAffinity: ListTileControlAffinity.leading, + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () async { + final p = await SharedPreferences.getInstance(); + await p.setBool('seen_login_benefit_prompt', doNotShowAgain); + if (Navigator.of(ctx).canPop()) Navigator.of(ctx).pop(); + }, + child: const Text('Nanti saja'), + ), + ElevatedButton( + onPressed: () async { + final p = await SharedPreferences.getInstance(); + await p.setBool('seen_login_benefit_prompt', doNotShowAgain); + if (Navigator.of(ctx).canPop()) Navigator.of(ctx).pop(); + if (!mounted) return; + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const UserPage()), + ); + }, + child: const Text('Ya, Login'), + ), + ], + ); + }, + ); + }, + ); + } + @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(_titles[_currentIndex]), actions: [ + const SizedBox(height: 16), IconButton( icon: const Icon(Icons.person), onPressed: () { @@ -433,9 +542,9 @@ class _MainScreenState extends State { body: IndexedStack( index: _currentIndex, children: [ - TodoListScreen(searchQuery: _searchQuery), - NotesScreen(searchQuery: _searchQuery), - FinanceScreen(searchQuery: _searchQuery), // Screen baru + TodoListScreen(key: _todoKey, searchQuery: _searchQuery, onGlobalRefresh: _refreshAll), + NotesScreen(key: _notesKey, searchQuery: _searchQuery, onGlobalRefresh: _refreshAll), + FinanceScreen(key: _financeKey, searchQuery: _searchQuery, onGlobalRefresh: _refreshAll), // Screen baru ], ), bottomNavigationBar: BottomNavigationBar( @@ -473,6 +582,7 @@ class _MainScreenState extends State { @override void dispose() { + _authSub?.cancel(); _searchController.dispose(); super.dispose(); } @@ -481,8 +591,9 @@ class _MainScreenState extends State { // Finance Screen (BARU) class FinanceScreen extends StatefulWidget { final String searchQuery; + final Future Function()? onGlobalRefresh; - const FinanceScreen({Key? key, required this.searchQuery}) : super(key: key); + const FinanceScreen({Key? key, required this.searchQuery, this.onGlobalRefresh}) : super(key: key); @override State createState() => _FinanceScreenState(); @@ -512,6 +623,9 @@ class _FinanceScreenState extends State { }); } + // Expose public refresh for global trigger + Future refresh() => _loadData(); + Future _saveTransactions() async { await DataService.saveTransactions(transactions); } @@ -702,7 +816,7 @@ class _FinanceScreenState extends State { // Transactions List Expanded( child: RefreshIndicator( - onRefresh: _loadData, + onRefresh: widget.onGlobalRefresh ?? _loadData, child: filteredTransactions.isEmpty ? ListView( physics: const AlwaysScrollableScrollPhysics(), @@ -1071,8 +1185,9 @@ class _AddEditTransactionDialogState extends State { // Todo List Screen (tetap sama seperti sebelumnya) class TodoListScreen extends StatefulWidget { final String searchQuery; + final Future Function()? onGlobalRefresh; - const TodoListScreen({Key? key, required this.searchQuery}) : super(key: key); + const TodoListScreen({Key? key, required this.searchQuery, this.onGlobalRefresh}) : super(key: key); @override State createState() => _TodoListScreenState(); @@ -1098,6 +1213,9 @@ class _TodoListScreenState extends State { }); } + // Expose public refresh for global trigger + Future refresh() => _loadData(); + Future _saveTasks() async { await DataService.saveTasks(tasks); } @@ -1245,7 +1363,7 @@ class _TodoListScreenState extends State { ), Expanded( child: RefreshIndicator( - onRefresh: _loadData, + onRefresh: widget.onGlobalRefresh ?? _loadData, child: filteredTasks.isEmpty ? ListView( physics: const AlwaysScrollableScrollPhysics(), @@ -1304,8 +1422,9 @@ class _TodoListScreenState extends State { // Notes Screen (tetap sama seperti sebelumnya) class NotesScreen extends StatefulWidget { final String searchQuery; + final Future Function()? onGlobalRefresh; - const NotesScreen({Key? key, required this.searchQuery}) : super(key: key); + const NotesScreen({Key? key, required this.searchQuery, this.onGlobalRefresh}) : super(key: key); @override State createState() => _NotesScreenState(); @@ -1332,6 +1451,9 @@ class _NotesScreenState extends State { }); } + // Expose public refresh for global trigger + Future refresh() => _loadData(); + Future _saveNotes() async { await DataService.saveNotes(notes); } @@ -1459,7 +1581,7 @@ class _NotesScreenState extends State { ), Expanded( child: RefreshIndicator( - onRefresh: _loadData, + onRefresh: widget.onGlobalRefresh ?? _loadData, child: filteredNotes.isEmpty ? ListView( physics: const AlwaysScrollableScrollPhysics(), diff --git a/lib/page/login.dart b/lib/page/login.dart index 3bd227e..9d21faa 100644 --- a/lib/page/login.dart +++ b/lib/page/login.dart @@ -171,7 +171,7 @@ class _LoginPageState extends State { ), const SizedBox(height: 12), Text( - 'Welcome to TList!', + 'Welcome Back', textAlign: TextAlign.center, style: Theme.of(context).textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, diff --git a/lib/page/user.dart b/lib/page/user.dart index 3e174cf..0e08347 100644 --- a/lib/page/user.dart +++ b/lib/page/user.dart @@ -45,7 +45,7 @@ class UserPage extends StatelessWidget { ), const SizedBox(height: 16), const Text( - 'You are not sign in', + 'You are not signed in', textAlign: TextAlign.center, style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), ), From e1a09161ccc50c73246c84dc969d752c9ca0a1ec Mon Sep 17 00:00:00 2001 From: Friyn Date: Tue, 12 Aug 2025 09:22:20 +0700 Subject: [PATCH 06/13] Add Update Popup --- .gitignore | 3 +- android/app/src/main/AndroidManifest.xml | 12 ++ lib/main.dart | 38 +++- lib/utils/app_update.dart | 189 ++++++++++++++++++ linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 4 + pubspec.lock | 90 ++++++++- pubspec.yaml | 5 +- web/manifest.json | 2 +- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 12 files changed, 346 insertions(+), 6 deletions(-) create mode 100644 lib/utils/app_update.dart diff --git a/.gitignore b/.gitignore index 51c0e79..d0bf2e1 100644 --- a/.gitignore +++ b/.gitignore @@ -52,4 +52,5 @@ firebase-debug.log .firebase .github firebaseconfig.js -lib/firebase_options.dart \ No newline at end of file +lib/firebase_options.dart +web/update.json \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 6a67716..b585870 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,16 @@ + + + + + + + + + + + + { final TextEditingController _searchController = TextEditingController(); String _searchQuery = ''; bool _loginPromptShown = false; + // Optional: URL ke manifest update (JSON) yang di-host di GitHub Pages/Releases. + // Kosongkan jika tidak ingin mengaktifkan fitur update popup. + static const String _updateManifestUrl = 'https://tlistserver.web.app/update.json'; final List _titles = ['Tasks', 'Notes', 'Keuangan']; // Auth subscription for auto refresh on login/logout/register StreamSubscription? _authSub; + bool _pendingAuthRefresh = false; // Global keys to control refresh on each tab final GlobalKey<_TodoListScreenState> _todoKey = GlobalKey<_TodoListScreenState>(); @@ -399,14 +404,40 @@ class _MainScreenState extends State { @override void initState() { super.initState(); - // Listen to auth state changes and trigger global refresh + // Listen to auth state changes and trigger global refresh (next frame, debounced) _authSub = FirebaseAuth.instance.authStateChanges().listen((_) { - if (mounted) _refreshAll(); + if (!mounted || _pendingAuthRefresh) return; + _pendingAuthRefresh = true; + try { + WidgetsBinding.instance.addPostFrameCallback((_) async { + try { + if (mounted) { + await _refreshAll(); + } + } catch (e, st) { + debugPrint('Auth refresh error: $e\n$st'); + } finally { + _pendingAuthRefresh = false; + } + }); + } catch (e, st) { + debugPrint('Scheduling auth refresh failed: $e\n$st'); + _pendingAuthRefresh = false; + } }); // Tampilkan popup benefit login setelah frame pertama agar context siap WidgetsBinding.instance.addPostFrameCallback((_) { _maybeShowLoginPrompt(); }); + // Cek pembaruan aplikasi (cross-platform) setelah frame pertama + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_updateManifestUrl.isNotEmpty) { + AppUpdate.checkAndPrompt( + context, + config: const AppUpdateConfig(manifestUrl: _updateManifestUrl), + ); + } + }); } Future _maybeShowLoginPrompt() async { @@ -863,6 +894,7 @@ class _FinanceScreenState extends State { ], ), floatingActionButton: FloatingActionButton( + heroTag: 'fab-finance', onPressed: () => _addTransaction('income'), child: const Icon(Icons.add), ), @@ -1412,6 +1444,7 @@ class _TodoListScreenState extends State { ], ), floatingActionButton: FloatingActionButton( + heroTag: 'fab-tasks', onPressed: _addTask, child: const Icon(Icons.add), ), @@ -1648,6 +1681,7 @@ class _NotesScreenState extends State { ], ), floatingActionButton: FloatingActionButton( + heroTag: 'fab-notes', onPressed: _addNote, child: const Icon(Icons.add), ), diff --git a/lib/utils/app_update.dart b/lib/utils/app_update.dart new file mode 100644 index 0000000..1aa54df --- /dev/null +++ b/lib/utils/app_update.dart @@ -0,0 +1,189 @@ +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class AppUpdateConfig { + final String manifestUrl; + // Optional: key names in manifest + const AppUpdateConfig({required this.manifestUrl}); +} + +class AppUpdateInfo { + final String version; // e.g. 1.2.0+5 + final String? title; + final String? notes; + final String? url; // generic fallback + final String? urlAndroid; + final String? urlWindows; + final String? urlWeb; + final String? minForce; // if set and > current, force update + + AppUpdateInfo({ + required this.version, + this.title, + this.notes, + this.url, + this.urlAndroid, + this.urlWindows, + this.urlWeb, + this.minForce, + }); + + factory AppUpdateInfo.fromJson(Map j) { + return AppUpdateInfo( + version: (j['version'] ?? '').toString(), + title: j['title'] as String?, + notes: j['notes'] as String?, + url: j['url'] as String?, + urlAndroid: j['url_android'] as String?, + urlWindows: j['url_windows'] as String?, + urlWeb: j['url_web'] as String?, + minForce: j['min_force'] as String?, + ); + } +} + +class AppUpdate { + static const _prefsDismissKey = 'dismissed_update_version'; + + static Future checkAndPrompt( + BuildContext context, { + required AppUpdateConfig config, + bool silentOnError = true, + }) async { + try { + final info = await _fetchUpdateInfo(config.manifestUrl); + if (info == null) return; + + final pkg = await PackageInfo.fromPlatform(); + final currentVersion = '${pkg.version}+${pkg.buildNumber}'; + + final isNewer = _isRemoteNewer(currentVersion, info.version); + if (!isNewer) return; + + final prefs = await SharedPreferences.getInstance(); + final dismissed = prefs.getString(_prefsDismissKey); + + final forced = info.minForce != null && _isRemoteNewer(currentVersion, info.minForce!); + if (!forced && dismissed == info.version) { + return; // already dismissed this optional version + } + + if (!context.mounted) return; + await _showDialog(context, info, forced: forced, onDismissRemember: () async { + await prefs.setString(_prefsDismissKey, info.version); + }); + } catch (e) { + if (!silentOnError) { + // Optionally log + debugPrint('Update check error: $e'); + } + } + } + + static Future _fetchUpdateInfo(String url) async { + final res = await http.get(Uri.parse(url)).timeout(const Duration(seconds: 10)); + if (res.statusCode != 200) return null; + final data = json.decode(res.body) as Map; + return AppUpdateInfo.fromJson(data); + } + + static bool _isRemoteNewer(String current, String remote) { + // Parse like 1.2.3+45 into [1,2,3,45] + List parse(String v) { + final parts = v.split('+'); + final ver = parts[0]; + final build = parts.length > 1 ? int.tryParse(parts[1]) ?? 0 : 0; + final nums = ver.split('.').map((e) => int.tryParse(e) ?? 0).toList(); + while (nums.length < 3) nums.add(0); + nums.add(build); + return nums; + } + + final a = parse(current); + final b = parse(remote); + for (int i = 0; i < a.length && i < b.length; i++) { + if (b[i] > a[i]) return true; + if (b[i] < a[i]) return false; + } + return false; // equal + } + + static Future _showDialog( + BuildContext context, + AppUpdateInfo info, { + required bool forced, + required Future Function() onDismissRemember, + }) async { + final title = info.title ?? 'Pembaruan Tersedia'; + final notes = info.notes ?? 'Versi baru: ${info.version}'; + + return showDialog( + context: context, + barrierDismissible: !forced, + builder: (ctx) { + return WillPopScope( + onWillPop: () async => !forced, + child: AlertDialog( + title: Text(title), + content: Text(notes), + actions: [ + if (!forced) + TextButton( + onPressed: () async { + await onDismissRemember(); + if (context.mounted) Navigator.of(ctx).pop(); + }, + child: const Text('Nanti'), + ), + ElevatedButton( + onPressed: () async { + final url = info.url ?? info.urlAndroid ?? info.urlWindows ?? info.urlWeb; + final opened = await _openUpdateUrl(context, url); + if (!forced) { + if (context.mounted) Navigator.of(ctx).pop(); + } + }, + child: const Text('Update'), + ), + ], + ), + ); + }, + ); + } + + static Future _openUpdateUrl(BuildContext context, String? url) async { + if (url == null || url.isEmpty) { + _showSnack(context, 'URL update tidak tersedia'); + return false; + } + final uri = Uri.tryParse(url); + if (uri == null) { + _showSnack(context, 'URL update tidak valid'); + return false; + } + try { + if (kIsWeb) { + // Di web, buka tab baru agar tidak diblokir popup blocker + return await launchUrl(uri, webOnlyWindowName: '_blank'); + } else { + // Mobile/desktop: buka aplikasi eksternal (browser/store) + if (await canLaunchUrl(uri)) { + return await launchUrl(uri, mode: LaunchMode.externalApplication); + } + } + } catch (_) {} + _showSnack(context, 'Gagal membuka tautan update'); + return false; + } + + static void _showSnack(BuildContext context, String msg) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg))); + } +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e71a16d..f6f23bf 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2e1de87..f16b4c3 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 65628e3..4092a20 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,12 +9,16 @@ import cloud_firestore import firebase_auth import firebase_core import firebase_database +import package_info_plus import shared_preferences_foundation +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseDatabasePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseDatabasePlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index aadf063..e59f115 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -289,7 +289,7 @@ packages: source: hosted version: "0.15.6" http: - dependency: transitive + dependency: "direct main" description: name: http sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" @@ -376,6 +376,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968" + url: "https://pub.dev" + source: hosted + version: "8.3.1" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + url: "https://pub.dev" + source: hosted + version: "3.2.1" path: dependency: transitive description: @@ -565,6 +581,70 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.2" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "0aedad096a85b49df2e4725fa32118f9fa580f3b14af7a2d2221896a02cd5656" + url: "https://pub.dev" + source: hosted + version: "6.3.17" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" + url: "https://pub.dev" + source: hosted + version: "6.3.3" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" vector_math: dependency: transitive description: @@ -589,6 +669,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03" + url: "https://pub.dev" + source: hosted + version: "5.14.0" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 6f252d2..396f8b0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: "A new Flutter project." publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 1.1.0+2 +version: 1.1.0+3 environment: sdk: '>=3.0.0 <4.0.0' @@ -19,6 +19,9 @@ dependencies: firebase_auth: ^6.0.0 cloud_firestore: ^6.0.0 firebase_database: ^12.0.0 + package_info_plus: ^8.0.2 + http: ^1.2.2 + url_launcher: ^6.3.0 dev_dependencies: flutter_test: diff --git a/web/manifest.json b/web/manifest.json index 1e5fabc..af34406 100644 --- a/web/manifest.json +++ b/web/manifest.json @@ -32,4 +32,4 @@ "purpose": "maskable" } ] -} +} \ No newline at end of file diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index bf6d21a..a3c4d16 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -9,6 +9,7 @@ #include #include #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { CloudFirestorePluginCApiRegisterWithRegistrar( @@ -17,4 +18,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi")); FirebaseCorePluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b83b40a..c215e65 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST cloud_firestore firebase_auth firebase_core + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST From 8a24163eb066e9c3d6d969463809dc5a790b333c Mon Sep 17 00:00:00 2001 From: Friyn Date: Wed, 13 Aug 2025 13:41:40 +0700 Subject: [PATCH 07/13] Update Popup-Update --- android/app/src/main/AndroidManifest.xml | 2 + lib/page/user.dart | 84 +++++++++++++++++++++++ lib/utils/app_update.dart | 61 +++++++++++++++- pubspec.yaml | 9 ++- web/faviconn.png | Bin 917 -> 0 bytes web/icons/Icon-192.png | Bin 5292 -> 8390 bytes web/icons/Icon-512.png | Bin 8252 -> 45353 bytes web/icons/Icon-maskable-192.png | Bin 5594 -> 8390 bytes web/icons/Icon-maskable-512.png | Bin 20998 -> 45353 bytes web/icons/light-4x.png | Bin 0 -> 15274 bytes web/manifest.json | 6 +- 11 files changed, 155 insertions(+), 7 deletions(-) delete mode 100644 web/faviconn.png create mode 100644 web/icons/light-4x.png diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b585870..916806c 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,6 @@ + + diff --git a/lib/page/user.dart b/lib/page/user.dart index 0e08347..e9ea1b6 100644 --- a/lib/page/user.dart +++ b/lib/page/user.dart @@ -3,6 +3,7 @@ import 'package:tlist/page/login.dart'; import 'package:tlist/page/register.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/foundation.dart'; +import 'package:tlist/utils/app_update.dart'; class UserPage extends StatelessWidget { const UserPage({super.key}); @@ -71,6 +72,25 @@ class UserPage extends StatelessWidget { style: OutlinedButton.styleFrom(minimumSize: const Size.fromHeight(48)), child: const Text('Daftar'), ), + const Divider(), + ListTile( + leading: const Icon(Icons.system_update_alt), + title: const Text('Cek pembaruan'), + subtitle: const Text('Periksa apakah ada versi terbaru'), + onTap: () async { + final manifestUrl = kIsWeb + ? Uri.base.resolve('update.json').toString() // same-origin to avoid CORS in web + : 'https://tlistserver.web.app/update.json'; + final cfg = AppUpdateConfig(manifestUrl: manifestUrl); + // Manual check: tampilkan prompt meskipun sebelumnya pernah di-dismiss. + await AppUpdate.checkAndPrompt( + context, + config: cfg, + silentOnError: false, + ignoreDismiss: true, + ); + }, + ), ], ); } @@ -242,6 +262,70 @@ class UserPage extends StatelessWidget { } }, ), + const Divider(), + ListTile( + leading: const Icon(Icons.system_update_alt), + title: const Text('Cek pembaruan'), + subtitle: const Text('Periksa apakah ada versi terbaru'), + onTap: () async { + final manifestUrl = kIsWeb + ? Uri.base.resolve('update.json').toString() + : 'https://tlistserver.web.app/update.json'; + final cfg = AppUpdateConfig(manifestUrl: manifestUrl); + final status = await AppUpdate.getStatus(config: cfg); + if (!context.mounted) return; + final info = status.info; + if (info == null) { + // gagal memuat manifest + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Gagal memeriksa pembaruan'), + content: const Text('Tidak bisa memuat informasi pembaruan saat ini. Coba lagi nanti.'), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(), + child: const Text('Tutup'), + ), + ], + ), + ); + return; + } + + if (!status.isNewer) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Sudah versi terbaru'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Versi terpasang: ${status.currentVersion}'), + Text('Versi terbaru: ${info.version}'), + const SizedBox(height: 8), + if ((info.notes ?? '').isNotEmpty) + Text(info.notes!), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(), + child: const Text('OK'), + ), + ], + ), + ); + } else { + await AppUpdate.checkAndPrompt( + context, + config: cfg, + silentOnError: false, + ); + } + }, + ), ], ); } diff --git a/lib/utils/app_update.dart b/lib/utils/app_update.dart index 1aa54df..0027a5c 100644 --- a/lib/utils/app_update.dart +++ b/lib/utils/app_update.dart @@ -47,6 +47,20 @@ class AppUpdateInfo { } } +class UpdateCheckResult { + final String currentVersion; + final AppUpdateInfo? info; + final bool isNewer; + final bool forced; + + UpdateCheckResult({ + required this.currentVersion, + required this.info, + required this.isNewer, + required this.forced, + }); +} + class AppUpdate { static const _prefsDismissKey = 'dismissed_update_version'; @@ -54,6 +68,7 @@ class AppUpdate { BuildContext context, { required AppUpdateConfig config, bool silentOnError = true, + bool ignoreDismiss = true, }) async { try { final info = await _fetchUpdateInfo(config.manifestUrl); @@ -69,7 +84,7 @@ class AppUpdate { final dismissed = prefs.getString(_prefsDismissKey); final forced = info.minForce != null && _isRemoteNewer(currentVersion, info.minForce!); - if (!forced && dismissed == info.version) { + if (!forced && !ignoreDismiss && dismissed == info.version) { return; // already dismissed this optional version } @@ -113,6 +128,50 @@ class AppUpdate { return false; // equal } + /// Fetch update status along with manifest details and current version. + static Future getStatus({ + required AppUpdateConfig config, + }) async { + try { + final info = await _fetchUpdateInfo(config.manifestUrl); + final pkg = await PackageInfo.fromPlatform(); + final currentVersion = '${pkg.version}+${pkg.buildNumber}'; + final isNewer = info != null ? _isRemoteNewer(currentVersion, info.version) : false; + final forced = info != null && info.minForce != null + ? _isRemoteNewer(currentVersion, info.minForce!) + : false; + return UpdateCheckResult( + currentVersion: currentVersion, + info: info, + isNewer: isNewer, + forced: forced, + ); + } catch (_) { + final pkg = await PackageInfo.fromPlatform(); + return UpdateCheckResult( + currentVersion: '${pkg.version}+${pkg.buildNumber}', + info: null, + isNewer: false, + forced: false, + ); + } + } + + /// Returns true if a newer version than the currently installed app is available. + static Future isUpdateAvailable({ + required AppUpdateConfig config, + }) async { + try { + final info = await _fetchUpdateInfo(config.manifestUrl); + if (info == null) return false; + final pkg = await PackageInfo.fromPlatform(); + final currentVersion = '${pkg.version}+${pkg.buildNumber}'; + return _isRemoteNewer(currentVersion, info.version); + } catch (_) { + return false; + } + } + static Future _showDialog( BuildContext context, AppUpdateInfo info, { diff --git a/pubspec.yaml b/pubspec.yaml index 396f8b0..d2219fa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,9 +1,12 @@ name: tlist -description: "A new Flutter project." +description: "A Todo list app with some aditional features" +homepage: https://list.novila.xyz +repository: https://github.com/friyn/tlist -publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 1.1.0+3 +publish_to: 'none' + +version: 1.2.0+120 environment: sdk: '>=3.0.0 <4.0.0' diff --git a/web/faviconn.png b/web/faviconn.png deleted file mode 100644 index 8aaa46ac1ae21512746f852a42ba87e4165dfdd1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 917 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|I14-?iy0X7 zltGxWVyS%@P(fs7NJL45ua8x7ey(0(N`6wRUPW#JP&EUCO@$SZnVVXYs8ErclUHn2 zVXFjIVFhG^g!Ppaz)DK8ZIvQ?0~DO|i&7O#^-S~(l1AfjnEK zjFOT9D}DX)@^Za$W4-*MbbUihOG|wNBYh(yU7!lx;>x^|#0uTKVr7USFmqf|i<65o z3raHc^AtelCMM;Vme?vOfh>Xph&xL%(-1c06+^uR^q@XSM&D4+Kp$>4P^%3{)XKjo zGZknv$b36P8?Z_gF{nK@`XI}Z90TzwSQO}0J1!f2c(B=V`5aP@1P1a|PZ!4!3&Gl8 zTYqUsf!gYFyJnXpu0!n&N*SYAX-%d(5gVjrHJWqXQshj@!Zm{!01WsQrH~9=kTxW#6SvuapgMqt>$=j#%eyGrQzr zP{L-3gsMA^$I1&gsBAEL+vxi1*Igl=8#8`5?A-T5=z-sk46WA1IUT)AIZHx1rdUrf zVJrJn<74DDw`j)Ki#gt}mIT-Q`XRa2-jQXQoI%w`nb|XblvzK${ZzlV)m-XcwC(od z71_OEC5Bt9GEXosOXaPTYOia#R4ID2TiU~`zVMl08TV_C%DnU4^+HE>9(CE4D6?Fz oujB08i7adh9xk7*FX66dWH6F5TM;?E2b5PlUHx3vIVCg!0Dx9vYXATM diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png index b749bfef07473333cf1dd31e9eed89862a5d52aa..f945daf59402dcff0900f132bf3be5f8acba6644 100644 GIT binary patch literal 8390 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 diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png index 88cfd48dff1169879ba46840804b412fe02fefd6..f3e94b9255b1c2f6064d89414865dbb8d00fc720 100644 GIT binary patch literal 45353 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%0DSiU&5AoU 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 diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png index eb9b4d76e525556d5d89141648c724331630325d..f945daf59402dcff0900f132bf3be5f8acba6644 100644 GIT binary patch literal 8390 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 X>c!)3PgKfL&lX^$Z>Cpu&6)6jvi^Z! diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png index d69c56691fbdb0b7efa65097c7cc1edac12a6d3e..f3e94b9255b1c2f6064d89414865dbb8d00fc720 100644 GIT binary patch literal 45353 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%0DSiU&5AoU zxY^f9{WRpve!$HAhW?*A+zE1uI*Al02Kx<%MnV3WTKc531DtlI{CYc)njDj@VeTS1 zN3w8~SAi-*D=XeI2Z&|f{B=ltniVQ;I#~>b(OI>N3&;?JARF10DoEn1nDM! zo3r0;%l`DRS}v;5N$MzqqaZOnVeT%WF`?vK#qEQ=;k04cbw9gQ)6QdkIFD!kZ0=xf z{_J#P?CcC-iG1KNt~YM@kYPgB4QY!O?8)UD?4u?oB%6kAr^^0FK3E8tO2@DdhbpnF zb*p%k$+<|Oo#_k7*U_9# zpdFhh=1)dP@g2+8fFms%&d$n5)NqB)6AM%CU>!`#zX@&VHXwCWjw|SJquvmpeQlyi zxEF7I&UPpLCx+3f$Srw9J$s0bZ=Wnw+@hZTRpfTe9gwW6^7x zdoV^FXJz8sd{-Lt#Pp_e5a+Jup0r4gshU{J*Qmhl^1SVw@gstG?!!|Oz}CUe4%6YU zD;o%c^QLvumh-E}gG--N{X<{@A%4?DJdhs%P3qXVtG7++luYo`mj_CjOCN~=aqD3t z#$#|&T@a`J5f$7T7;fYnyh?&`NqWBc;)i&>O*K9DqMj!E*AK%zWA7|h-9CuC7%N#x z!e$1|wrvPRG-^kOjS5hOvXUrYHH${BF|7S*Wz#jS`>~M|)Xe*>eR$mN#eqxa;y&RX zkBJL~anJKQ{9OH{Gy}CW*<@th+<&4S1Og5$*sYlC}UL!8Pgi26esm+sQ zY1S`9>=KfDlQMFZDF1u*qS)A8LUmP>W#$Fs!2V2=7yy1s$X^$>)I6m;p>+5Tj;8Bo z@@2V#r8DVVo-s}k>e#_Xe_b85$d=6)P_Y9eudDr<8a0Cb;hqF*pAa7Vr7 ztRl$RjzMU{QRk%70)q#G#rKb9m@{f(|r?Fh#FuNaMWGl60$&f6X#LKJ>$(Wv>eiS<3 zwK@=b8=ZK19(Or3Udk1iEPLvwb3zMCaEGC*HAzKuON7-o83N?4JodeGD1E#Zxr=fU zZrdtaAz~L|yyLF>t#}r4UX`9>5MW_*i$qdN+s|As5M1HUIdUvH4IR_DXtcd*{Bg9# z6HSW3VoE=L7U)W|<8t@v{t=pEa0=pq0p&BR(NSyL-M( zhe)VG5JfSV%zoVvkn%IR=TpZuO`a^S>N$YFxP8Evb=K%V8HwLbj>@ln1w)g*Tb686 z(^;Z7W_Wjt+IGwc@Io$JA=0|wTLdh`@g_Q#9-KXu7wv7jnTmPDPIt!ZiT8X?Ck{bZ z;wz<2| z#IlfDWLA0&W=+@>k}KzMd$CP@g+m3io}&`+NY_z!w5h^H%8_W7lWa?9*SM49W$xMe zR`S|uYo>N85ujM_uOxoCR~Fm7DS1z0_`j-s!U^7oP$-x(WTxrl>?MKFG&t)w?`O_@ zB*Aoj?5Ibst=qqLD(OwLww_+1yL1XXn6&vRKnDOg$SG|1+#6xXmhix^LcWK%oDcM@ z5{x3%BYe4+Froo*sxeJSOv&2&sF!-tBG3R6voec!twWr7bqxmtJg0vK7Onw>>qLJQ z0&DbS)X-rl(a0VTG?E2I?0S_52!ZC8G`hJd5gs_(uy}G2K=-@Dp|OycgSef3ENaxvG4h_(~_RJgRAcTy$c6i9jq2- z>LlK)SE9kBwnTqRP}TH48|tVl;2V95U2%u^Bh;hUvA8f>ND`=pg>yk=VIJSgcrlSH@rzA2e_7nK=4?@$gAErLIk9%r5e$Jr; z=j_-E$bM`TGadG5s4Pg1zoi0`I z)^nm7V0z6b^o~4LnpFHbsf?Z$v@8F>Nhonl;SXU$V^!iU-wmlPha;lA$L3My2}38% zokkv;zlQU=hqv*32C@WNJb$6+ez}B5(`6y&Q`ehPx2UF{;-iHTgA)1y-v0=byhWNi zx5J86hq%a}Hdu=+Jc~GTMJppuATR%{{@C)7o(b`^rr$oh{VCYPYd6fFA`-^EcCHwR z9ae}X66_`-R8$G;=`BGrgr(KR#7gQK_sxYg#?5=E&f-PeHw7N6nLK)5!U}#DkrOy> zPUv^YmO|ih-5HE$tOJUq(V%wI*XW@%<|R8JY{GhMu~8Gl(vJ*G0FxSVcwK3kUQ0AgDr2G8{dt*ro^X*Pu`A0J1pu}u(>A=$zaMF%lvs%E z??ab5o(P~Bb5ogqy}e42u?a4j2;;-khjR1tdfQf`3Z=rI1dTpvi$K=S!{Om7$#I=+ zX%0-li&L#W!hQH0;MU#ZyHqTpq<6u0b6>vUiZ^{5EUm<{V|g{@86myBo~q+EmA*Z3 zD8`zn`E@s@IIY$aCB<#x7PfwbLN13cUs|3kJs?smtv^rreeR9_sR);*hms8QNm57) z@xxO3sW83dgzYGTg0K{DcHXBWcSizLli%x>4YZeP5jR3G<}@3>rhD#gKZe5ceS}L+<--*uToNL2Vuta?-X5TBG=?wMc;zkzcHH3{x@=L*d%WC z_`cq}ux``HX?t9(W(_`+YaIysp6s==jdwt7c?WLtF^07Z(?FyDt0qHJfkz76(6m?B z+foAiXk)!-fHqCMgb~Bx9NG`wr(x#GpNx|=-+J4Ua)_Q*ft-x=4<)NSkM#x&IWL=d zm-kyQZLVieSqyuEk=e>Px|>=L(cr|AwWbm{nPGqoJKtX?VnW|MU3%{tz-CeN;LSJ= zatgLg`h{u61v#0f$4R~pcFnETIA_F>Df|pl!o&aoG!i!7ml6A|?8w(u#Vo$|R<>x$ z>Y+DbU?i4r5PLWWk0m8+H4{))vu_J`w~z(T)`Xdr9vcUDWp)^sQHacCa>~o=fBQ24 zBc!a5pxqq=Za`%-aEg`cu2c*;N~c^6B*+em$fY7x&lhQRzM`rDVFbG%U1^KFFLJ5) zFq83xD%mrm7A-9AMeK2|4d#I_c7`EyD;MeCJBN~Q_v@PiKwE2|nfCl?r@CK@s^Pm} z{s1bp`CW`l#ALQC_FLbYe7o=Namtlv--#of{OmOb|G?IOyQI1*3!9(9%_A1gL=H+7 zR}jM&Bar#U;UQL~OZ~@y+L)l?`D7+oP6zb+5u}D3xF)mxNn0MQ}Q&&A5&yy^X*|WFL)Czj+zna(J zFCI8zd`gDiNG9ByRSiCm)~QEL?IYuZ%@RoD?43$lwz~<2I^IbAaV^R;DgbTzH2|!K z?WMpxW1mT^{OnTUZ*t8qT>Zp$c{s18S18?7NQOi*$NTiv%O!7wlPUPe^@+!nKbg@L zcIweA`P8{ZI>r|T->MY{<46hNIBC{;Y`}t-4X(bkt?)Hcn&M&Cfwf5!-05?6g}+$h zLBeL!)9KGR*W0*Ap&t=fbOnDT)Oj)<*d*g?_q;$(TsdD^TDTabMlzgi{n{ zf5s6$9MZ2U0LS5A39jV^l(v|Zn46J2@H$Dq>Ap`Uv1KW1>l7Ds?sS(cHlF%l{|vY& z5-YB8LU7GT>Pq-h*=Of8!8SC(nL<4_Lg0DH8rB{BMY*(zEGKlXpazvD0Fa z#fJHdM228hy`v6AdMCF+%TbyUrJ_&lcl7Yne_wa?Lfa?J2`d}#?E7=u1?<0+7kM=f zce7<>rZo?lOy^k+tkgbPakVm2H+e}u-+XntS2S@rpgg8K+nM}0RU~!6-6H9N_UmOn z3`d>&+xaf~QMIg6=voNnWVNs%ky+otPNSVWuV;Tlf`jpboX@~@vqqcTTU!ax{KI)c zX=tekT*a!(Ne@zP?9R74OTV!&`*!nw3qDy0TZgy%&`8r(GPn4>^V9~HNc0Rh^rgkl z58Xq=NG_P4{WG3zxmWsKi9SVyDdM@?@J-465FS#+<(XYCgJ1=-baU0NWYJL3(*Cot za${{&iTw<8)629hT)HLO=kt(hQ?u$5w;c+^%eB`RRr+24Fxc=a@BGLcf2LkmSMjuW zCW&m887*!Riv=`B1>tsk+VEQCgSqY@bMLEa8WPvsICsc|m2E}hHm%2m9jZUC1kFk< zlDn3Heq)ywi!CibC(G^E{Meq^+G)Lv>YdyvI_wj3aSlB2TfUKrf30(>^loP#xl@JQ z5$heM7Tn{)E<>Lfum+SqB{i+ExY*CgA-XL?NFWBI8~T|zRYgk1wL6!FP9DVra)VEh zgG7@uW2%v_NUEmkpIgyv7wr6g(E8~2aNf}GsEojZJ0vegPs%<;Tg_jTg{8_J8BNym zrJ<45(8lZsS*i3jH{xN&ywrNvG_&roC>m3)5fPY+s*?1@QhB|*@esrZDY?3AZp&I9 zwcAqI9K0rC+-07fyNzUXuN!&$qo4Qkw@l5+z9<~Xf9!knGvLvwaq6EW+wu{)EqpXi zcBa&R$uXgc_8h7Ne|>_ogtKLvSqI&(0ut=oVY62Yh3=)EiV{AHc-$7NdPt!(=9;UVVIqDdJ+c z(1P6zdY!UTOfW6~Whk4ulyz(mn$5e0O>7|MNPb-WV)56U#LkA#QY^4lu)p4_`dIWU zpJQ$KnlHU|Xt_E_CB*A~siYahh}vjAl0HAGvB$uhS}!ng?BN z3ng7v8#al)09vYfmXla)pb*NRn@ZwdYPuZNKB)SBjhF@b$ajJ3Re)=_Hd$3lu-`R{ z&rNMg$25)eXTKc3nwsx_9|GFe%)a-DQVB?Dv5}Z;GI({{Kl*CHQqPQh_z~re7PWkM z_A^Gc<#?~?`RWwT{;=-v>A{tZ0;x5gzPcsW z45l9~_t1f}<*kAfh& zs*H)N3gJXSFDAOo1A2fO;~%oIWUpy01t#wCiKeUyF@AX_Ykf27vX*5({pXw8b0Hse zllS~uGMd6E06Ojd&j-T|ZsI=e;J}1v_n=3z8z4q;^1o!d=qj)sQ59L=lIF7vK6PmM zB%xq28Pq#r2alzNhlctYJ5}9&`s;&VBu~9vijRA$^@NZ@x&8=F1_IynJv`%!)?}mC zpj&Za_j~`6cS5ozk=`w)4VxlX2QDe;lzDi5xqUWyd3R@Vf%?sEcq_za?TaO2KjuGY zQSdTyC!3Fzs#jC@ujYGJGQXks)V2dK5G~A@G6be--X_>hDl9d|AE@*?$o;;Jy% z-Cocv^y9;P)ab~mBiwlY@51P|w^RBb1z-N2STmPPTG~qeQQC@nt)lgNl`ckf9PelC zbm&?R#gr9VmYXiZy;|p`k54J_19-*>71jxS%1ll81D?Z{pjHZYKbb&H2v?rF56JDurFJe(i)96}Mvb6Z32~qp9P3V2l ztJUEm-pRB2H^2^MI${-}!t`*Tby(K#@vo4uoe(l{zTY8PtjHI!^jHDnMVfNk2`<-X zeO9E#RK34%KYi!gLpIaYHlM(+=x>@5ni6MlXPm$;DJ9d8cu(d|bG z0{vLFDLqHgFFheNYB-c9L*suH;Ynxt8<9Op!qcv`Od-!T-Bg%oFTW{Na?h_qlzD*L zR(qStk6pJQ+QmkL-|`3)nhb`n68!zh&_|tE0`-pT*s7&ob8I*?kSW_!KLN9t(; zIJj3)cdZtaqvC&64T>9ErUABmBDr{;VjrmDSac}^5(b3^nRBRLsoAnRJk*$|Wb>w( zMhj0fdK$df7iE{5J}qZU%t>Fqu2pdAI# zK)cEVmCj5}jS}M%wG~AT)`EM%NF+TY10$bY0x7>OM~9A~Zl7qoC>u)$rN!Yc?syy# zm+oPMyw3}A1x2DFQP&u$7^w>R8Pq$$AZRGc5;iK#k&~DzKocac-WcW?@l$g{4f?vn ztvkexta4L&O8?1+$?4{kOx#=@{^az}4r#y{VPqWMdz##`EMhFq-%MG+lC1Zah(C%& z)S96ivR)c#NV}t}dvkAqo`ou9J=4-H&ljc*%}=T6C~<<*4u)YI!<^XI<0CB#!yQ$Y zXPgsU8+kCPFw%R-5c*fq8X0(n-jO&kX@Ts5Rt=+rJ72juaoHcJH74mm zvhJE`MM(1vRy|SGY1C?JI!@m{IGH>4|BcY_w|g_(hulU$E{-~*dY^nue$SzygwX}% zmX`EH=2sId$K^#0>#zpNpgzdO4A{M5snTp;s*n^4_K{%~I?_m2nnf(8|J+-5`^hQ~0+ z_jGZvZ^Adbq);x(ZkCVI-J^9jzD07klQUxi`dw54=C1kO z$ztXE(0M?^$fDNo8ow+VaNUldkD6_tbg2(Rk_vLXTIS!W^CJd0siSGg)eQ7-f(Mey zwrA=S+BGD@H(cu^r-fj3&_A(A50#{&>(`I)xnFPW-n(#E!Bcoa)S?8AxJCiO52mRP J{iIlT2MkSQ344eAvrhici!td|HJuyvJm#Y_w1Q9Yu3!26dNlO-oxUDK_C#XnW^Co z5C{VN6#{~B0)K2j7}*1Xq(Nqemv23A-6&=ZpEijkVnSwVGqLv40?n0=p;k3-U5e5+ z+z3>aS`u9DS=!wg8ROu?X4TFoW6CFLL&{GzoVT)ldhLekLM|+j3tIxRd|*5=c{=s&*vfPdBr(Fyj(v@%eQj1Soy7m4^@VRl1~@-PV7y+c!xz$8436WBn$t{=}mEdK#k`aystimGgI{(IBx$!pAwFoE9Y`^t^;> zKAD)C(Dl^s%`?q5$P|fZf8Xymrtu^Pv(7D`rn>Z-w$Ahs!z9!94WNVxrJuXfHAaxg zC6s@|Z1$7R$(!#t%Jb{{s6(Y?NoQXDYq)!}X@jKPhe`{9KQ@sAU8y-5`xt?S9$jKH zoi}6m5PcG*^{kjvt+kwPpyQzVg4o)a>;LK`aaN2x4@itBD3Aq?yWTM20VRn1rrd+2 zKO=P0rMjEGq_UqpMa`~7B|p?xAN1SCoCp}QxAv8O`jLJ5CVh@umR%c%i^)6!o+~`F zaalSTQcl5iwOLC&H)efzd{8(88mo`GI(56T<(&p7>Qd^;R1hn1Y~jN~tApaL8>##U zd65bo8)79CplWxr#z4!6HvLz&N7_5AN#x;kLG?zQ(#p|lj<8VUlKY=Aw!ATqeL-VG z42gA!^cMNPj>(`ZMEbCrnkg*QTsn*u(nQPWI9pA{MQ=IsPTzd7q5E#7+z>Ch=fx$~ z;J|?(5jTo5UWGvsJa(Sx0?S#56+8SD!I^tftyeh_{5_31l6&Hywtn`bbqYDqGZXI( zCG7hBgvksX2ak8+)hB4jnxlO@A32C_RM&g&qDSb~3kM&)@A_j1*oTO@nicGUyv+%^ z=vB)4(q!ykzT==Z)3*3{atJ5}2PV*?Uw+HhN&+RvKvZL3p9E?gHjv{6zM!A|z|UHK z-r6jeLxbGn0D@q5aBzlco|nG2tr}N@m;CJX(4#Cn&p&sLKwzLFx1A5izu?X_X4x8r@K*d~7>t1~ zDW1Mv5O&WOxbzFC`DQ6yNJ(^u9vJdj$fl2dq`!Yba_0^vQHXV)vqv1gssZYzBct!j zHr9>ydtM8wIs}HI4=E}qAkv|BPWzh3^_yLH(|kdb?x56^BlDC)diWyPd*|f!`^12_U>TD^^94OCN0lVv~Sgvs94ecpE^}VY$w`qr_>Ue zTfH~;C<3H<0dS5Rkf_f@1x$Gms}gK#&k()IC0zb^QbR!YLoll)c$Agfi6MKI0dP_L z=Uou&u~~^2onea2%XZ@>`0x^L8CK6=I{ge;|HXMj)-@o~h&O{CuuwBX8pVqjJ*o}5 z#8&oF_p=uSo~8vn?R0!AMWvcbZmsrj{ZswRt(aEdbi~;HeVqIe)-6*1L%5u$Gbs}| zjFh?KL&U(rC2izSGtwP5FnsR@6$-1toz?RvLD^k~h9NfZgzHE7m!!7s6(;)RKo2z} zB$Ci@h({l?arO+vF;s35h=|WpefaOtKVx>l399}EsX@Oe3>>4MPy%h&^3N_`UTAHJ zI$u(|TYC~E4)|JwkWW3F!Tib=NzjHs5ii2uj0^m|Qlh-2VnB#+X~RZ|`SA*}}&8j9IDv?F;(Y^1=Z0?wWz;ikB zewU>MAXDi~O7a~?jx1x=&8GcR-fTp>{2Q`7#BE#N6D@FCp`?ht-<1|y(NArxE_WIu zP+GuG=Qq>SHWtS2M>34xwEw^uvo4|9)4s|Ac=ud?nHQ>ax@LvBqusFcjH0}{T3ZPQ zLO1l<@B_d-(IS682}5KA&qT1+{3jxKolW+1zL4inqBS-D>BohA!K5++41tM@ z@xe<-qz27}LnV#5lk&iC40M||JRmZ*A##K3+!j93eouU8@q-`W0r%7N`V$cR&JV;iX(@cS{#*5Q>~4BEDA)EikLSP@>Oo&Bt1Z~&0d5)COI%3$cLB_M?dK# z{yv2OqW!al-#AEs&QFd;WL5zCcp)JmCKJEdNsJlL9K@MnPegK23?G|O%v`@N{rIRa zi^7a}WBCD77@VQ-z_v{ZdRsWYrYgC$<^gRQwMCi6);%R~uIi31OMS}=gUTE(GKmCI z$zM>mytL{uNN+a&S38^ez(UT=iSw=l2f+a4)DyCA1Cs_N-r?Q@$3KTYosY!;pzQ0k zzh1G|kWCJjc(oZVBji@kN%)UBw(s{KaYGy=i{g3{)Z+&H8t2`^IuLLKWT6lL<-C(! zSF9K4xd-|VO;4}$s?Z7J_dYqD#Mt)WCDnsR{Kpjq275uUq6`v0y*!PHyS(}Zmv)_{>Vose9-$h8P0|y;YG)Bo}$(3Z%+Gs0RBmFiW!^5tBmDK-g zfe5%B*27ib+7|A*Fx5e)2%kIxh7xWoc3pZcXS2zik!63lAG1;sC1ja>BqH7D zODdi5lKW$$AFvxgC-l-)!c+9@YMC7a`w?G(P#MeEQ5xID#<}W$3bSmJ`8V*x2^3qz zVe<^^_8GHqYGF$nIQm0Xq2kAgYtm#UC1A(=&85w;rmg#v906 zT;RyMgbMpYOmS&S9c38^40oUp?!}#_84`aEVw;T;r%gTZkWeU;;FwM@0y0adt{-OK z(vGnPSlR=Nv2OUN!2=xazlnHPM9EWxXg2EKf0kI{iQb#FoP>xCB<)QY>OAM$Dcdbm zU6dU|%Mo(~avBYSjRc13@|s>axhrPl@Sr81{RSZUdz4(=|82XEbV*JAX6Lfbgqgz584lYgi0 z2-E{0XCVON$wHfvaLs;=dqhQJ&6aLn$D#0i(FkAVrXG9LGm3pSTf&f~RQb6|1_;W> z?n-;&hrq*~L=(;u#jS`*Yvh@3hU-33y_Kv1nxqrsf>pHVF&|OKkoC)4DWK%I!yq?P z=vXo8*_1iEWo8xCa{HJ4tzxOmqS0&$q+>LroMKI*V-rxhOc%3Y!)Y|N6p4PLE>Yek>Y(^KRECg8<|%g*nQib_Yc#A5q8Io z6Ig&V>k|~>B6KE%h4reAo*DfOH)_01tE0nWOxX0*YTJgyw7moaI^7gW*WBAeiLbD?FV9GSB zPv3`SX*^GRBM;zledO`!EbdBO_J@fEy)B{-XUTVQv}Qf~PSDpK9+@I`7G7|>Dgbbu z_7sX9%spVo$%qwRwgzq7!_N;#Td08m5HV#?^dF-EV1o)Q=Oa+rs2xH#g;ykLbwtCh znUnA^dW!XjspJ;otq$yV@I^s9Up(5k7rqhQd@OLMyyxVLj_+$#Vc*}Usevp^I(^vH zmDgHc0VMme|K&X?9&lkN{yq_(If)O`oUPW8X}1R5pSVBpfJe0t{sPA(F#`eONTh_) zxeLqHMfJX#?P(@6w4CqRE@Eiza; z;^5)Kk=^5)KDvd9Q<`=sJU8rjjxPmtWMTmzcH={o$U)j=QBuHarp?=}c??!`3d=H$nrJMyr3L-& zA#m?t(NqLM?I3mGgWA_C+0}BWy3-Gj7bR+d+U?n*mN$%5P`ugrB{PeV>jDUn;eVc- zzeMB1mI4?fVJatrNyq|+zn=!AiN~<}eoM#4uSx^K?Iw>P2*r=k`$<3kT00BE_1c(02MRz4(Hq`L^M&xt!pV2 zn+#U3@j~PUR>xIy+P>51iPayk-mqIK_5rlQMSe5&tDkKJk_$i(X&;K(11YGpEc-K= zq4Ln%^j>Zi_+Ae9eYEq_<`D+ddb8_aY!N;)(&EHFAk@Ekg&41ABmOXfWTo)Z&KotA zh*jgDGFYQ^y=m)<_LCWB+v48DTJw*5dwMm_YP0*_{@HANValf?kV-Ic3xsC}#x2h8 z`q5}d8IRmqWk%gR)s~M}(Qas5+`np^jW^oEd-pzERRPMXj$kS17g?H#4^trtKtq;C?;c ztd|%|WP2w2Nzg@)^V}!Gv++QF2!@FP9~DFVISRW6S?eP{H;;8EH;{>X_}NGj^0cg@ z!2@A>-CTcoN02^r6@c~^QUa={0xwK0v4i-tQ9wQq^=q*-{;zJ{Qe%7Qd!&X2>rV@4 z&wznCz*63_vw4>ZF8~%QCM?=vfzW0r_4O^>UA@otm_!N%mH)!ERy&b!n3*E*@?9d^ zu}s^By@FAhG(%?xgJMuMzuJw2&@$-oK>n z=UF}rt%vuaP9fzIFCYN-1&b#r^Cl6RDFIWsEsM|ROf`E?O(cy{BPO2Ie~kT+^kI^i zp>Kbc@C?}3vy-$ZFVX#-cx)Xj&G^ibX{pWggtr(%^?HeQL@Z( zM-430g<{>vT*)jK4aY9(a{lSy{8vxLbP~n1MXwM527ne#SHCC^F_2@o`>c>>KCq9c(4c$VSyMl*y3Nq1s+!DF| z^?d9PipQN(mw^j~{wJ^VOXDCaL$UtwwTpyv8IAwGOg<|NSghkAR1GSNLZ1JwdGJYm zP}t<=5=sNNUEjc=g(y)1n5)ynX(_$1-uGuDR*6Y^Wgg(LT)Jp><5X|}bt z_qMa&QP?l_n+iVS>v%s2Li_;AIeC=Ca^v1jX4*gvB$?H?2%ndnqOaK5-J%7a} zIF{qYa&NfVY}(fmS0OmXA70{znljBOiv5Yod!vFU{D~*3B3Ka{P8?^ zfhlF6o7aNT$qi8(w<}OPw5fqA7HUje*r*Oa(YV%*l0|9FP9KW@U&{VSW{&b0?@y)M zs%4k1Ax;TGYuZ9l;vP5@?3oQsp3)rjBeBvQQ>^B;z5pc=(yHhHtq6|0m(h4envn_j787fizY@V`o(!SSyE7vlMT zbo=Z1c=atz*G!kwzGB;*uPL$Ei|EbZLh8o+1BUMOpnU(uX&OG1MV@|!&HOOeU#t^x zr9=w2ow!SsTuJWT7%Wmt14U_M*3XiWBWHxqCVZI0_g0`}*^&yEG9RK9fHK8e+S^m? zfCNn$JTswUVbiC#>|=wS{t>-MI1aYPLtzO5y|LJ9nm>L6*wpr_m!)A2Fb1RceX&*|5|MwrvOk4+!0p99B9AgP*9D{Yt|x=X}O% zgIG$MrTB=n-!q%ROT|SzH#A$Xm;|ym)0>1KR}Yl0hr-KO&qMrV+0Ej3d@?FcgZ+B3 ztEk16g#2)@x=(ko8k7^Tq$*5pfZHC@O@}`SmzT1(V@x&NkZNM2F#Q-Go7-uf_zKC( zB(lHZ=3@dHaCOf6C!6i8rDL%~XM@rVTJbZL09?ht@r^Z_6x}}atLjvH^4Vk#Ibf(^LiBJFqorm?A=lE zzFmwvp4bT@Nv2V>YQT92X;t9<2s|Ru5#w?wCvlhcHLcsq0TaFLKy(?nzezJ>CECqj zggrI~Hd4LudM(m{L@ezfnpELsRFVFw>fx;CqZtie`$BXRn#Ns%AdoE$-Pf~{9A8rV zf7FbgpKmVzmvn-z(g+&+-ID=v`;6=)itq8oM*+Uz**SMm_{%eP_c0{<%1JGiZS19o z@Gj7$Se~0lsu}w!%;L%~mIAO;AY-2i`9A*ZfFs=X!LTd6nWOZ7BZH2M{l2*I>Xu)0 z`<=;ObglnXcVk!T>e$H?El}ra0WmPZ$YAN0#$?|1v26^(quQre8;k20*dpd4N{i=b zuN=y}_ew9SlE~R{2+Rh^7%PA1H5X(p8%0TpJ=cqa$65XL)$#ign-y!qij3;2>j}I; ziO@O|aYfn&up5F`YtjGw68rD3{OSGNYmBnl?zdwY$=RFsegTZ=kkzRQ`r7ZjQP!H( zp4>)&zf<*N!tI00xzm-ME_a{_I!TbDCr;8E;kCH4LlL-tqLxDuBn-+xgPk37S&S2^ z2QZumkIimwz!c@!r0)j3*(jPIs*V!iLTRl0Cpt_UVNUgGZzdvs0(-yUghJfKr7;=h zD~y?OJ-bWJg;VdZ^r@vlDoeGV&8^--!t1AsIMZ5S440HCVr%uk- z2wV>!W1WCvFB~p$P$$_}|H5>uBeAe>`N1FI8AxM|pq%oNs;ED8x+tb44E) zTj{^fbh@eLi%5AqT?;d>Es5D*Fi{Bpk)q$^iF!!U`r2hHAO_?#!aYmf>G+jHsES4W zgpTKY59d?hsb~F0WE&dUp6lPt;Pm zcbTUqRryw^%{ViNW%Z(o8}dd00H(H-MmQmOiTq{}_rnwOr*Ybo7*}3W-qBT!#s0Ie z-s<1rvvJx_W;ViUD`04%1pra*Yw0BcGe)fDKUK8aF#BwBwMPU;9`!6E(~!043?SZx z13K%z@$$#2%2ovVlgFIPp7Q6(vO)ud)=*%ZSucL2Dh~K4B|%q4KnSpj#n@(0B})!9 z8p*hY@5)NDn^&Pmo;|!>erSYg`LkO?0FB@PLqRvc>4IsUM5O&>rRv|IBRxi(RX(gJ ztQ2;??L~&Mv;aVr5Q@(?y^DGo%pO^~zijld41aA0KKsy_6FeHIn?fNHP-z>$OoWer zjZ5hFQTy*-f7KENRiCE$ZOp4|+Wah|2=n@|W=o}bFM}Y@0e62+_|#fND5cwa3;P{^pEzlJbF1Yq^}>=wy8^^^$I2M_MH(4Dw{F6hm+vrWV5!q;oX z;tTNhz5`-V={ew|bD$?qcF^WPR{L(E%~XG8eJx(DoGzt2G{l8r!QPJ>kpHeOvCv#w zr=SSwMDaUX^*~v%6K%O~i)<^6`{go>a3IdfZ8hFmz&;Y@P%ZygShQZ2DSHd`m5AR= zx$wWU06;GYwXOf(%MFyj{8rPFXD};JCe85Bdp4$YJ2$TzZ7Gr#+SwCvBI1o$QP0(c zy`P51FEBV2HTisM3bHqpmECT@H!Y2-bv2*SoSPoO?wLe{M#zDTy@ujAZ!Izzky~3k zRA1RQIIoC*Mej1PH!sUgtkR0VCNMX(_!b65mo66iM*KQ7xT8t2eev$v#&YdUXKwGm z7okYAqYF&bveHeu6M5p9xheRCTiU8PFeb1_Rht0VVSbm%|1cOVobc8mvqcw!RjrMRM#~=7xibH&Fa5Imc|lZ{eC|R__)OrFg4@X_ ze+kk*_sDNG5^ELmHnZ7Ue?)#6!O)#Nv*Dl2mr#2)w{#i-;}0*_h4A%HidnmclH#;Q zmQbq+P4DS%3}PpPm7K_K3d2s#k~x+PlTul7+kIKol0@`YN1NG=+&PYTS->AdzPv!> zQvzT=)9se*Jr1Yq+C{wbK82gAX`NkbXFZ)4==j4t51{|-v!!$H8@WKA={d>CWRW+g z*`L>9rRucS`vbXu0rzA1#AQ(W?6)}1+oJSF=80Kf_2r~Qm-EJ6bbB3k`80rCv(0d` zvCf3;L2ovYG_TES%6vSuoKfIHC6w;V31!oqHM8-I8AFzcd^+_86!EcCOX|Ta9k1!s z_Vh(EGIIsI3fb&dF$9V8v(sTBC%!#<&KIGF;R+;MyC0~}$gC}}= zR`DbUVc&Bx`lYykFZ4{R{xRaUQkWCGCQlEc;!mf=+nOk$RUg*7 z;kP7CVLEc$CA7@6VFpsp3_t~m)W0aPxjsA3e5U%SfY{tp5BV5jH-5n?YX7*+U+Zs%LGR>U- z!x4Y_|4{gx?ZPJobISy991O znrmrC3otC;#4^&Rg_iK}XH(XX+eUHN0@Oe06hJk}F?`$)KmH^eWz@@N%wEc)%>?Ft z#9QAroDeyfztQ5Qe{m*#R#T%-h*&XvSEn@N$hYRTCMXS|EPwzF3IIysD2waj`vQD{ zv_#^Pgr?s~I*NE=acf@dWVRNWTr(GN0wrL)Z2=`Dr>}&ZDNX|+^Anl{Di%v1Id$_p zK5_H5`RDjJx`BW7hc85|> zHMMsWJ4KTMRHGu+vy*kBEMjz*^K8VtU=bXJYdhdZ-?jTXa$&n)C?QQIZ7ln$qbGlr zS*TYE+ppOrI@AoPP=VI-OXm}FzgXRL)OPvR$a_=SsC<3Jb+>5makX|U!}3lx4tX&L z^C<{9TggZNoeX!P1jX_K5HkEVnQ#s2&c#umzV6s2U-Q;({l+j^?hi7JnQ7&&*oOy9 z(|0asVTWUCiCnjcOnB2pN0DpuTglKq;&SFOQ3pUdye*eT<2()7WKbXp1qq9=bhMWlF-7BHT|i3TEIT77AcjD(v=I207wi-=vyiw5mxgPdTVUC z&h^FEUrXwWs9en2C{ywZp;nvS(Mb$8sBEh-*_d-OEm%~p1b2EpcwUdf<~zmJmaSTO zSX&&GGCEz-M^)G$fBvLC2q@wM$;n4jp+mt0MJFLuJ%c`tSp8$xuP|G81GEd2ci$|M z4XmH{5$j?rqDWoL4vs!}W&!?!rtj=6WKJcE>)?NVske(p;|#>vL|M_$as=mi-n-()a*OU3Okmk0wC<9y7t^D(er-&jEEak2!NnDiOQ99Wx8{S8}=Ng!e0tzj*#T)+%7;aM$ z&H}|o|J1p{IK0Q7JggAwipvHvko6>Epmh4RFRUr}$*2K4dz85o7|3#Bec9SQ4Y*;> zXWjT~f+d)dp_J`sV*!w>B%)#GI_;USp7?0810&3S=WntGZ)+tzhZ+!|=XlQ&@G@~3 z-dw@I1>9n1{+!x^Hz|xC+P#Ab`E@=vY?3%Bc!Po~e&&&)Qp85!I|U<-fCXy*wMa&t zgDk!l;gk;$taOCV$&60z+}_$ykz=Ea*)wJQ3-M|p*EK(cvtIre0Pta~(95J7zoxBN zS(yE^3?>88AL0Wfuou$BM{lR1hkrRibz=+I9ccwd`ZC*{NNqL)3pCcw^ygMmrG^Yp zn5f}Xf>%gncC=Yq96;rnfp4FQL#{!Y*->e82rHgY4Zwy{`JH}b9*qr^VA{%~Z}jtp z_t$PlS6}5{NtTqXHN?uI8ut8rOaD#F1C^ls73S=b_yI#iZDOGz3#^L@YheGd>L;<( z)U=iYj;`{>VDNzIxcjbTk-X3keXR8Xbc`A$o5# zKGSk-7YcoBYuAFFSCjGi;7b<;n-*`USs)IX z=0q6WZ=L!)PkYtZE-6)azhXV|+?IVGTOmMCHjhkBjfy@k1>?yFO3u!)@cl{fFAXnRYsWk)kpT?X{_$J=|?g@Q}+kFw|%n!;Zo}|HE@j=SFMvT8v`6Y zNO;tXN^036nOB2%=KzxB?n~NQ1K8IO*UE{;Xy;N^ZNI#P+hRZOaHATz9(=)w=QwV# z`z3+P>9b?l-@$@P3<;w@O1BdKh+H;jo#_%rr!ute{|YX4g5}n?O7Mq^01S5;+lABE+7`&_?mR_z7k|Ja#8h{!~j)| zbBX;*fsbUak_!kXU%HfJ2J+G7;inu#uRjMb|8a){=^))y236LDZ$$q3LRlat1D)%7K0!q5hT5V1j3qHc7MG9 z_)Q=yQ>rs>3%l=vu$#VVd$&IgO}Za#?aN!xY>-<3PhzS&q!N<=1Q7VJBfHjug^4|) z*fW^;%3}P7X#W3d;tUs3;`O&>;NKZBMR8au6>7?QriJ@gBaorz-+`pUWOP73DJL=M z(33uT6Gz@Sv40F6bN|H=lpcO z^AJl}&=TIjdevuDQ!w0K*6oZ2JBOhb31q!XDArFyKpz!I$p4|;c}@^bX{>AXdt7Bm zaLTk?c%h@%xq02reu~;t@$bv`b3i(P=g}~ywgSFpM;}b$zAD+=I!7`V~}ARB(Wx0C(EAq@?GuxOL9X+ffbkn3+Op0*80TqmpAq~EXmv%cq36celXmRz z%0(!oMp&2?`W)ALA&#|fu)MFp{V~~zIIixOxY^YtO5^FSox8v$#d0*{qk0Z)pNTt0QVZ^$`4vImEB>;Lo2!7K05TpY-sl#sWBz_W-aDIV`Ksabi zvpa#93Svo!70W*Ydh)Qzm{0?CU`y;T^ITg-J9nfWeZ-sbw)G@W?$Eomf%Bg2frfh5 zRm1{|E0+(4zXy){$}uC3%Y-mSA2-^I>Tw|gQx|7TDli_hB>``)Q^aZ`LJC2V3U$SABP}T)%}9g2pF9dT}aC~!rFFgkl1J$ z`^z{Arn3On-m%}r}TGF8KQe*OjSJ=T|caa_E;v89A{t@$yT^(G9=N9F?^kT*#s3qhJq!IH5|AhnqFd z0B&^gm3w;YbMNUKU>naBAO@fbz zqw=n!@--}o5;k6DvTW9pw)IJVz;X}ncbPVrmH>4x);8cx;q3UyiML1PWp%bxSiS|^ zC5!kc4qw%NSOGQ*Kcd#&$30=lDvs#*4W4q0u8E02U)7d=!W7+NouEyuF1dyH$D@G& zaFaxo9Ex|ZXA5y{eZT*i*dP~INSMAi@mvEX@q5i<&o&#sM}Df?Og8n8Ku4vOux=T% zeuw~z1hR}ZNwTn8KsQHKLwe2>p^K`YWUJEdVEl|mO21Bov!D0D$qPoOv=vJJ`)|%_ z>l%`eexY7t{BlVKP!`a^U@nM?#9OC*t76My_E_<16vCz1x_#82qj2PkWiMWgF8bM9 z(1t4VdHcJ;B~;Q%x01k_gQ0>u2*OjuEWNOGX#4}+N?Gb5;+NQMqp}Puqw2HnkYuKA zzKFWGHc&K>gwVgI1Sc9OT1s6fq=>$gZU!!xsilA$fF`kLdGoX*^t}ao@+^WBpk>`8 z4v_~gK|c2rCq#DZ+H)$3v~Hoi=)=1D==e3P zpKrRQ+>O^cyTuWJ%2}__0Z9SM_z9rptd*;-9uC1tDw4+A!=+K%8~M&+Zk#13hY$Y$ zo-8$*8dD5@}XDi19RjK6T^J~DIXbF5w&l?JLHMrf0 zLv0{7*G!==o|B%$V!a=EtVHdMwXLtmO~vl}P6;S(R2Q>*kTJK~!}gloxj)m|_LYK{ zl(f1cB=EON&wVFwK?MGn^nWuh@f95SHatPs(jcwSY#Dnl1@_gkOJ5=f`%s$ZHljRH0 z+c%lrb=Gi&N&1>^L_}#m>=U=(oT^vTA&3!xXNyqi$pdW1BDJ#^{h|2tZc{t^vag3& zAD7*8C`chNF|27itjBUo^CCDyEpJLX3&u+(L;YeeMwnXEoyN(ytoEabcl$lSgx~Ltatn}b$@j_yyMrBb03)shJE*$;Mw=;mZd&8e>IzE+4WIoH zCSZE7WthNUL$|Y#m!Hn?x7V1CK}V`KwW2D$-7&ODy5Cj;!_tTOOo1Mm%(RUt)#$@3 zhurA)t<7qik%%1Et+N1?R#hdBB#LdQ7{%-C zn$(`5e0eFh(#c*hvF>WT*07fk$N_631?W>kfjySN8^XC9diiOd#s?4tybICF;wBjp zIPzilX3{j%4u7blhq)tnaOBZ_`h_JqHXuI7SuIlNTgBk9{HIS&3|SEPfrvcE<@}E` zKk$y*nzsqZ{J{uWW9;#n=de&&h>m#A#q)#zRonr(?mDOYU&h&aQWD;?Z(22wY?t$U3qo`?{+amA$^TkxL+Ex2dh`q7iR&TPd0Ymwzo#b? zP$#t=elB5?k$#uE$K>C$YZbYUX_JgnXA`oF_Ifz4H7LEOW~{Gww&3s=wH4+j8*TU| zSX%LtJWqhr-xGNSe{;(16kxnak6RnZ{0qZ^kJI5X*It_YuynSpi(^-}Lolr{)#z_~ zw!(J-8%7Ybo^c3(mED`Xz8xecP35a6M8HarxRn%+NJBE;dw>>Y2T&;jzRd4FSDO3T zt*y+zXCtZQ0bP0yf6HRpD|WmzP;DR^-g^}{z~0x~z4j8m zucTe%k&S9Nt-?Jb^gYW1w6!Y3AUZ0Jcq;pJ)Exz%7k+mUOm6%ApjjSmflfKwBo6`B zhNb@$NHTJ>guaj9S{@DX)!6)b-Shav=DNKWy(V00k(D!v?PAR0f0vDNq*#mYmUp6> z76KxbFDw5U{{qx{BRj(>?|C`82ICKbfLxoldov-M?4Xl+3;I4GzLHyPOzYw7{WQST zPNYcx5onA%MAO9??41Po*1zW(Y%Zzn06-lUp{s<3!_9vv9HBjT02On0Hf$}NP;wF) zP<`2p3}A^~1YbvOh{ePMx$!JGUPX-tbBzp3mDZMY;}h;sQ->!p97GA)9a|tF(Gh{1$xk7 zUw?ELkT({Xw!KIr);kTRb1b|UL`r2_`a+&UFVCdJ)1T#fdh;71EQl9790Br0m_`$x z9|ZANuchFci8GNZ{XbP=+uXSJRe(;V5laQz$u18#?X*9}x7cIEbnr%<=1cX3EIu7$ zhHW6pe5M(&qEtsqRa>?)*{O;OJT+YUhG5{km|YI7I@JL_3Hwao9aXneiSA~a* z|Lp@c-oMNyeAEuUz{F?kuou3x#C*gU?lon!RC1s37gW^0Frc`lqQWH&(J4NoZg3m8 z;Lin#8Q+cFPD7MCzj}#|ws7b@?D9Q4dVjS4dpco=4yX5SSH=A@U@yqPdp@?g?qeia zH=Tt_9)G=6C2QIPsi-QipnK(mc0xXIN;j$WLf@n8eYvMk;*H-Q4tK%(3$CN}NGgO8n}fD~+>?<3UzvsrMf*J~%i;VKQHbF%TPalFi=#sgj)(P#SM^0Q=Tr>4kJVw8X3iWsP|e8tj}NjlMdWp z@2+M4HQu~3!=bZpjh;;DIDk&X}=c8~kn)FWWH z2KL1w^rA5&1@@^X%MjZ7;u(kH=YhH2pJPFQe=hn>tZd5RC5cfGYis8s9PKaxi*}-s6*W zRA^PwR=y^5Z){!(4D9-KC;0~;b*ploznFOaU`bJ_7U?qAi#mTo!&rIECRL$_y@yI27x2?W+zqDBD5~KCVYKFZLK+>ABC(Kj zeAll)KMgIlAG`r^rS{loBrGLtzhHY8$)<_S<(Dpkr(Ym@@vnQ&rS@FC*>2@XCH}M+an74WcRDcoQ+a3@A z9tYhl5$z7bMdTvD2r&jztBuo37?*k~wcU9GK2-)MTFS-lux-mIRYUuGUCI~V$?s#< z?1qAWb(?ZLm(N>%S%y10COdaq_Tm5c^%ooIxpR=`3e4C|@O5wY+eLik&XVi5oT7oe zmxH)Jd*5eo@!7t`x8!K=-+zJ-Sz)B_V$)s1pW~CDU$=q^&ABvf6S|?TOMB-RIm@CoFg>mjIQE)?+A1_3s6zmFU_oW&BqyMz1mY*IcP_2knjq5 zqw~JK(cVsmzc7*EvTT2rvpeqhg)W=%TOZ^>f`rD4|7Z5fq*2D^lpCttIg#ictgqZ$P@ru6P#f$x#KfnfTZj~LG6U_d-kE~`;kU_X)`H5so@?C zWmb!7x|xk@0L~0JFall*@ltyiL^)@3m4MqC7(7H0sH!WidId1#f#6R{Q&A!XzO1IAcIx;$k66dumt6lpUw@nL2MvqJ5^kbOVZ<^2jt5-njy|2@`07}0w z;M%I1$FCoLy`8xp8Tk)bFr;7aJeQ9KK6p=O$U0-&JYYy8woV*>b+FB?xLX`=pirYM z5K$BA(u)+jR{?O2r$c_Qvl?M{=Ar{yQ!UVsVn4k@0!b?_lA;dVz9uaQUgBH8Oz(Sb zrEs;&Ey>_ex8&!N{PmQjp+-Hlh|OA&wvDai#GpU=^-B70V0*LF=^bi+Nhe_o|azZ%~ZZ1$}LTmWt4aoB1 zPgccm$EwYU+jrdBaQFxQfn5gd(gM`Y*Ro1n&Zi?j=(>T3kmf94vdhf?AuS8>$Va#P zGL5F+VHpxdsCUa}+RqavXCobI-@B;WJbMphpK2%6t=XvKWWE|ruvREgM+|V=i6;;O zx$g=7^`$XWn0fu!gF=Xe9cMB8Z_SelD>&o&{1XFS`|nInK3BXlaeD*rc;R-#osyIS zWv&>~^TLIyBB6oDX+#>3<_0+2C4u2zK^wmHXXDD9_)kmLYJ!0SzM|%G9{pi)`X$uf zW}|%%#LgyK7m(4{V&?x_0KEDq56tk|0YNY~B(Sr|>WVz-pO3A##}$JCT}5P7DY+@W z#gJv>pA5>$|E3WO2tV7G^SuymB?tY`ooKcN3!vaQMnBNk-WATF{-$#}FyzgtJ8M^; zUK6KWSG)}6**+rZ&?o@PK3??uN{Q)#+bDP9i1W&j)oaU5d0bIWJ_9T5ac!qc?x66Q z$KUSZ`nYY94qfN_dpTFr8OW~A?}LD;Yty-BA)-be5Z3S#t2Io%q+cAbnGj1t$|qFR z9o?8B7OA^KjCYL=-!p}w(dkC^G6Nd%_I=1))PC0w5}ZZGJxfK)jP4Fwa@b-SYBw?% zdz9B-<`*B2dOn(N;mcTm%Do)rIvfXRNFX&1h`?>Rzuj~Wx)$p13nrDlS8-jwq@e@n zNIj_|8or==8~1h*Ih?w*8K7rYkGlwlTWAwLKc5}~dfz3y`kM&^Q|@C%1VAp_$wnw6zG~W4O+^ z>i?NY?oXf^Puc~+fDM$VgRNBpOZj{2cMP~gCqWAX4 z7>%$ux8@a&_B(pt``KSt;r+sR-$N;jdpY>|pyvPiN)9ohd*>mVST3wMo)){`B(&eX z1?zZJ-4u9NZ|~j1rdZYq4R$?swf}<6(#ex%7r{kh%U@kT)&kWuAszS%oJts=*OcL9 zaZwK<5DZw%1IFHXgFplP6JiL^dk8+SgM$D?8X+gE4172hXh!WeqIO>}$I9?Nry$*S zQ#f)RuH{P7RwA3v9f<-w>{PSzom;>(i&^l{E0(&Xp4A-*q-@{W1oE3K;1zb{&n28dSC2$N+6auXe0}e4b z)KLJ?5c*>@9K#I^)W;uU_Z`enquTUxr>mNq z1{0_puF-M7j${rs!dxxo3EelGodF1TvjV;Zpo;s{5f1pyCuRp=HDZ?s#IA4f?h|-p zGd|Mq^4hDa@Bh!c4ZE?O&x&XZ_ptZGYK4$9F4~{%R!}G1leCBx`dtNUS|K zL-7J5s4W@%mhXg1!}a4PD%!t&Qn%f_oquRajn3@C*)`o&K9o7V6DwzVMEhjVdDJ1fjhr#@=lp#@4EBqi=CCQ>73>R(>QKPNM&_Jpe5G`n4wegeC`FYEPJ{|vwS>$-`fuRSp3927qOv|NC3T3G-0 zA{K`|+tQy1yqE$ShWt8ny&5~)%ITb@^+x$w0)f&om;P8B)@}=Wzy59BwUfZ1vqw87 za2lB8J(&*l#(V}Id8SyQ0C(2amzkz3EqG&Ed0Jq1)$|&>4_|NIe=5|n=3?siFV0fI z{As5DLW^gs|B-b4C;Hd(SM-S~GQhzb>HgF2|2Usww0nL^;x@1eaB)=+Clj+$fF@H( z-fqP??~QMT$KI-#m;QC*&6vkp&8699G3)Bq0*kFZXINw=b9OVaed(3(3kS|IZ)CM? zJdnW&%t8MveBuK21uiYj)_a{Fnw0OErMzMN?d$QoPwkhOwcP&p+t>P)4tHlYw-pPN z^oJ=uc$Sl>pv@fZH~ZqxSvdhF@F1s=oZawpr^-#l{IIOGG=T%QXjtwPhIg-F@k@uIlr?J->Ia zpEUQ*=4g|XYn4Gez&aHr*;t$u3oODPmc2Ku)2Og|xjc%w;q!Zz+zY)*3{7V8bK4;& zYV82FZ+8?v)`J|G1w4I0fWdKg|2b#iaazCv;|?(W-q}$o&Y}Q5d@BRk^jL7#{kbCK zSgkyu;=DV+or2)AxCBgq-nj5=@n^`%T#V+xBGEkW4lCqrE)LMv#f;AvD__cQ@Eg3`~x| zW+h9mofSXCq5|M)9|ez(#X?-sxB%Go8};sJ?2abp(Y!lyi>k)|{M*Z$c{e1-K4ky` MPgg&ebxsLQ025IeI{*Lx diff --git a/web/icons/light-4x.png b/web/icons/light-4x.png new file mode 100644 index 0000000000000000000000000000000000000000..c3ea8bb0e7ba4f713faa63b98958cfa8d7709ccc GIT binary patch literal 15274 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 Date: Wed, 13 Aug 2025 16:05:05 +0700 Subject: [PATCH 08/13] Add a simple Balance widget --- android/app/src/main/AndroidManifest.xml | 10 +++++ .../com/friyn/tlist/FinanceWidgetProvider.kt | 31 ++++++++++++++ .../src/main/res/layout/finance_widget.xml | 40 +++++++++++++++++++ .../main/res/xml/finance_widget_provider.xml | 8 ++++ lib/main.dart | 35 ++++++++++++++++ macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 32 +++++++++++++++ pubspec.yaml | 1 + 8 files changed, 159 insertions(+) create mode 100644 android/app/src/main/kotlin/com/friyn/tlist/FinanceWidgetProvider.kt create mode 100644 android/app/src/main/res/layout/finance_widget.xml create mode 100644 android/app/src/main/res/xml/finance_widget_provider.xml diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 916806c..6d90ff4 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -44,6 +44,16 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..790bcaa --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + + TList + Quick add tasks and notes from home screen + diff --git a/android/app/src/main/res/xml/quick_add_widget_provider.xml b/android/app/src/main/res/xml/quick_add_widget_provider.xml new file mode 100644 index 0000000..90791ce --- /dev/null +++ b/android/app/src/main/res/xml/quick_add_widget_provider.xml @@ -0,0 +1,10 @@ + + diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart index 758f391..3dfa001 100644 --- a/lib/firebase_options.dart +++ b/lib/firebase_options.dart @@ -51,7 +51,8 @@ class DefaultFirebaseOptions { appId: '1:582262425557:web:bb5480d783f97b58029f5c', messagingSenderId: '582262425557', projectId: 'tlistserver', - authDomain: 'tlistserver.firebaseapp.com', + // authDomain: 'tlistserver.firebaseapp.com', + authDomain: 'list.novila.xyz', storageBucket: 'tlistserver.firebasestorage.app', measurementId: 'G-GL9ZWVCBD5', ); @@ -69,7 +70,8 @@ class DefaultFirebaseOptions { appId: '1:582262425557:web:a2fb9a11a3eb69e6029f5c', messagingSenderId: '582262425557', projectId: 'tlistserver', - authDomain: 'tlistserver.firebaseapp.com', + // authDomain: 'tlistserver.firebaseapp.com', + authDomain: 'list.novila.xyz', storageBucket: 'tlistserver.firebasestorage.app', measurementId: 'G-K1HVP6VG7G', ); diff --git a/lib/main.dart b/lib/main.dart index 4d8f782..1421fa7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,23 +1,84 @@ // main.dart +import 'dart:async'; +import 'dart:convert'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +import 'package:flutter/services.dart'; import 'package:home_widget/home_widget.dart'; +import 'package:intl/intl.dart'; +import 'package:shared_preferences/shared_preferences.dart'; -import 'dart:convert'; -import 'dart:async'; -import 'package:firebase_core/firebase_core.dart'; -import 'package:firebase_auth/firebase_auth.dart'; -import 'package:cloud_firestore/cloud_firestore.dart'; import 'firebase_options.dart'; +import 'page/user.dart'; import 'utils/app_update.dart'; -import 'package:tlist/page/user.dart'; +String _formatCurrency(double amount) { + final format = NumberFormat.currency(locale: 'id_ID', symbol: 'Rp ', decimalDigits: 0); + return format.format(amount); +} + +// Custom formatter untuk input angka dengan pemisah ribuan +class ThousandsSeparatorInputFormatter extends TextInputFormatter { + static const _separator = '.'; + + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, + TextEditingValue newValue, + ) { + // Hapus semua karakter non-digit + String digitsOnly = newValue.text.replaceAll(RegExp(r'[^\d]'), ''); + + if (digitsOnly.isEmpty) { + return const TextEditingValue(); + } + + // Format dengan pemisah ribuan + String formatted = _addThousandsSeparator(digitsOnly); + + return TextEditingValue( + text: formatted, + selection: TextSelection.collapsed(offset: formatted.length), + ); + } + + String _addThousandsSeparator(String value) { + if (value.length <= 3) return value; + + String result = ''; + int counter = 0; + + for (int i = value.length - 1; i >= 0; i--) { + if (counter == 3) { + result = _separator + result; + counter = 0; + } + result = value[i] + result; + counter++; + } + + return result; + } +} + +// Helper function untuk convert formatted text ke double +double _parseFormattedAmount(String formattedText) { + String digitsOnly = formattedText.replaceAll(RegExp(r'[^\d]'), ''); + return digitsOnly.isEmpty ? 0.0 : double.parse(digitsOnly); +} void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); + + // Initialize home widget + await HomeWidget.setAppGroupId('group.com.friyn.tlist'); + runApp(const MyApp()); } @@ -30,7 +91,48 @@ class MyApp extends StatelessWidget { debugShowCheckedModeBanner: false, title: 'TList', theme: ThemeData( - primarySwatch: Colors.blue, + brightness: Brightness.light, + primaryColor: const Color(0xFF128C7E), + colorScheme: ColorScheme.fromSeed( + seedColor: const Color(0xFF128C7E), + brightness: Brightness.light, + ), + scaffoldBackgroundColor: Colors.white, + appBarTheme: const AppBarTheme( + backgroundColor: Colors.white, + foregroundColor: Color(0xFF128C7E), + elevation: 1, + ), + cardTheme: CardThemeData( + elevation: 1, + color: Colors.grey[50], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + bottomNavigationBarTheme: BottomNavigationBarThemeData( + backgroundColor: Colors.white, + selectedItemColor: const Color(0xFF128C7E), + unselectedItemColor: Colors.grey[600], + elevation: 2, + ), + floatingActionButtonTheme: const FloatingActionButtonThemeData( + backgroundColor: Color(0xFF128C7E), + foregroundColor: Colors.white, + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: Colors.grey.shade100, + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: Color(0xFF128C7E), width: 2), + ), + ), visualDensity: VisualDensity.adaptivePlatformDensity, ), home: const MainScreen(), @@ -131,7 +233,7 @@ class Note { this.category = 'Personal', required this.createdAt, DateTime? updatedAt, - this.color = Colors.yellow, + this.color = const Color(0xFFFFF59D), // Warna default: light yellow }) : updatedAt = updatedAt ?? createdAt; Map toJson() { @@ -153,8 +255,10 @@ class Note { content: json['content'], category: json['category'] ?? 'Personal', createdAt: DateTime.fromMillisecondsSinceEpoch(json['createdAt']), - updatedAt: DateTime.fromMillisecondsSinceEpoch(json['updatedAt']), - color: Color(json['color'] ?? Colors.yellow.value), + updatedAt: json['updatedAt'] != null + ? DateTime.fromMillisecondsSinceEpoch(json['updatedAt']) + : DateTime.fromMillisecondsSinceEpoch(json['createdAt']), + color: json['color'] != null ? Color(json['color']) : const Color(0xFFFFF59D), // Default to light yellow ); } } @@ -377,6 +481,7 @@ class MainScreen extends StatefulWidget { class _MainScreenState extends State { int _currentIndex = 0; final TextEditingController _searchController = TextEditingController(); + late PageController _pageController; String _searchQuery = ''; bool _loginPromptShown = false; // Optional: URL ke manifest update (JSON) yang di-host di GitHub Pages/Releases. @@ -406,6 +511,7 @@ class _MainScreenState extends State { @override void initState() { super.initState(); + _pageController = PageController(initialPage: _currentIndex); // Listen to auth state changes and trigger global refresh (next frame, debounced) _authSub = FirebaseAuth.instance.authStateChanges().listen((_) { if (!mounted || _pendingAuthRefresh) return; @@ -560,7 +666,6 @@ class _MainScreenState extends State { borderRadius: BorderRadius.circular(25), ), filled: true, - fillColor: Colors.white, contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), ), onChanged: (value) { @@ -572,25 +677,30 @@ class _MainScreenState extends State { ), ), ), - body: IndexedStack( - index: _currentIndex, + body: PageView( + controller: _pageController, + onPageChanged: (index) { + setState(() { + _currentIndex = index; + _searchController.clear(); + _searchQuery = ''; + }); + }, children: [ TodoListScreen(key: _todoKey, searchQuery: _searchQuery, onGlobalRefresh: _refreshAll), NotesScreen(key: _notesKey, searchQuery: _searchQuery, onGlobalRefresh: _refreshAll), - FinanceScreen(key: _financeKey, searchQuery: _searchQuery, onGlobalRefresh: _refreshAll), // Screen baru + FinanceScreen(key: _financeKey, searchQuery: _searchQuery, onGlobalRefresh: _refreshAll), ], ), bottomNavigationBar: BottomNavigationBar( currentIndex: _currentIndex, type: BottomNavigationBarType.fixed, - selectedItemColor: const Color(0xFF128C7E), - unselectedItemColor: Colors.grey, onTap: (index) { - setState(() { - _currentIndex = index; - _searchController.clear(); - _searchQuery = ''; - }); + _pageController.animateToPage( + index, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); }, items: const [ BottomNavigationBarItem( @@ -617,6 +727,7 @@ class _MainScreenState extends State { void dispose() { _authSub?.cancel(); _searchController.dispose(); + _pageController.dispose(); super.dispose(); } } @@ -637,6 +748,7 @@ class _FinanceScreenState extends State { List incomeCategories = []; List expenseCategories = []; String selectedFilter = 'Semua'; // 'Semua', 'Pemasukan', 'Pengeluaran' + bool isLoading = true; double get _balance { double income = 0; @@ -661,13 +773,6 @@ class _FinanceScreenState extends State { } } - String _formatCurrency(double value) { - // Simple currency formatting without intl to avoid extra deps - final isNeg = value < 0; - final abs = value.abs(); - final s = abs.toStringAsFixed(2); - return (isNeg ? '-' : '') + 'Rp ' + s; - } @override void initState() { @@ -676,6 +781,10 @@ class _FinanceScreenState extends State { } Future _loadData() async { + setState(() { + isLoading = true; + }); + final loadedTransactions = await DataService.loadTransactions(); final loadedIncomeCategories = await DataService.loadIncomeCategories(); final loadedExpenseCategories = await DataService.loadExpenseCategories(); @@ -684,6 +793,7 @@ class _FinanceScreenState extends State { transactions = loadedTransactions; incomeCategories = loadedIncomeCategories; expenseCategories = loadedExpenseCategories; + isLoading = false; }); await _updateAndroidFinanceWidget(); } @@ -792,7 +902,7 @@ class _FinanceScreenState extends State { _saveTransactions(); Navigator.pop(context); }, - child: const Text('Hapus', style: TextStyle(color: Colors.red)), + child: Text('Hapus', style: TextStyle(color: Theme.of(context).colorScheme.error)), ), ], ), @@ -801,6 +911,21 @@ class _FinanceScreenState extends State { @override Widget build(BuildContext context) { + if (isLoading) { + return const Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('Memuat data keuangan...'), + ], + ), + ), + ); + } + return Scaffold( body: Column( children: [ @@ -812,8 +937,9 @@ class _FinanceScreenState extends State { Expanded( child: _buildSummaryCard( 'Pemasukan', - totalIncome, - Colors.green, + _formatCurrency(totalIncome), + Theme.of(context).colorScheme.primary.withOpacity(0.1), + Theme.of(context).colorScheme.primary, Icons.trending_up, ), ), @@ -821,8 +947,9 @@ class _FinanceScreenState extends State { Expanded( child: _buildSummaryCard( 'Saldo', - balance, - balance >= 0 ? Colors.green : Colors.red, + _formatCurrency(balance), + (balance >= 0 ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.error).withOpacity(0.1), + balance >= 0 ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.error, Icons.account_balance_wallet, ), ), @@ -830,8 +957,9 @@ class _FinanceScreenState extends State { Expanded( child: _buildSummaryCard( 'Pengeluaran', - totalExpense, - Colors.red, + _formatCurrency(totalExpense), + Theme.of(context).colorScheme.error.withOpacity(0.1), + Theme.of(context).colorScheme.error, Icons.trending_down, ), ), @@ -893,18 +1021,18 @@ class _FinanceScreenState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.receipt_outlined, size: 64, color: Colors.grey[400]), + Icon(Icons.receipt_outlined, size: 64, color: Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.5)), const SizedBox(height: 16), Text( widget.searchQuery.isNotEmpty ? 'Tidak ada transaksi yang cocok' : 'Belum ada transaksi', - style: TextStyle(fontSize: 18, color: Colors.grey[600]), + style: TextStyle(fontSize: 18, color: Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.7)), ), if (widget.searchQuery.isEmpty) Text( 'Tap + untuk menambah transaksi baru', - style: TextStyle(color: Colors.grey[500]), + style: TextStyle(color: Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.6)), ), ], ), @@ -936,7 +1064,7 @@ class _FinanceScreenState extends State { ); } - Widget _buildSummaryCard(String title, double amount, Color color, IconData icon) { + Widget _buildSummaryCard(String title, String amount, Color backgroundColor, Color color, IconData icon) { return Card( child: Padding( padding: const EdgeInsets.all(12.0), @@ -950,7 +1078,7 @@ class _FinanceScreenState extends State { ), const SizedBox(height: 4), Text( - 'Rp ${amount.toStringAsFixed(0).replaceAllMapped(RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]}.')}', + amount, style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, @@ -986,10 +1114,10 @@ class TransactionCard extends StatelessWidget { margin: const EdgeInsets.symmetric(vertical: 4.0), child: ListTile( leading: CircleAvatar( - backgroundColor: isIncome ? Colors.green.shade100 : Colors.red.shade100, + backgroundColor: isIncome ? Theme.of(context).colorScheme.primary.withOpacity(0.1) : Theme.of(context).colorScheme.error.withOpacity(0.1), child: Icon( isIncome ? Icons.trending_up : Icons.trending_down, - color: isIncome ? Colors.green : Colors.red, + color: isIncome ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.error, ), ), title: Text( @@ -1007,21 +1135,21 @@ class TransactionCard extends StatelessWidget { Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( - color: isIncome ? Colors.green.shade100 : Colors.red.shade100, + color: isIncome ? Theme.of(context).colorScheme.primary.withOpacity(0.1) : Theme.of(context).colorScheme.error.withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), child: Text( transaction.category, style: TextStyle( fontSize: 12, - color: isIncome ? Colors.green.shade700 : Colors.red.shade700, + color: isIncome ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.error, ), ), ), const Spacer(), Text( '${transaction.createdAt.day}/${transaction.createdAt.month}/${transaction.createdAt.year}', - style: const TextStyle(fontSize: 12, color: Colors.grey), + style: TextStyle(fontSize: 12, color: Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.6)), ), ], ), @@ -1035,10 +1163,10 @@ class TransactionCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( - '${isIncome ? '+' : '-'}Rp ${transaction.amount.toStringAsFixed(0).replaceAllMapped(RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]}.')}', + _formatCurrency(transaction.amount), style: TextStyle( fontWeight: FontWeight.bold, - color: isIncome ? Colors.green : Colors.red, + color: isIncome ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.error, fontSize: 14, ), ), @@ -1046,10 +1174,18 @@ class TransactionCard extends StatelessWidget { ), PopupMenuButton( icon: const Icon(Icons.more_vert), + onSelected: (String value) { + if (value == 'edit') { + // Defer to next microtask to ensure the popup has fully closed + Future.microtask(onEdit); + } else if (value == 'delete') { + Future.microtask(onDelete); + } + }, itemBuilder: (context) => [ - PopupMenuItem( - onTap: onEdit, - child: const Row( + const PopupMenuItem( + value: 'edit', + child: Row( children: [ Icon(Icons.edit, size: 20), SizedBox(width: 8), @@ -1058,12 +1194,12 @@ class TransactionCard extends StatelessWidget { ), ), PopupMenuItem( - onTap: onDelete, - child: const Row( + value: 'delete', + child: Row( children: [ - Icon(Icons.delete, color: Colors.red, size: 20), - SizedBox(width: 8), - Text('Hapus', style: TextStyle(color: Colors.red)), + Icon(Icons.delete, color: Theme.of(context).colorScheme.error, size: 20), + const SizedBox(width: 8), + Text('Hapus', style: TextStyle(color: Theme.of(context).colorScheme.error)), ], ), ), @@ -1124,8 +1260,8 @@ class _AddEditTransactionDialogState extends State { return; } - final amount = double.tryParse(_amountController.text.trim()); - if (amount == null || amount <= 0) { + final amount = _parseFormattedAmount(_amountController.text.trim()); + if (amount <= 0) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Jumlah harus berupa angka yang valid!')), ); @@ -1166,6 +1302,12 @@ class _AddEditTransactionDialogState extends State { if (widget.transaction == null) DropdownButtonFormField( value: _transactionType, + decoration: InputDecoration( + labelText: 'Jenis Transaksi', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(25), + ), + ), items: const [ DropdownMenuItem(child: Text('Pemasukan'), value: 'income'), DropdownMenuItem(child: Text('Pengeluaran'), value: 'expense'), @@ -1180,36 +1322,48 @@ class _AddEditTransactionDialogState extends State { const SizedBox(height: 16), TextField( controller: _titleController, - decoration: const InputDecoration( + decoration: InputDecoration( labelText: 'Judul Transaksi', - border: OutlineInputBorder(), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(25), + ), ), ), const SizedBox(height: 16), TextField( controller: _amountController, keyboardType: TextInputType.number, - decoration: const InputDecoration( + inputFormatters: [ + ThousandsSeparatorInputFormatter(), + ], + decoration: InputDecoration( labelText: 'Jumlah (Rp)', - border: OutlineInputBorder(), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(25), + ), prefixText: 'Rp ', + hintText: '1.000.000', ), ), const SizedBox(height: 16), TextField( controller: _descriptionController, - decoration: const InputDecoration( + decoration: InputDecoration( labelText: 'Deskripsi (opsional)', - border: OutlineInputBorder(), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(25), + ), ), maxLines: 3, ), const SizedBox(height: 16), DropdownButtonFormField( value: _selectedCategory, - decoration: const InputDecoration( + decoration: InputDecoration( labelText: 'Kategori', - border: OutlineInputBorder(), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(25), + ), ), items: categories.map((category) { return DropdownMenuItem( @@ -1264,6 +1418,7 @@ class _TodoListScreenState extends State { List tasks = []; List categories = []; String selectedCategory = 'Semua'; + bool isLoading = true; @override void initState() { @@ -1272,11 +1427,16 @@ class _TodoListScreenState extends State { } Future _loadData() async { + setState(() { + isLoading = true; + }); + final loadedTasks = await DataService.loadTasks(); final loadedCategories = await DataService.loadTaskCategories(); setState(() { tasks = loadedTasks; categories = ['Semua'] + loadedCategories; + isLoading = false; }); } @@ -1359,7 +1519,7 @@ class _TodoListScreenState extends State { _saveTasks(); Navigator.pop(context); }, - child: const Text('Hapus', style: TextStyle(color: Colors.red)), + child: Text('Hapus', style: TextStyle(color: Theme.of(context).colorScheme.error)), ), ], ), @@ -1396,6 +1556,21 @@ class _TodoListScreenState extends State { @override Widget build(BuildContext context) { + if (isLoading) { + return const Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('Memuat tasks...'), + ], + ), + ), + ); + } + return Scaffold( body: Column( children: [ @@ -1502,6 +1677,7 @@ class _NotesScreenState extends State { List notes = []; List categories = []; String selectedCategory = 'Semua'; + bool isLoading = true; bool isGridView = false; @override @@ -1511,11 +1687,16 @@ class _NotesScreenState extends State { } Future _loadData() async { + setState(() { + isLoading = true; + }); + final loadedNotes = await DataService.loadNotes(); final loadedCategories = await DataService.loadNoteCategories(); setState(() { notes = loadedNotes; categories = ['Semua'] + loadedCategories; + isLoading = false; }); } @@ -1598,7 +1779,7 @@ class _NotesScreenState extends State { _saveNotes(); Navigator.pop(context); }, - child: const Text('Hapus', style: TextStyle(color: Colors.red)), + child: Text('Hapus', style: TextStyle(color: Theme.of(context).colorScheme.error)), ), ], ), @@ -1607,6 +1788,21 @@ class _NotesScreenState extends State { @override Widget build(BuildContext context) { + if (isLoading) { + return const Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('Memuat notes...'), + ], + ), + ), + ); + } + return Scaffold( body: Column( children: [ @@ -2108,26 +2304,32 @@ class _AddEditTaskDialogState extends State { children: [ TextField( controller: _titleController, - decoration: const InputDecoration( + decoration: InputDecoration( labelText: 'Judul Task', - border: OutlineInputBorder(), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(25), + ), ), ), const SizedBox(height: 16), TextField( controller: _descriptionController, - decoration: const InputDecoration( + decoration: InputDecoration( labelText: 'Deskripsi (opsional)', - border: OutlineInputBorder(), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(25), + ), ), maxLines: 3, ), const SizedBox(height: 16), DropdownButtonFormField( value: _selectedCategory, - decoration: const InputDecoration( + decoration: InputDecoration( labelText: 'Kategori', - border: OutlineInputBorder(), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(25), + ), ), items: widget.categories.map((category) { return DropdownMenuItem( @@ -2155,9 +2357,11 @@ class _AddEditTaskDialogState extends State { Expanded( child: TextField( controller: _subTaskController, - decoration: const InputDecoration( + decoration: InputDecoration( hintText: 'Tambah subtask', - border: OutlineInputBorder(), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(25), + ), ), onSubmitted: (_) => _addSubTask(), ), @@ -2285,17 +2489,21 @@ class _AddEditNoteDialogState extends State { children: [ TextField( controller: _titleController, - decoration: const InputDecoration( + decoration: InputDecoration( labelText: 'Judul Note', - border: OutlineInputBorder(), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(25), + ), ), ), const SizedBox(height: 16), TextField( controller: _contentController, - decoration: const InputDecoration( + decoration: InputDecoration( labelText: 'Isi Note', - border: OutlineInputBorder(), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(25), + ), alignLabelWithHint: true, ), maxLines: 8, @@ -2303,9 +2511,11 @@ class _AddEditNoteDialogState extends State { const SizedBox(height: 16), DropdownButtonFormField( value: _selectedCategory, - decoration: const InputDecoration( + decoration: InputDecoration( labelText: 'Kategori', - border: OutlineInputBorder(), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(25), + ), ), items: widget.categories.map((category) { return DropdownMenuItem( @@ -2320,7 +2530,7 @@ class _AddEditNoteDialogState extends State { }, ), const SizedBox(height: 16), - const Align( + Align( alignment: Alignment.centerLeft, child: Text( 'Warna Note:', diff --git a/lib/page/login.dart b/lib/page/login.dart index 7263fab..0f3aafa 100644 --- a/lib/page/login.dart +++ b/lib/page/login.dart @@ -3,6 +3,7 @@ import 'package:tlist/page/register.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/foundation.dart'; import 'package:google_sign_in/google_sign_in.dart'; +import 'package:tlist/main.dart'; class LoginPage extends StatefulWidget { const LoginPage({super.key}); @@ -46,7 +47,11 @@ class _LoginPageState extends State { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Login berhasil')), ); - Navigator.pop(context); + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (_) => const MainScreen()), + (route) => false, + ); } on FirebaseAuthException catch (e) { final msg = _humanizeAuthError(e.code); ScaffoldMessenger.of(context).showSnackBar( @@ -172,7 +177,11 @@ class _LoginPageState extends State { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Login Google berhasil')), ); - Navigator.pop(context); + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (_) => const MainScreen()), + (route) => false, + ); } on FirebaseAuthException catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( @@ -227,10 +236,12 @@ class _LoginPageState extends State { controller: _emailController, keyboardType: TextInputType.emailAddress, textInputAction: TextInputAction.next, - decoration: const InputDecoration( + decoration: InputDecoration( labelText: 'Email', hintText: 'you@example.com', - border: OutlineInputBorder(), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(25), + ), prefixIcon: Icon(Icons.email_outlined), ), validator: (value) { @@ -249,8 +260,10 @@ class _LoginPageState extends State { onFieldSubmitted: (_) => _submit(), decoration: InputDecoration( labelText: 'Password', - border: const OutlineInputBorder(), - prefixIcon: const Icon(Icons.lock_outline), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(25), + ), + prefixIcon: Icon(Icons.lock_outline), suffixIcon: IconButton( onPressed: () => setState(() => _obscure = !_obscure), icon: Icon(_obscure ? Icons.visibility : Icons.visibility_off), @@ -269,6 +282,9 @@ class _LoginPageState extends State { onPressed: _submit, style: ElevatedButton.styleFrom( minimumSize: const Size.fromHeight(48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(25), + ), ), child: const Text('Login'), ), @@ -283,10 +299,13 @@ class _LoginPageState extends State { label: const Text('Lanjutkan dengan Google'), style: OutlinedButton.styleFrom( minimumSize: const Size.fromHeight(48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(25), + ), ), ), const SizedBox(height: 12), - const Divider(), + Divider(color: Theme.of(context).dividerColor.withOpacity(0.5)), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -302,7 +321,7 @@ class _LoginPageState extends State { }, child: const Text('Daftar'), ), - const Text(' | '), + Text(' | ', style: TextStyle(color: Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.6))), TextButton( onPressed: _forgotPassword, child: const Text('Lupa password?'), diff --git a/lib/page/register.dart b/lib/page/register.dart index 2a338c7..cd806e8 100644 --- a/lib/page/register.dart +++ b/lib/page/register.dart @@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart'; import 'package:google_sign_in/google_sign_in.dart'; import 'package:flutter/material.dart'; import 'package:firebase_auth/firebase_auth.dart'; +import 'package:tlist/main.dart'; import 'package:tlist/page/login.dart'; class RegisterPage extends StatefulWidget { @@ -114,7 +115,11 @@ class _RegisterPageState extends State { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Login Google berhasil')), ); - Navigator.pop(context); + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (_) => const MainScreen()), + (route) => false, + ); } on FirebaseAuthException catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( @@ -158,9 +163,11 @@ class _RegisterPageState extends State { TextFormField( controller: _nameController, textInputAction: TextInputAction.next, - decoration: const InputDecoration( + decoration: InputDecoration( labelText: 'Nama Lengkap', - border: OutlineInputBorder(), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(25), + ), prefixIcon: Icon(Icons.person_outline), ), validator: (value) { @@ -175,10 +182,12 @@ class _RegisterPageState extends State { controller: _emailController, keyboardType: TextInputType.emailAddress, textInputAction: TextInputAction.next, - decoration: const InputDecoration( + decoration: InputDecoration( labelText: 'Email', hintText: 'you@example.com', - border: OutlineInputBorder(), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(25), + ), prefixIcon: Icon(Icons.email_outlined), ), validator: (value) { @@ -196,7 +205,9 @@ class _RegisterPageState extends State { textInputAction: TextInputAction.next, decoration: InputDecoration( labelText: 'Password', - border: const OutlineInputBorder(), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(25), + ), prefixIcon: const Icon(Icons.lock_outline), suffixIcon: IconButton( onPressed: () => setState(() => _obscurePass = !_obscurePass), @@ -219,7 +230,9 @@ class _RegisterPageState extends State { onFieldSubmitted: (_) => _submit(), decoration: InputDecoration( labelText: 'Konfirmasi Password', - border: const OutlineInputBorder(), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(25), + ), prefixIcon: const Icon(Icons.lock_outline), suffixIcon: IconButton( onPressed: () => setState(() => _obscureConfirm = !_obscureConfirm), diff --git a/lib/page/user.dart b/lib/page/user.dart index 039f8a6..c532eab 100644 --- a/lib/page/user.dart +++ b/lib/page/user.dart @@ -46,7 +46,7 @@ class UserPage extends StatelessWidget { ), const SizedBox(height: 12), Text( - 'Kamu Belum Log In', + 'Kamu Belum Login', textAlign: TextAlign.center, style: Theme.of(context).textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, @@ -74,7 +74,7 @@ class UserPage extends StatelessWidget { style: OutlinedButton.styleFrom(minimumSize: const Size.fromHeight(48)), child: const Text('Daftar'), ), - const Divider(), + Divider(color: Theme.of(context).dividerColor.withOpacity(0.5)), ListTile( leading: const Icon(Icons.system_update_alt), title: const Text('Cek pembaruan'), @@ -172,7 +172,7 @@ class UserPage extends StatelessWidget { title: Text(displayName), subtitle: Text(email), ), - const Divider(), + Divider(color: Theme.of(context).dividerColor.withOpacity(0.5)), if (!(user.emailVerified)) ListTile( leading: const Icon(Icons.mark_email_unread_outlined), @@ -319,15 +319,38 @@ class UserPage extends StatelessWidget { leading: const Icon(Icons.logout), title: const Text('Keluar'), onTap: () async { - await FirebaseAuth.instance.signOut(); - if (context.mounted) { + final confirm = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Konfirmasi'), + content: const Text('Yakin ingin keluar?'), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(false), + child: const Text('Batal'), + ), + ElevatedButton( + onPressed: () => Navigator.of(ctx).pop(true), + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.error, + foregroundColor: Colors.white, + ), + child: const Text('Keluar'), + ), + ], + ), + ); + + if (confirm == true) { + await FirebaseAuth.instance.signOut(); + if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Signed out')), ); } }, ), - const Divider(), + Divider(color: Theme.of(context).dividerColor.withOpacity(0.5)), ListTile( leading: const Icon(Icons.system_update_alt), title: const Text('Cek pembaruan'), @@ -405,23 +428,38 @@ class UserPage extends StatelessWidget { final result = await showDialog( context: context, builder: (context) { - return AlertDialog( - title: Text(title), - content: TextField( - controller: controller, - obscureText: obscure, - decoration: InputDecoration(hintText: hint), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Batal'), - ), - ElevatedButton( - onPressed: () => Navigator.pop(context, controller.text.trim()), - child: const Text('OK'), - ), - ], + bool isObscure = obscure; + return StatefulBuilder( + builder: (ctx, setState) { + return AlertDialog( + title: Text(title), + content: TextField( + controller: controller, + obscureText: isObscure, + decoration: InputDecoration( + hintText: hint, + suffixIcon: obscure + ? IconButton( + icon: Icon(isObscure ? Icons.visibility_off : Icons.visibility), + onPressed: () => setState(() => isObscure = !isObscure), + ) + : null, + ), + textInputAction: TextInputAction.done, + onSubmitted: (_) => Navigator.pop(ctx, controller.text.trim()), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('Batal'), + ), + ElevatedButton( + onPressed: () => Navigator.pop(ctx, controller.text.trim()), + child: const Text('OK'), + ), + ], + ); + }, ); }, ); @@ -430,4 +468,3 @@ class UserPage extends StatelessWidget { } } - diff --git a/pubspec.lock b/pubspec.lock index bb8d8c0..c828dc4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -368,6 +368,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.4" + intl: + dependency: "direct main" + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" json_annotation: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b1c16de..5637c25 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ repository: https://github.com/friyn/tlist publish_to: 'none' -version: 1.2.0+120 +version: 1.5.0+150 environment: sdk: '>=3.0.0 <4.0.0' @@ -16,7 +16,7 @@ dependencies: sdk: flutter cupertino_icons: ^1.0.8 - + intl: ^0.19.0 shared_preferences: ^2.5.3 firebase_core: ^4.0.0 firebase_auth: ^6.0.0 diff --git a/web/privacy.html b/web/privacy.html new file mode 100644 index 0000000..3ebe283 --- /dev/null +++ b/web/privacy.html @@ -0,0 +1,86 @@ + + + + + + TList | Kebijakan Privasi + + + + + + \ No newline at end of file diff --git a/web/terms.html b/web/terms.html new file mode 100644 index 0000000..56c111c --- /dev/null +++ b/web/terms.html @@ -0,0 +1,71 @@ + + + + + + TList | Ketentuan Layanan + + + +
+

Ketentuan Layanan TList

+

Pembaruan terakhir: 16 Agustus 2025

+ +

Dengan mengakses atau menggunakan TList ("Layanan"), Anda menyatakan setuju untuk terikat pada Ketentuan Layanan ini. Jika Anda tidak setuju, mohon untuk tidak menggunakan Layanan.

+ +

1. Deskripsi Layanan

+

TList menyediakan aplikasi untuk manajemen akun dan autentikasi pengguna. Layanan ini menggunakan Firebase Authentication dan opsi masuk dengan Google.

+ +

2. Akun Pengguna

+
    +
  • Anda bertanggung jawab atas kerahasiaan kredensial akun Anda.
  • +
  • Anda harus memberikan informasi yang akurat dan memperbaruinya bila ada perubahan.
  • +
  • Anda setuju menerima email verifikasi dan komunikasi terkait keamanan akun (mis. reset sandi).
  • +
+ +

3. Privasi

+

Penggunaan data pribadi diatur dalam Kebijakan Privasi. Dengan menggunakan Layanan, Anda juga menyetujui Kebijakan Privasi tersebut.

+ +

4. Penggunaan yang Dilarang

+
    +
  • Melanggar hukum atau hak pihak lain.
  • +
  • Mengganggu keamanan atau integritas Layanan.
  • +
  • Mencoba mengakses area atau data yang tidak Anda berhak akses.
  • +
+ +

5. Kepemilikan dan Lisensi

+

Seluruh hak kekayaan intelektual atas Layanan dimiliki oleh pemilik TList. Anda diberi lisensi terbatas, non-eksklusif, dan dapat dibatalkan untuk menggunakan Layanan sesuai Ketentuan ini.

+ +

6. Layanan Pihak Ketiga

+

Layanan memanfaatkan penyedia pihak ketiga, termasuk Firebase Authentication (oleh Google) dan Google Sign-In. Penggunaan Anda juga tunduk pada ketentuan dan kebijakan mereka.

+ +

7. Pengakhiran

+

Kami dapat menangguhkan atau menghentikan akses Anda ke Layanan jika Anda melanggar Ketentuan ini atau menimbulkan risiko terhadap Layanan atau pengguna lain.

+ +

8. Penyangkalan Jaminan

+

Layanan disediakan "sebagaimana adanya" tanpa jaminan apa pun, tersurat maupun tersirat.

+ +

9. Batasan Tanggung Jawab

+

Sejauh diizinkan hukum yang berlaku, kami tidak bertanggung jawab atas kerugian tidak langsung, insidental, khusus, atau konsekuensial yang timbul dari penggunaan Layanan.

+ +

10. Perubahan Ketentuan

+

Kami dapat memperbarui Ketentuan ini dari waktu ke waktu. Versi terbaru akan ditampilkan di halaman ini.

+ +

11. Hukum yang Berlaku

+

Ketentuan ini diatur oleh hukum yang berlaku di Indonesia, tanpa mengesampingkan pertentangan kaidah hukum.

+ +

12. Kontak

+

Untuk pertanyaan, silakan hubungi kami di: email kontak Anda di sini.

+ +

Lihat juga: Kebijakan Privasi

+
+ + \ No newline at end of file
+

Kebijakan Privasi TList

+

Pembaruan terakhir: 16 Agustus 2025

+ +

Kebijakan Privasi ini menjelaskan bagaimana TList ("kami") mengumpulkan, menggunakan, menyimpan, melindungi, dan membagikan informasi pribadi Anda saat Anda menggunakan layanan kami ("Layanan"). Dengan menggunakan Layanan, Anda menyetujui praktik yang dijelaskan di sini.

+ +

1. Data yang Kami Kumpulkan

+
    +
  • Informasi Akun: nama, alamat email, dan sandi (disimpan dan dikelola oleh Firebase Authentication).
  • +
  • Masuk dengan Google: informasi profil dasar dari akun Google Anda (mis. nama, email, foto profil) serta token autentikasi untuk keperluan login.
  • +
  • Data Teknis: informasi perangkat dan penggunaan yang wajar untuk keperluan keamanan dan peningkatan Layanan (mis. alamat IP, tipe peramban, cap waktu), sebagaimana disediakan oleh platform yang Anda gunakan.
  • +
+ +

2. Cara Kami Menggunakan Data

+
    +
  • Menyediakan dan memelihara Layanan (autentikasi, manajemen akun).
  • +
  • Keamanan, pencegahan penyalahgunaan, dan pemulihan akun (mis. verifikasi email, reset sandi).
  • +
  • Peningkatan pengalaman pengguna dan dukungan.
  • +
  • Kepatuhan terhadap hukum yang berlaku.
  • +
+ +

3. Dasar Pemrosesan

+

Kami memroses data berdasarkan pelaksanaan kontrak (penyediaan Layanan), kepentingan sah (keamanan dan peningkatan), serta persetujuan Anda (mis. saat menggunakan Google Sign-In).

+ +

4. Berbagi Data dengan Pihak Ketiga

+

Kami menggunakan penyedia layanan pihak ketiga untuk menjalankan Layanan, termasuk:

+
    +
  • Firebase Authentication (oleh Google LLC) untuk autentikasi email/sandi dan pengelolaan kredensial.
  • +
  • Google Sign-In untuk proses masuk menggunakan akun Google.
  • +
+

Penggunaan Anda terhadap fitur tersebut juga tunduk pada ketentuan dan kebijakan privasi mereka.

+ +

5. Cookie dan Teknologi Serupa

+

Pada versi web, kami dapat menggunakan cookie, Local Storage, atau teknologi serupa untuk menjaga sesi login dan meningkatkan kinerja. Anda dapat menonaktifkannya melalui pengaturan peramban, namun beberapa fitur mungkin tidak berfungsi dengan baik.

+ +

6. Penyimpanan dan Retensi Data

+
    +
  • Data akun disimpan selama akun Anda aktif.
  • +
  • Anda dapat meminta penghapusan akun; setelah dihapus, data akan dihapus atau dianonimkan sesuai kebijakan retensi yang wajar.
  • +
+ +

7. Keamanan

+

Kami menerapkan langkah-langkah keamanan yang wajar, termasuk penggunaan penyedia terkemuka seperti Firebase. Namun, tidak ada metode transmisi atau penyimpanan yang sepenuhnya aman, sehingga kami tidak dapat menjamin keamanan absolut.

+ +

8. Hak Anda

+
    +
  • Mengakses dan memperbarui informasi akun.
  • +
  • Meminta penghapusan data tertentu atau penutupan akun.
  • +
  • Menarik persetujuan (mis. berhenti menggunakan Google Sign-In).
  • +
  • Menghubungi kami untuk pertanyaan terkait privasi.
  • +
+ +

9. Anak di Bawah Umur

+

Layanan tidak ditujukan untuk anak di bawah usia yang mewajibkan persetujuan orang tua menurut hukum yang berlaku. Jika Anda adalah orang tua/wali dan percaya anak Anda memberikan data kepada kami, silakan hubungi kami.

+ +

10. Transfer Internasional

+

Data dapat diproses di negara lain tempat penyedia layanan kami beroperasi (mis. infrastruktur Google). Kami akan mengambil langkah yang wajar untuk memastikan perlindungan yang sesuai.

+ +

11. Perubahan Kebijakan

+

Kami dapat memperbarui Kebijakan Privasi ini dari waktu ke waktu. Perubahan akan dipublikasikan di halaman ini dengan tanggal pembaruan terbaru.

+ +

12. Kontak

+

Untuk pertanyaan atau permintaan terkait data pribadi, hubungi: email kontak Anda di sini.

+ +

Lihat juga: Ketentuan Layanan

+