diff --git a/ios/Flutter/flutter_export_environment.sh b/ios/Flutter/flutter_export_environment.sh index bf33d1da..7f2f3c40 100755 --- a/ios/Flutter/flutter_export_environment.sh +++ b/ios/Flutter/flutter_export_environment.sh @@ -2,10 +2,9 @@ # This is a generated file; do not edit or check into version control. export "FLUTTER_ROOT=/Users/rainvisitor/development/flutter" export "FLUTTER_APPLICATION_PATH=/Users/rainvisitor/Documents/GitHub-NKUST-ITC/NKUST-AP-Flutter" -export "FLUTTER_TARGET=/Users/rainvisitor/Documents/GitHub-NKUST-ITC/NKUST-AP-Flutter/lib/main.dart" +export "FLUTTER_TARGET=lib/main.dart" export "FLUTTER_BUILD_DIR=build" export "SYMROOT=${SOURCE_ROOT}/../build/ios" -export "FLUTTER_FRAMEWORK_DIR=/Users/rainvisitor/development/flutter/bin/cache/artifacts/engine/ios" -export "FLUTTER_BUILD_NAME=3.2.9" -export "FLUTTER_BUILD_NUMBER=30209" -export "TRACK_WIDGET_CREATION=true" +export "FLUTTER_FRAMEWORK_DIR=/Users/rainvisitor/development/flutter/bin/cache/artifacts/engine/ios-release" +export "FLUTTER_BUILD_NAME=3.2.11" +export "FLUTTER_BUILD_NUMBER=30211" diff --git a/lib/api/helper.dart b/lib/api/helper.dart index 15adcb95..89b9a9f6 100644 --- a/lib/api/helper.dart +++ b/lib/api/helper.dart @@ -24,7 +24,7 @@ import 'package:nkust_ap/utils/preferences.dart'; import 'package:nkust_ap/utils/utils.dart'; class Helper { - static const HOST = 'nkus-ap-staging.rainvisitor.me'; + static const HOST = 'nkust-ap-staging.rainvisitor.me'; static const VERSION = 'v3'; @@ -113,6 +113,27 @@ class Helper { } } + Future adminLogin(String username, String password) async { + try { + var response = await dio.post( + '/oauth/admin/token', + data: { + 'username': username, + 'password': password, + }, + ); + if (response == null) print('null'); + var loginResponse = LoginResponse.fromJson(response.data); + options.headers = _createBearerTokenAuth(loginResponse.token); + expireTime = loginResponse.expireTime; + Helper.username = username; + Helper.password = password; + return loginResponse; + } catch (dioError) { + throw dioError; + } + } + Future deleteToken() async { try { var response = await dio.delete( @@ -162,6 +183,42 @@ class Helper { } } + Future addAnnouncement(Announcements announcements) async { + try { + var response = await dio.post( + "/news/announcements/add", + data: announcements.toUpdateJson(), + ); + return response; + } on DioError catch (dioError) { + throw dioError; + } + } + + Future updateAnnouncement(Announcements announcements) async { + try { + var response = await dio.put( + "/news/announcements/update/${announcements.id}", + data: announcements.toUpdateJson(), + ); + return response; + } on DioError catch (dioError) { + throw dioError; + } + } + + Future deleteAnnouncement(Announcements announcements) async { + try { + var response = await dio.delete( + "/news/announcements/remove/${announcements.id}", + data: announcements.toUpdateJson(), + ); + return response; + } on DioError catch (dioError) { + throw dioError; + } + } + Future getUsersInfo() async { if (isExpire()) await login(username, password); try { diff --git a/lib/app.dart b/lib/app.dart new file mode 100644 index 00000000..870f1add --- /dev/null +++ b/lib/app.dart @@ -0,0 +1,176 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:firebase_analytics/firebase_analytics.dart'; +import 'package:firebase_analytics/observer.dart'; +import 'package:firebase_crashlytics/firebase_crashlytics.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart' + show debugDefaultTargetPlatformOverride; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:nkust_ap/config/constants.dart'; +import 'package:nkust_ap/pages/home/bus/bus_rule_page.dart'; +import 'package:nkust_ap/pages/home/news/news_admin_page.dart'; +import 'package:nkust_ap/pages/page.dart'; +import 'package:nkust_ap/res/app_icon.dart'; +import 'package:nkust_ap/res/app_theme.dart'; +import 'package:nkust_ap/utils/app_localizations.dart'; +import 'package:nkust_ap/utils/firebase_analytics_utils.dart'; +import 'package:nkust_ap/utils/preferences.dart'; +import 'package:nkust_ap/utils/utils.dart'; +import 'package:nkust_ap/widgets/drawer_body.dart'; +import 'package:nkust_ap/widgets/share_data_widget.dart'; + +import 'api/helper.dart'; +import 'models/login_response.dart'; +import 'models/user_info.dart'; + +class MyApp extends StatefulWidget { + final ThemeData themeData; + + const MyApp({Key key, @required this.themeData}) : super(key: key); + + @override + MyAppState createState() => MyAppState(); +} + +class MyAppState extends State { + FirebaseAnalytics analytics; + FirebaseMessaging firebaseMessaging; + ThemeData themeData; + UserInfo userInfo; + LoginResponse loginResponse; + Uint8List pictureBytes; + bool isLogin = false, offlineLogin = false; + + setThemeData(ThemeData themeData) { + setState(() { + this.themeData = themeData; + }); + } + + logout() { + setState(() { + this.isLogin = false; + this.offlineLogin = false; + this.userInfo = null; + this.loginResponse = null; + this.pictureBytes = null; + Helper.clearSetting(); + }); + } + + @override + void initState() { + themeData = widget.themeData; + if (kIsWeb) { + } else if (Platform.isAndroid || Platform.isIOS) { + analytics = FirebaseAnalytics(); + firebaseMessaging = FirebaseMessaging(); + _initFCM(); + FA.analytics = analytics; + FA.setUserProperty('theme', AppTheme.code); + FA.setUserProperty('icon_style', AppIcon.code); + Preferences.init(); + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + print(AppLocalizations.languageCode); + return ShareDataWidget( + data: this, + child: MaterialApp( + localeResolutionCallback: + (Locale locale, Iterable supportedLocales) { + return locale; + }, + onGenerateTitle: (context) => AppLocalizations.of(context).appName, + debugShowCheckedModeBanner: false, + routes: { + Navigator.defaultRouteName: (context) => HomePage(), + LoginPage.routerName: (BuildContext context) => LoginPage(), + HomePage.routerName: (BuildContext context) => HomePage(), + CoursePage.routerName: (BuildContext context) => CoursePage(), + BusPage.routerName: (BuildContext context) => BusPage(), + BusRulePage.routerName: (BuildContext context) => BusRulePage(), + ScorePage.routerName: (BuildContext context) => ScorePage(), + SchoolInfoPage.routerName: (BuildContext context) => SchoolInfoPage(), + SettingPage.routerName: (BuildContext context) => SettingPage(), + AboutUsPage.routerName: (BuildContext context) => AboutUsPage(), + OpenSourcePage.routerName: (BuildContext context) => OpenSourcePage(), + UserInfoPage.routerName: (BuildContext context) => UserInfoPage(), + NewsAdminPage.routerName: (BuildContext context) => NewsAdminPage(), + CalculateUnitsPage.routerName: (BuildContext context) => + CalculateUnitsPage(), + NewsContentPage.routerName: (BuildContext context) => + NewsContentPage(null), + LeavePage.routerName: (BuildContext context) => LeavePage(), + }, + theme: themeData, + navigatorObservers: (kIsWeb) + ? [] + : (Platform.isIOS || Platform.isAndroid) + ? [ + FirebaseAnalyticsObserver(analytics: analytics), + ] + : [], + localizationsDelegates: [ + const AppLocalizationsDelegate(), + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: [ + const Locale('en', 'US'), // English + const Locale('zh', 'TW'), // Chinese + ], + ), + ); + } + + void _initFCM() { + firebaseMessaging.requestNotificationPermissions(); + firebaseMessaging.configure( + onMessage: (Map message) async { + if (Constants.isInDebugMode) print("onMessage: $message"); + Utils.showFCMNotification( + message['notification']['title'] ?? '', + message['notification']['title'] ?? '', + message['notification']['body'] ?? ''); + }, + onLaunch: (Map message) async { + if (Constants.isInDebugMode) print("onLaunch: $message"); + //_navigateToItemDetail(message); + }, + onResume: (Map message) async { + if (Constants.isInDebugMode) print("onResume: $message"); + }, + ); + firebaseMessaging.requestNotificationPermissions( + const IosNotificationSettings( + sound: true, + badge: true, + alert: true, + ), + ); + firebaseMessaging.onIosSettingsRegistered + .listen((IosNotificationSettings settings) { + print("Settings registered: $settings"); + }); + firebaseMessaging.getToken().then((String token) { + if (token == null) return; + if (Constants.isInDebugMode) { + print("Push Messaging token: $token"); + } + if (Platform.isAndroid) + firebaseMessaging.subscribeToTopic("Android"); + else if (Platform.isIOS) firebaseMessaging.subscribeToTopic("IOS"); + }); + } +} diff --git a/lib/main.dart b/lib/main.dart index b72f6b14..7cd7cf0b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,32 +1,17 @@ -import 'dart:async'; import 'dart:io'; -import 'dart:typed_data'; -import 'package:firebase_analytics/firebase_analytics.dart'; -import 'package:firebase_analytics/observer.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; -import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart' show debugDefaultTargetPlatformOverride; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:nkust_ap/app.dart'; import 'package:nkust_ap/config/constants.dart'; -import 'package:nkust_ap/pages/home/bus/bus_rule_page.dart'; -import 'package:nkust_ap/pages/page.dart'; import 'package:nkust_ap/res/app_icon.dart'; import 'package:nkust_ap/res/app_theme.dart'; -import 'package:nkust_ap/utils/app_localizations.dart'; -import 'package:nkust_ap/utils/firebase_analytics_utils.dart'; import 'package:nkust_ap/utils/preferences.dart'; -import 'package:nkust_ap/utils/utils.dart'; -import 'package:nkust_ap/widgets/drawer_body.dart'; -import 'package:nkust_ap/widgets/share_data_widget.dart'; -import 'api/helper.dart'; -import 'models/login_response.dart'; -import 'models/user_info.dart'; void main() async { bool isInDebugMode = Constants.isInDebugMode; @@ -60,149 +45,4 @@ void _setTargetPlatformForDesktop() { if (targetPlatform != null) { debugDefaultTargetPlatformOverride = targetPlatform; } -} - -class MyApp extends StatefulWidget { - final ThemeData themeData; - - const MyApp({Key key, @required this.themeData}) : super(key: key); - - @override - MyAppState createState() => MyAppState(); -} - -class MyAppState extends State { - FirebaseAnalytics analytics; - FirebaseMessaging firebaseMessaging; - ThemeData themeData; - UserInfo userInfo; - LoginResponse loginResponse; - Uint8List pictureBytes; - bool isLogin = false, offlineLogin = false; - - setThemeData(ThemeData themeData) { - setState(() { - this.themeData = themeData; - }); - } - - logout() { - setState(() { - this.isLogin = false; - this.offlineLogin = false; - this.userInfo = null; - this.loginResponse = null; - this.pictureBytes = null; - Helper.clearSetting(); - }); - } - - @override - void initState() { - themeData = widget.themeData; - if (kIsWeb) { - } else if (Platform.isAndroid || Platform.isIOS) { - analytics = FirebaseAnalytics(); - firebaseMessaging = FirebaseMessaging(); - _initFCM(); - FA.analytics = analytics; - FA.setUserProperty('theme', AppTheme.code); - FA.setUserProperty('icon_style', AppIcon.code); - Preferences.init(); - } - super.initState(); - } - - @override - Widget build(BuildContext context) { - print(AppLocalizations.languageCode); - return ShareDataWidget( - this, - child: MaterialApp( - localeResolutionCallback: - (Locale locale, Iterable supportedLocales) { - return locale; - }, - onGenerateTitle: (context) => AppLocalizations.of(context).appName, - debugShowCheckedModeBanner: false, - routes: { - Navigator.defaultRouteName: (context) => HomePage(), - LoginPage.routerName: (BuildContext context) => LoginPage(), - HomePage.routerName: (BuildContext context) => HomePage(), - CoursePage.routerName: (BuildContext context) => CoursePage(), - BusPage.routerName: (BuildContext context) => BusPage(), - BusRulePage.routerName: (BuildContext context) => BusRulePage(), - ScorePage.routerName: (BuildContext context) => ScorePage(), - SchoolInfoPage.routerName: (BuildContext context) => SchoolInfoPage(), - SettingPage.routerName: (BuildContext context) => SettingPage(), - AboutUsPage.routerName: (BuildContext context) => AboutUsPage(), - OpenSourcePage.routerName: (BuildContext context) => OpenSourcePage(), - UserInfoPage.routerName: (BuildContext context) => UserInfoPage(), - CalculateUnitsPage.routerName: (BuildContext context) => - CalculateUnitsPage(), - NewsContentPage.routerName: (BuildContext context) => - NewsContentPage(null), - LeavePage.routerName: (BuildContext context) => LeavePage(), - }, - theme: themeData, - navigatorObservers: (kIsWeb) - ? [] - : (Platform.isIOS || Platform.isAndroid) - ? [ - FirebaseAnalyticsObserver(analytics: analytics), - ] - : [], - localizationsDelegates: [ - const AppLocalizationsDelegate(), - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: [ - const Locale('en', 'US'), // English - const Locale('zh', 'TW'), // Chinese - ], - ), - ); - } - - void _initFCM() { - firebaseMessaging.requestNotificationPermissions(); - firebaseMessaging.configure( - onMessage: (Map message) async { - if (Constants.isInDebugMode) print("onMessage: $message"); - Utils.showFCMNotification( - message['notification']['title'] ?? '', - message['notification']['title'] ?? '', - message['notification']['body'] ?? ''); - }, - onLaunch: (Map message) async { - if (Constants.isInDebugMode) print("onLaunch: $message"); - //_navigateToItemDetail(message); - }, - onResume: (Map message) async { - if (Constants.isInDebugMode) print("onResume: $message"); - }, - ); - firebaseMessaging.requestNotificationPermissions( - const IosNotificationSettings( - sound: true, - badge: true, - alert: true, - ), - ); - firebaseMessaging.onIosSettingsRegistered - .listen((IosNotificationSettings settings) { - print("Settings registered: $settings"); - }); - firebaseMessaging.getToken().then((String token) { - if (token == null) return; - if (Constants.isInDebugMode) { - print("Push Messaging token: $token"); - } - if (Platform.isAndroid) - firebaseMessaging.subscribeToTopic("Android"); - else if (Platform.isIOS) firebaseMessaging.subscribeToTopic("IOS"); - }); - } -} +} \ No newline at end of file diff --git a/lib/models/announcements_data.dart b/lib/models/announcements_data.dart index 2ce8d8ca..1a8eb2da 100644 --- a/lib/models/announcements_data.dart +++ b/lib/models/announcements_data.dart @@ -38,6 +38,7 @@ class Announcements { String url; String description; String publishedTime; + String expireTime; Announcements({ this.title, @@ -49,6 +50,7 @@ class Announcements { this.url, this.description, this.publishedTime, + this.expireTime, }); factory Announcements.fromRawJson(String str) => @@ -56,6 +58,8 @@ class Announcements { String toRawJson() => json.encode(toJson()); + String toRawUpdateJson() => json.encode(toUpdateJson()); + factory Announcements.fromJson(Map json) => Announcements( title: json["title"], id: json["id"], @@ -66,6 +70,7 @@ class Announcements { url: json["url"], description: json["description"], publishedTime: json["publishedTime"], + expireTime: json["expireTime"], ); Map toJson() => { @@ -78,5 +83,15 @@ class Announcements { "url": url, "description": description, "publishedTime": publishedTime, + "expireTime": expireTime, + }; + + Map toUpdateJson() => { + "title": title, + "weight": weight, + "imgUrl": imgUrl, + "url": url, + "description": description, + "expireTime": expireTime, }; } diff --git a/lib/models/login_response.dart b/lib/models/login_response.dart index 375ed639..07008071 100644 --- a/lib/models/login_response.dart +++ b/lib/models/login_response.dart @@ -1,12 +1,18 @@ +// To parse this JSON data, do +// +// final loginResponse = loginResponseFromJson(jsonString); + import 'dart:convert'; class LoginResponse { - DateTime expireTime; String token; + DateTime expireTime; + bool isAdmin; LoginResponse({ - this.expireTime, this.token, + this.expireTime, + this.isAdmin, }); factory LoginResponse.fromRawJson(String str) => @@ -14,14 +20,17 @@ class LoginResponse { String toRawJson() => json.encode(toJson()); - factory LoginResponse.fromJson(Map json) => - new LoginResponse( - expireTime: DateTime.parse(json["expireTime"]), - token: json["token"], + factory LoginResponse.fromJson(Map json) => LoginResponse( + token: json["token"] == null ? null : json["token"], + expireTime: json["expireTime"] == null + ? null + : DateTime.parse(json["expireTime"]), + isAdmin: json["isAdmin"] == null ? null : json["isAdmin"], ); Map toJson() => { - "expireTime": expireTime.toIso8601String(), - "token": token, + "token": token == null ? null : token, + "expireTime": expireTime == null ? null : expireTime.toIso8601String(), + "isAdmin": isAdmin == null ? null : isAdmin, }; } diff --git a/lib/pages/home/about/about_us_page.dart b/lib/pages/home/about/about_us_page.dart index 4a5424aa..e46c39c5 100644 --- a/lib/pages/home/about/about_us_page.dart +++ b/lib/pages/home/about/about_us_page.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'dart:math'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:nkust_ap/res/app_icon.dart'; import 'package:nkust_ap/res/assets.dart'; @@ -30,7 +31,8 @@ class AboutUsPageState extends State { } String get sectionImage { - final department = ShareDataWidget.of(context).data.userInfo?.department ?? ''; + final department = + ShareDataWidget.of(context).data.userInfo?.department ?? ''; Random random = Random(); bool halfSnapFingerChance = random.nextInt(2000) % 2 == 0; if (department.contains('建工') || department.contains('燕巢')) @@ -133,11 +135,16 @@ class AboutUsPageState extends State { IconButton( icon: Image.asset(ImageAssets.fb), onPressed: () { - if (Platform.isAndroid) + if (kIsWeb) + Utils.launchUrl( + 'https://www.facebook.com/NKUST.ITC/') + .catchError((onError) => Utils.showToast( + context, app.platformError)); + else if (Platform.isAndroid) Utils.launchUrl('fb://page/735951703168873') .catchError((onError) => Utils.launchUrl( 'https://www.facebook.com/NKUST.ITC/')); - if (Platform.isIOS) + else if (Platform.isIOS) Utils.launchUrl('fb://profile/735951703168873') .catchError((onError) => Utils.launchUrl( 'https://www.facebook.com/NKUST.ITC/')); @@ -153,7 +160,11 @@ class AboutUsPageState extends State { IconButton( icon: Image.asset(ImageAssets.github), onPressed: () { - if (Platform.isAndroid) + if (kIsWeb) + Utils.launchUrl('https://github.com/NKUST-ITC') + .catchError((onError) => Utils.showToast( + context, app.platformError)); + else if (Platform.isAndroid) Utils.launchUrl( 'github://organization/NKUST-ITC') .catchError((onError) => Utils.launchUrl( diff --git a/lib/pages/home/news/news_admin_page.dart b/lib/pages/home/news/news_admin_page.dart new file mode 100644 index 00000000..c9c38be2 --- /dev/null +++ b/lib/pages/home/news/news_admin_page.dart @@ -0,0 +1,403 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:nkust_ap/api/helper.dart'; +import 'package:nkust_ap/models/announcements_data.dart'; +import 'package:nkust_ap/models/login_response.dart'; +import 'package:nkust_ap/pages/home/news/news_edit_page.dart'; +import 'package:nkust_ap/res/app_icon.dart'; +import 'package:nkust_ap/res/resource.dart' as Resource; +import 'package:nkust_ap/utils/app_localizations.dart'; +import 'package:nkust_ap/utils/utils.dart'; +import 'package:nkust_ap/widgets/hint_content.dart'; +import 'package:nkust_ap/widgets/progress_dialog.dart'; +import 'package:nkust_ap/widgets/yes_no_dialog.dart'; +import 'package:sprintf/sprintf.dart'; + +enum _State { notLogin, loading, finish, error, empty, offline } + +class NewsAdminPage extends StatefulWidget { + static const String routerName = "/news/admin"; + final bool isAdmin; + + const NewsAdminPage({Key key, this.isAdmin = false}) : super(key: key); + + @override + _NewsAdminPageState createState() => _NewsAdminPageState(); +} + +class _NewsAdminPageState extends State { + final GlobalKey _scaffoldKey = new GlobalKey(); + + final TextEditingController _username = TextEditingController(); + final TextEditingController _password = TextEditingController(); + + AppLocalizations app; + + _State state = _State.notLogin; + + AnnouncementsData announcementsResponse; + + bool isOffline = false; + FocusNode usernameFocusNode; + FocusNode passwordFocusNode; + + TextStyle get _editTextStyle => TextStyle( + fontSize: 18.0, + decorationColor: Resource.Colors.blueAccent, + ); + + @override + void initState() { + //FA.setCurrentScreen('ScorePage', 'score_page.dart'); + if (widget.isAdmin) { + state = _State.loading; + _getData(); + } else { + usernameFocusNode = FocusNode(); + passwordFocusNode = FocusNode(); + } + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + app = AppLocalizations.of(context); + return Scaffold( + key: _scaffoldKey, + appBar: AppBar( + title: Text(app.news), + backgroundColor: Resource.Colors.blue, + ), + floatingActionButton: state == _State.notLogin + ? null + : FloatingActionButton( + child: Icon(Icons.add), + onPressed: () async { + var success = await Navigator.push( + context, + CupertinoPageRoute( + builder: (_) => NewsEditPage( + mode: Mode.add, + ), + ), + ); + if (success is bool && success != null) { + if (success) { + _getData(); + } + } + }, + ), + body: RefreshIndicator( + onRefresh: () async { + await _getData(); + return null; + }, + child: _body(), + ), + ); + } + + _body() { + switch (state) { + case _State.notLogin: + return _loginContent(); + case _State.loading: + return Container( + child: CircularProgressIndicator(), alignment: Alignment.center); + case _State.empty: + case _State.error: + return FlatButton( + onPressed: () { + _getData(); + }, + child: HintContent( + icon: AppIcon.classIcon, + content: app.clickToRetry, + ), + ); + case _State.offline: + return HintContent( + icon: AppIcon.classIcon, + content: app.noOfflineData, + ); + default: + return ListView.builder( + itemBuilder: (_, index) { + return _item(announcementsResponse.data[index]); + }, + itemCount: announcementsResponse.data.length, + ); + } + } + + _loginContent() { + Widget usernameTextField = TextField( + maxLines: 1, + controller: _username, + textInputAction: TextInputAction.next, + focusNode: usernameFocusNode, + onSubmitted: (text) { + usernameFocusNode.unfocus(); + FocusScope.of(context).requestFocus(passwordFocusNode); + }, + decoration: InputDecoration( + labelText: app.username, + ), + style: _editTextStyle, + ); + Widget passwordTextField = TextField( + obscureText: true, + maxLines: 1, + textInputAction: TextInputAction.send, + controller: _password, + focusNode: passwordFocusNode, + onSubmitted: (text) { + passwordFocusNode.unfocus(); + _login(); + }, + decoration: InputDecoration( + labelText: app.password, + ), + style: _editTextStyle, + ); + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 32.0, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + usernameTextField, + passwordTextField, + SizedBox(height: 32.0), + Container( + width: double.infinity, + child: RaisedButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(30.0), + ), + ), + padding: EdgeInsets.all(14.0), + onPressed: () async { + _login(); + }, + color: Colors.white, + child: Text( + app.login, + style: TextStyle(color: Resource.Colors.blue, fontSize: 18.0), + ), + ), + ), + ], + ), + ); + } + + Widget _item(Announcements item) { + return Card( + elevation: 4.0, + margin: const EdgeInsets.all(8.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), + ), + child: InkWell( + radius: 12.0, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: ListTile( + title: Text( + item.title, + style: TextStyle(fontSize: 18.0), + ), + trailing: IconButton( + icon: Icon( + AppIcon.cancel, + color: Resource.Colors.red, + ), + onPressed: () async { + showDialog( + context: context, + builder: (BuildContext context) => YesNoDialog( + title: app.deleteNewsTitle, + contentWidget: Text( + "${app.deleteNewsContent}", + textAlign: TextAlign.center, + ), + leftActionText: app.back, + rightActionText: app.determine, + rightActionFunction: () { + Helper.instance.deleteAnnouncement(item).then((response) { + _scaffoldKey.currentState.showSnackBar( + SnackBar( + content: Text(app.deleteSuccess), + duration: Duration(seconds: 2), + ), + ); + _getData(); + }).catchError((e) { + if (e is DioError) { + switch (e.type) { + case DioErrorType.RESPONSE: + Utils.showToast(context, e.response?.data ?? ''); + break; + case DioErrorType.CANCEL: + break; + default: + Utils.handleDioError(context, e); + break; + } + } else { + throw e; + } + }); + }, + ), + ); + }, + ), + subtitle: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: RichText( + text: TextSpan( + style: TextStyle( + color: Resource.Colors.grey, height: 1.3, fontSize: 16.0), + children: [ + TextSpan( + text: '${app.weight}:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + TextSpan(text: '${item.weight ?? 1}\n'), + TextSpan( + text: '${app.imageUrl}:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + TextSpan( + text: '${item.imgUrl}', + style: TextStyle( + color: Resource.Colors.blueAccent, + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + Utils.launchUrl(item.imgUrl); + }, + ), + TextSpan( + text: '\n${app.url}:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + TextSpan( + text: '${item.url}', + style: TextStyle( + color: Resource.Colors.blueAccent, + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + Utils.launchUrl(item.url); + }, + ), + TextSpan( + text: '\n${app.expireTime}:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + TextSpan(text: '${item.expireTime?? app.noExpiration}\n'), + TextSpan( + text: '${app.description}:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + TextSpan(text: '${item.description}'), + ], + ), + ), + ), + ), + ), + onTap: () async { + var success = await Navigator.push( + context, + CupertinoPageRoute( + builder: (_) => NewsEditPage( + mode: Mode.edit, + announcement: item, + ), + ), + ); + if (success is bool && success != null) { + if (success) { + _getData(); + } + } + }, + ), + ); + } + + _getData() async { + Helper.instance.getAllAnnouncements().then((announcementsResponse) { + this.announcementsResponse = announcementsResponse; + setState(() { + state = announcementsResponse.data.length == 0 + ? _State.empty + : _State.finish; + }); + }).catchError((e) { + setState(() { + state = _State.error; + }); + }); + } + + void _login() async { + if (_username.text.isEmpty || _password.text.isEmpty) { + Utils.showToast(context, app.doNotEmpty); + } else { + showDialog( + context: context, + builder: (BuildContext context) => WillPopScope( + child: ProgressDialog(app.logining), + onWillPop: () async { + return false; + }), + barrierDismissible: false, + ); + Helper.instance + .adminLogin(_username.text, _password.text) + .then((LoginResponse response) async { + Navigator.of(context, rootNavigator: true).pop(); + Utils.showToast(context, app.loginSuccess); + setState(() { + state = _State.loading; + _getData(); + }); + }).catchError((e) { + Navigator.of(context, rootNavigator: true).pop(); + if (e is DioError) { + switch (e.type) { + case DioErrorType.RESPONSE: + Utils.showToast(context, app.loginFail); + break; + case DioErrorType.CANCEL: + break; + default: + Utils.handleDioError(context, e); + break; + } + } else { + throw e; + } + }); + } + } +} diff --git a/lib/pages/home/news/news_edit_page.dart b/lib/pages/home/news/news_edit_page.dart new file mode 100644 index 00000000..de9a1a9b --- /dev/null +++ b/lib/pages/home/news/news_edit_page.dart @@ -0,0 +1,327 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/painting.dart'; +import 'package:flutter/widgets.dart'; +import 'package:nkust_ap/api/helper.dart'; +import 'package:nkust_ap/models/announcements_data.dart'; +import 'package:nkust_ap/res/app_icon.dart'; +import 'package:nkust_ap/res/resource.dart' as Resource; +import 'package:nkust_ap/utils/app_localizations.dart'; +import 'package:nkust_ap/utils/utils.dart'; +import 'package:intl/intl.dart'; + +enum _State { loading, finish, error, empty, offline } +enum Mode { add, edit } + +class NewsEditPage extends StatefulWidget { + static const String routerName = "/news/edit"; + + final Mode mode; + final Announcements announcement; + + const NewsEditPage({ + Key key, + @required this.mode, + this.announcement, + }) : super(key: key); + + @override + _NewsEditPageState createState() => _NewsEditPageState(); +} + +class _NewsEditPageState extends State { + final _formKey = GlobalKey(); + + AppLocalizations app; + + _State state = _State.loading; + + Announcements announcements; + + var _title = TextEditingController(); + var _description = TextEditingController(); + var _imgUrl = TextEditingController(); + var _url = TextEditingController(); + var _weight = TextEditingController(); + + var formatter = DateFormat('yyyy-MM-ddTHH:mm:ssZ'); + var dateFormat = DateFormat('yyyy-MM-dd HH:mm:ss'); + + DateTime expireTime; + + final dividerHeight = 16.0; + + @override + void initState() { + if (widget.mode == Mode.edit) { + announcements = widget.announcement; + _title.text = announcements.title; + _imgUrl.text = announcements.imgUrl; + _url.text = announcements.url; + _weight.text = announcements.weight.toString(); + if (announcements.expireTime != null) + expireTime = formatter.parse(announcements.expireTime); + _description.text = announcements.description; + } else { + announcements = Announcements(); + } + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + app = AppLocalizations.of(context); + return Scaffold( + appBar: AppBar( + title: Text(app.news), + backgroundColor: Resource.Colors.blue, + ), + body: Form( + key: _formKey, + child: ListView( + padding: EdgeInsets.symmetric(horizontal: 16.0), + children: [ + SizedBox(height: dividerHeight), + TextFormField( + maxLines: 1, + controller: _title, + validator: (value) { + if (value.isEmpty) { + return app.doNotEmpty; + } + return null; + }, + decoration: InputDecoration( + border: OutlineInputBorder(), + fillColor: Resource.Colors.blueAccent, + labelStyle: TextStyle( + color: Resource.Colors.grey, + ), + labelText: app.title, + ), + ), + SizedBox(height: dividerHeight), + TextFormField( + maxLines: 1, + controller: _weight, + validator: (value) { + if (value.isEmpty) { + return app.doNotEmpty; + } else { + try { + int.parse(value); + } catch (e) { + return app.formatError; + } + } + return null; + }, + keyboardType: TextInputType.number, + decoration: InputDecoration( + border: OutlineInputBorder(), + fillColor: Resource.Colors.blueAccent, + labelStyle: TextStyle( + color: Resource.Colors.grey, + ), + labelText: app.weight, + ), + ), + SizedBox(height: dividerHeight), + TextFormField( + maxLines: 1, + controller: _imgUrl, + validator: (value) { + if (value.isEmpty) { + return app.doNotEmpty; + } + return null; + }, + keyboardType: TextInputType.url, + decoration: InputDecoration( + border: OutlineInputBorder(), + fillColor: Resource.Colors.blueAccent, + labelStyle: TextStyle( + color: Resource.Colors.grey, + ), + labelText: app.imageUrl, + ), + ), + SizedBox(height: dividerHeight), + TextFormField( + maxLines: 1, + controller: _url, + validator: (value) { + return null; + }, + keyboardType: TextInputType.url, + decoration: InputDecoration( + border: OutlineInputBorder(), + fillColor: Resource.Colors.blueAccent, + labelStyle: TextStyle( + color: Resource.Colors.grey, + ), + labelText: app.url, + ), + ), + SizedBox(height: dividerHeight), + Container(color: Resource.Colors.grey, height: 1), + SizedBox(height: 8.0), + FractionallySizedBox( + widthFactor: 0.3, + child: RaisedButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(30.0), + ), + ), + padding: EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 4.0, + ), + color: Resource.Colors.blueAccent, + onPressed: () async { + setState(() { + expireTime = null; + }); + }, + child: Text( + app.setNoExpireTime, + style: TextStyle(color: Colors.white), + ), + ), + ), + SizedBox(height: 8.0), + ListTile( + onTap: _pickStartDateTime, + contentPadding: EdgeInsets.symmetric( + horizontal: 24, + vertical: 8, + ), + leading: Icon( + AppIcon.accessTime, + size: 30, + color: Resource.Colors.grey, + ), + trailing: Icon( + AppIcon.keyboardArrowDown, + size: 30, + color: Resource.Colors.grey, + ), + title: Text( + app.expireTime, + style: TextStyle(fontSize: 20), + ), + subtitle: Text( + expireTime == null + ? app.newsExpireTimeHint + : dateFormat.format(expireTime), + style: TextStyle(fontSize: 20), + ), + ), + Container(color: Resource.Colors.grey, height: 1), + SizedBox(height: dividerHeight), + TextFormField( + maxLines: 2, + controller: _description, + validator: (value) { + if (value.isEmpty) { + return app.doNotEmpty; + } + return null; + }, + decoration: InputDecoration( + border: OutlineInputBorder(), + fillColor: Resource.Colors.blueAccent, + labelStyle: TextStyle( + color: Resource.Colors.grey, + ), + labelText: app.description, + ), + ), + SizedBox(height: 36), + FractionallySizedBox( + widthFactor: 0.8, + child: RaisedButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(30.0), + ), + ), + padding: EdgeInsets.all(14.0), + onPressed: () { + _announcementSubmit(); + }, + color: Resource.Colors.blueAccent, + child: Text( + widget.mode == Mode.add ? app.submit : app.update, + style: TextStyle( + color: Colors.white, + fontSize: 18.0, + ), + ), + ), + ), + SizedBox(height: 36), + ], + ), + ), + ); + } + + Future _pickStartDateTime() async { + DateTime dateTime = + this.expireTime ?? DateTime.now().add(Duration(days: 7)); + DateTime picked = await showDatePicker( + context: context, + locale: AppLocalizations.locale, + initialDate: dateTime ?? DateTime.now().add(Duration(days: 7)), + firstDate: DateTime(1950), + lastDate: DateTime(2099), + ); + TimeOfDay timeOfDay = + TimeOfDay(hour: dateTime.hour, minute: dateTime.minute); + timeOfDay = await showTimePicker(context: context, initialTime: timeOfDay); + if (picked != null && timeOfDay != null) { + setState( + () => this.expireTime = DateTime( + picked.year, + picked.month, + picked.day, + timeOfDay.hour, + timeOfDay.minute, + ), + ); + } + } + + void _announcementSubmit() async { + if (_formKey.currentState.validate()) { + Future instance; + announcements.title = _title.text; + announcements.description = _description.text; + announcements.imgUrl = _imgUrl.text; + announcements.url = _url.text; + announcements.weight = int.parse(_weight.text); + announcements.expireTime = + (expireTime == null) ? null : formatter.format(expireTime); + switch (widget.mode) { + case Mode.add: + instance = Helper.instance.addAnnouncement(announcements); + break; + case Mode.edit: + instance = Helper.instance.updateAnnouncement(announcements); + break; + } + instance.then((response) { + Navigator.of(context).pop(true); + Utils.showToast(context, app.addSuccess); + }).catchError((e) { + Utils.showToast(context, app.somethingError); + }); + } + } +} diff --git a/lib/pages/home/news_content_page.dart b/lib/pages/home/news_content_page.dart index bd0a71f2..8332f9c7 100644 --- a/lib/pages/home/news_content_page.dart +++ b/lib/pages/home/news_content_page.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:nkust_ap/models/announcements_data.dart'; import 'package:nkust_ap/res/app_icon.dart'; @@ -78,12 +79,14 @@ class NewsContentPageState extends State { aspectRatio: orientation == Orientation.portrait ? 4 / 3 : 9 / 16, child: Hero( tag: widget.news.hashCode, - child: (Platform.isIOS || Platform.isAndroid) - ? CachedNetworkImage( - imageUrl: widget.news.imgUrl, - errorWidget: (context, url, error) => Icon(Icons.error), - ) - : Image.network(widget.news.imgUrl), + child: kIsWeb + ? Image.network(widget.news.imgUrl) + : (Platform.isIOS || Platform.isAndroid) + ? CachedNetworkImage( + imageUrl: widget.news.imgUrl, + errorWidget: (context, url, error) => Icon(Icons.error), + ) + : Image.network(widget.news.imgUrl), ), ), onTap: () { diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 2900c620..2864640d 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -3,11 +3,13 @@ import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:dio/dio.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:nkust_ap/models/announcements_data.dart'; import 'package:nkust_ap/models/login_response.dart'; import 'package:nkust_ap/models/models.dart'; +import 'package:nkust_ap/pages/home/news/news_admin_page.dart'; import 'package:nkust_ap/res/app_icon.dart'; import 'package:nkust_ap/res/colors.dart' as Resource; import 'package:nkust_ap/utils/cache_utils.dart'; @@ -73,7 +75,18 @@ class HomePageState extends State { IconButton( icon: Icon(AppIcon.info), onPressed: _showInformationDialog, - ) + ), + if (ShareDataWidget.of(context).data.loginResponse?.isAdmin ?? + false) + IconButton( + icon: Icon(Icons.add_to_queue), + onPressed: () { + Utils.pushCupertinoStyle( + context, + NewsAdminPage(isAdmin: true), + ); + }, + ) ], ), drawer: DrawerBody( @@ -152,12 +165,14 @@ class HomePageState extends State { }, child: Hero( tag: announcement.hashCode, - child: (Platform.isIOS || Platform.isAndroid) - ? CachedNetworkImage( - imageUrl: announcement.imgUrl, - errorWidget: (context, url, error) => Icon(AppIcon.error), - ) - : Image.network(announcement.imgUrl), + child: kIsWeb + ? Image.network(announcement.imgUrl) + : (Platform.isIOS || Platform.isAndroid) + ? CachedNetworkImage( + imageUrl: announcement.imgUrl, + errorWidget: (context, url, error) => Icon(AppIcon.error), + ) + : Image.network(announcement.imgUrl), ), ), ); diff --git a/lib/utils/app_localizations.dart b/lib/utils/app_localizations.dart index f18f05ea..57114982 100644 --- a/lib/utils/app_localizations.dart +++ b/lib/utils/app_localizations.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:nkust_ap/config/constants.dart'; import 'package:nkust_ap/res/app_icon.dart'; import 'package:nkust_ap/res/app_theme.dart'; +import 'package:nkust_ap/utils/preferences.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'firebase_analytics_utils.dart'; @@ -407,6 +408,22 @@ class AppLocalizations { 'leaveSubmitFail': 'Oops Leaves Submit Fail!', 'loginSuccess': 'Login Success', 'retry': 'Retry', + 'title': 'Title', + 'description': 'Description', + 'imageUrl': 'Image Link', + 'url': 'Link', + 'expireTime': 'Expire Time', + 'weight': 'Weight', + 'newsContentFormat': + 'Weight:%d\nImage Link:%s\nLink:%s\nExpire Time:%s\nDescription:%s', + 'deleteNewsTitle': 'Delete News', + 'deleteNewsContent': 'Sure delete?', + 'deleteSuccess': 'Delete Success', + 'updateSuccess': 'Update Success', + 'formatError': 'Format Error', + 'newsExpireTimeHint': 'No expiration time, please pick time.', + 'setNoExpireTime': 'Set No Expiration Time', + 'noExpiration': 'No Expiration', }, 'zh': { 'app_name': '高科校務通', @@ -728,6 +745,21 @@ class AppLocalizations { 'leaveSubmitFail': 'Oops 請假送出失敗', 'loginSuccess': '登入成功', 'retry': '重試', + 'title': '標題', + 'description': '描述', + 'imageUrl': '圖片網址', + 'url': '連結網址', + 'expireTime': '到期時間', + 'weight': '權重', + 'newsContentFormat': '權重:%d\n圖片網址:%s\n連結網址:%s\n到期時間:%s\n描述:%s', + 'deleteNewsTitle': '刪除最新消息', + 'deleteNewsContent': '確定要刪除?', + 'deleteSuccess': '刪除成功', + 'updateSuccess': '更新成功', + 'formatError': '格式錯誤', + 'newsExpireTimeHint': '無到期時間 請選擇時間', + 'setNoExpireTime': '設定無到期時間', + 'noExpiration': '無到期時間', }, }; @@ -1305,6 +1337,36 @@ class AppLocalizations { String get loginSuccess => _vocabularies['loginSuccess']; String get retry => _vocabularies['retry']; + + String get title => _vocabularies['title']; + + String get description => _vocabularies['description']; + + String get imageUrl => _vocabularies['imageUrl']; + + String get url => _vocabularies['url']; + + String get expireTime => _vocabularies['expireTime']; + + String get weight => _vocabularies['weight']; + + String get newsContentFormat => _vocabularies['newsContentFormat']; + + String get deleteNewsTitle => _vocabularies['deleteNewsTitle']; + + String get deleteNewsContent => _vocabularies['deleteNewsContent']; + + String get deleteSuccess => _vocabularies['deleteSuccess']; + + String get updateSuccess => _vocabularies['updateSuccess']; + + String get formatError => _vocabularies['formatError']; + + String get newsExpireTimeHint => _vocabularies['newsExpireTimeHint']; + + String get setNoExpireTime => _vocabularies['setNoExpireTime']; + + String get noExpiration => _vocabularies['noExpiration']; } class AppLocalizationsDelegate extends LocalizationsDelegate { @@ -1316,27 +1378,16 @@ class AppLocalizationsDelegate extends LocalizationsDelegate { @override Future load(Locale locale) async { print('Load ${locale.languageCode}'); - if (kIsWeb) { - return AppLocalizations(locale); - } else if (Platform.isAndroid || Platform.isIOS) { - SharedPreferences preference = await SharedPreferences.getInstance(); - String languageCode = - preference.getString(Constants.PREF_LANGUAGE_CODE) ?? - AppLocalizations.SYSTEM; - AppLocalizations localizations = AppLocalizations( - (languageCode == AppLocalizations.SYSTEM) - ? locale - : Locale(languageCode)); - FA.setUserProperty( - 'language', - (languageCode == AppLocalizations.SYSTEM) - ? locale.languageCode - : languageCode); - return localizations; - } else { - //TODO if other platform can use SharedPreferences, need update. - return AppLocalizations(locale); - } + String languageCode = Preferences.getString( + Constants.PREF_LANGUAGE_CODE, AppLocalizations.SYSTEM); + FA.setUserProperty( + 'language', + (languageCode == AppLocalizations.SYSTEM) + ? locale.languageCode + : languageCode); + return AppLocalizations((languageCode == AppLocalizations.SYSTEM) + ? locale + : Locale(languageCode)); } @override diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 34b82f34..789064c7 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -414,7 +414,7 @@ class Utils { Preferences.setString( Constants.PREF_CURRENT_VERSION, packageInfo.buildNumber); } - if (Constants.isInDebugMode) { + if (!Constants.isInDebugMode) { final RemoteConfig remoteConfig = await RemoteConfig.instance; try { await remoteConfig.fetch( diff --git a/lib/widgets/share_data_widget.dart b/lib/widgets/share_data_widget.dart index 50d6b1cd..abe74525 100644 --- a/lib/widgets/share_data_widget.dart +++ b/lib/widgets/share_data_widget.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; +import 'package:nkust_ap/app.dart'; import 'package:nkust_ap/main.dart'; class ShareDataWidget extends InheritedWidget { final MyAppState data; - ShareDataWidget(this.data, {Widget child}) : super(child: child); + ShareDataWidget({this.data, Widget child}) : super(child: child); static ShareDataWidget of(BuildContext context) { return context.inheritFromWidgetOfExactType(ShareDataWidget); diff --git a/pubspec.yaml b/pubspec.yaml index 7338c2c2..72509343 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: nkust_ap description: A new Flutter application. -version: 3.2.10+30210 +version: 3.2.11+30211 environment: sdk: ">=2.2.2 <3.0.0"