diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 0e40f33..ad3d42e 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -195,6 +195,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, Base, ); @@ -244,24 +245,22 @@ buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${PODS_ROOT}/../.symlinks/flutter/ios/Flutter.framework", + "${PODS_ROOT}/../.symlinks/flutter/ios-release/Flutter.framework", "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", "${BUILT_PRODUCTS_DIR}/path_provider/path_provider.framework", + "${BUILT_PRODUCTS_DIR}/shared_preferences/shared_preferences.framework", "${BUILT_PRODUCTS_DIR}/url_launcher/url_launcher.framework", ); name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - ); outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher.framework", ); runOnlyForDeploymentPostprocessing = 0; @@ -274,15 +273,11 @@ buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", "${PODS_ROOT}/GoogleMaps/Maps/Frameworks/GoogleMaps.framework/Resources/GoogleMaps.bundle", ); name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - ); outputPaths = ( "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleMaps.bundle", ); @@ -430,6 +425,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.droidknights.flutterdroidknights; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; @@ -562,6 +558,7 @@ SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_SWIFT3_OBJC_INFERENCE = On; SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -590,6 +587,7 @@ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_SWIFT3_OBJC_INFERENCE = On; SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 78b4216..46247c7 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -29,8 +29,6 @@ UISupportedInterfaceOrientations UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad diff --git a/lib/bloc/schedule_like_bloc.dart b/lib/bloc/schedule_like_bloc.dart new file mode 100644 index 0000000..7ec86fa --- /dev/null +++ b/lib/bloc/schedule_like_bloc.dart @@ -0,0 +1,52 @@ +import 'dart:convert'; + +import 'package:rxdart/rxdart.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import './bloc_provider.dart'; + +class ScheduleLikeBloc implements BlocBase { + final _likeMap = BehaviorSubject>(); + Observable> get $likeMap => _likeMap.stream; + Base64Codec _base64 = Base64Codec(); + Utf8Codec _utf8 = Utf8Codec(); + Map _map = {}; + SharedPreferences prefs; + + ScheduleLikeBloc() { + init(); + } + + void init() async { + prefs = await SharedPreferences.getInstance(); + _map = json.decode(prefs.getString('dkf_schedule_like_map') ?? "{}"); + _likeMap.add(_map); + } + + Future addLike(String id) { + _map[toBase64(id)] = true; + _likeMap.add(_map); + prefs.setString('dkf_schedule_like_map', json.encode(_map)); + return prefs.commit(); + } + + Future removeLike(String id) { + _map.remove(toBase64(id)); + _likeMap.add(_map); + prefs.setString('dkf_schedule_like_map', json.encode(_map)); + return prefs.commit(); + } + + @override + void dispose() { + _likeMap.close(); + } + + String toBase64(String str) { + return _base64.encode(_utf8.encode(str)); + } + + String fromBase64(String str) { + return _base64.decode(str).toString(); + } +} diff --git a/lib/bloc/tab_bloc.dart b/lib/bloc/tab_bloc.dart deleted file mode 100644 index 280692a..0000000 --- a/lib/bloc/tab_bloc.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:rxdart/rxdart.dart'; - -import './bloc_provider.dart'; - -class TabBloc implements BlocBase{ - - final _bottomTab = BehaviorSubject(); - Observable get $bottomTab => _bottomTab.stream; - - /* - * start bottom tab from Info - */ - TabBloc() { - _bottomTab.add(0); - } - - /* - * change tab - */ - void changeBottomTab(int index) { - print(index); - _bottomTab.add(index); - } - - @override - void dispose() { - - } -} \ No newline at end of file diff --git a/lib/droidknightsapp_home.dart b/lib/droidknightsapp_home.dart index 10e7c1d..09633e2 100644 --- a/lib/droidknightsapp_home.dart +++ b/lib/droidknightsapp_home.dart @@ -3,32 +3,36 @@ import 'package:droidknights/pages/info_page.dart'; import 'package:droidknights/pages/schedule_page.dart'; import 'package:droidknights/res/strings.dart'; import 'package:flutter/material.dart'; - -import 'bloc/bloc_provider.dart'; -import 'bloc/tab_bloc.dart'; import 'package:flutter/cupertino.dart'; import 'dart:io' show Platform; -class DroidknightsAppHome extends StatelessWidget { + +class DroidknightsAppHome extends StatefulWidget { + @override + _DroidknightsAppHomeState createState() => _DroidknightsAppHomeState(); +} + +class _DroidknightsAppHomeState extends State { + + int _index = 0; + List _pages = [Platform.isAndroid ? InfoPage() : InfoIosPage(), SchedulePage()]; + @override Widget build(BuildContext context) { - final _tabBloc = BlocProvider.of(context); - return StreamBuilder( - stream: _tabBloc.$bottomTab, - builder: (context, snapshot) { - if (!snapshot.hasData) return Container(); - return Platform.isAndroid - ? createAndroidWidget(_tabBloc, snapshot) - : createIosWidget(); - }); + return Platform.isAndroid ? createAndroidWidget() : createIosWidget(); } - Widget createAndroidWidget(TabBloc _tabBloc, var snapshot) { + Widget createAndroidWidget() { + return Scaffold( - body: bodyPages(snapshot.data), + body: bodyPages(), bottomNavigationBar: BottomNavigationBar( - onTap: (int index) => _tabBloc.changeBottomTab(index), - currentIndex: snapshot.data, + onTap: (int index) => { + setState(() { + _index = index; + }) + }, + currentIndex: _index, items: [ BottomNavigationBarItem( icon: Icon(Icons.info), @@ -42,20 +46,36 @@ class DroidknightsAppHome extends StatelessWidget { )); } - Widget bodyPages(index) { - switch (index) { - case 0: - return Platform.isAndroid ? InfoPage() : InfoIosPage(); - case 1: - return SchedulePage(); - } - return null; + Widget bodyPages() { + return new Stack( + children: [ + new Offstage( + offstage: _index != 0, + child: new TickerMode( + enabled: _index == 0, + child: _pages[0], + ), + ), + new Offstage( + offstage: _index != 1, + child: new TickerMode( + enabled: _index == 1, + child: _pages[1], + ), + ), + ], + ); } Widget createIosWidget() { return CupertinoTabScaffold( backgroundColor: const Color(0xFF112030), tabBar: CupertinoTabBar( + onTap: (int index) => { + setState(() { + _index = index; + }) + }, backgroundColor: CupertinoColors.lightBackgroundGray, items: [ BottomNavigationBarItem( @@ -71,7 +91,7 @@ class DroidknightsAppHome extends StatelessWidget { tabBuilder: (context, index) { return CupertinoTabView( builder: (context) { - return bodyPages(index); + return bodyPages(); }, ); }, diff --git a/lib/main.dart b/lib/main.dart index 7fd27db..dc3b13e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,15 +4,7 @@ import 'package:flutter/material.dart'; import 'package:droidknights/droidknightsapp_home.dart'; import 'package:droidknights/res/strings.dart'; -import 'bloc/bloc_provider.dart'; -import 'bloc/tab_bloc.dart'; - -void main() => runApp( - BlocProvider( - bloc: TabBloc(), - child: MyApp() - ) -); +void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override diff --git a/lib/models/track_schedule.dart b/lib/models/track_schedule.dart index 3ab9db5..1bd04d8 100644 --- a/lib/models/track_schedule.dart +++ b/lib/models/track_schedule.dart @@ -1,18 +1,17 @@ -class ScheduleListModel{ +class ScheduleListModel { final List list; ScheduleListModel({this.list}); factory ScheduleListModel.fromJson(List parsedJson) { List lists = new List(); - lists = parsedJson.map((i)=>ScheduleModel.fromJson(i)).toList(); + lists = parsedJson.map((i) => ScheduleModel.fromJson(i)).toList(); - return ScheduleListModel( - list: lists - ); + return ScheduleListModel(list: lists); } } -class ScheduleModel{ + +class ScheduleModel { final int type; final String title; final String time; @@ -20,24 +19,29 @@ class ScheduleModel{ final String contents; List get names => speakers.map((speaker) => speaker.name).toList(); - List get avatarUrls => speakers.map((speaker) => speaker.avatarUrl).toList(); + List get avatarUrls => + speakers.map((speaker) => speaker.avatarUrl).toList(); - ScheduleModel({this.type, this.title, this.time, this.speakers, this.contents}); + ScheduleModel( + {this.type, this.title, this.time, this.speakers, this.contents}); - factory ScheduleModel.fromJson(Map parsedJson){ + factory ScheduleModel.fromJson(Map parsedJson) { final List speakers = (parsedJson['speakers'] as List) - ?.map((e) => e == null ? null : SpeakerModel.fromJson(e as Map)) - ?.toList() ?? []; + ?.map((e) => e == null + ? null + : SpeakerModel.fromJson(e as Map)) + ?.toList() ?? + []; return ScheduleModel( type: parsedJson['type'], title: parsedJson['title'], - time: parsedJson ['time'], + time: parsedJson['time'], speakers: speakers, - contents: parsedJson ['contents'] - ); + contents: parsedJson['contents']); } } -class SpeakerModel{ + +class SpeakerModel { final String name; final String avatarUrl; @@ -49,4 +53,4 @@ class SpeakerModel{ avatarUrl: parsedJson['avatarUrl'], ); } -} \ No newline at end of file +} diff --git a/lib/pages/schedule_page.dart b/lib/pages/schedule_page.dart index 52e231a..17a0c11 100644 --- a/lib/pages/schedule_page.dart +++ b/lib/pages/schedule_page.dart @@ -1,13 +1,17 @@ +import 'dart:io' show Platform; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:droidknights/bloc/schedule_like_bloc.dart'; import 'package:droidknights/models/schedule_service.dart'; import 'package:droidknights/models/track_schedule.dart'; import 'package:droidknights/pages/session_detail_dialog.dart'; import 'package:droidknights/res/strings.dart'; import 'package:flutter/material.dart'; -import 'dart:io' show Platform; class SchedulePage extends StatelessWidget { static final int ITEMVIEW_TYPE_NORMAL = 0; static final int ITEMVIEW_TYPE_SESSTION = 1; + final ScheduleLikeBloc _likeBloc = new ScheduleLikeBloc(); Widget scheduleAppbar() { return SliverAppBar( @@ -29,18 +33,17 @@ class SchedulePage extends StatelessWidget { } Widget androidAppBarTitle() => Image.asset( - Strings.SCHEDULE_TAB_IMAGES_APP_BAR, - fit: BoxFit.fitHeight, - height: 25, - ); + Strings.SCHEDULE_TAB_IMAGES_APP_BAR, + fit: BoxFit.fitHeight, + height: 25, + ); - Widget iosAppBarTitle() => - Text( - Strings.SCHEDULE_TAB_APPBAR_TITLE, - style: new TextStyle( - fontSize: 24.0, - fontWeight: FontWeight.w600, - ), + Widget iosAppBarTitle() => Text( + Strings.SCHEDULE_TAB_APPBAR_TITLE, + style: new TextStyle( + fontSize: 24.0, + fontWeight: FontWeight.w600, + ), ); @override @@ -49,9 +52,10 @@ class SchedulePage extends StatelessWidget { length: 3, child: Scaffold( body: NestedScrollView( - headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) => [ - scheduleAppbar(), - ], + headerSliverBuilder: + (BuildContext context, bool innerBoxIsScrolled) => [ + scheduleAppbar(), + ], body: TabBarView( children: [ trackScreen(Strings.SCHEDULE_TAB_JSON_TRACK_SCREEN1), @@ -60,8 +64,7 @@ class SchedulePage extends StatelessWidget { ], ), ), - ) - ); + )); } Widget trackScreen(String filePath) { @@ -82,14 +85,24 @@ class SchedulePage extends StatelessWidget { } Widget _itemView(context, data) { - if (data.type == ITEMVIEW_TYPE_SESSTION) { - return _showItemSection(context, data); - } else { - return _showItemNormal(context, data); - } + return StreamBuilder( + stream: _likeBloc.$likeMap, + builder: (context, snapshot) { + if (!snapshot.hasData) return Container(); + return data.type == ITEMVIEW_TYPE_SESSTION + ? _showItemSection( + context, + data, + snapshot.data[_likeBloc.toBase64(data.title + data.time)] == + null + ? false + : snapshot + .data[_likeBloc.toBase64(data.title + data.time)]) + : _showItemNormal(context, data); + }); } - Widget _showItemSection(context, data) { + Widget _showItemSection(context, data, liked) { return ListTile( leading: Text( data.time, @@ -114,19 +127,26 @@ class SchedulePage extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ - ClipOval( - child: FadeInImage.assetNetwork( - width: 56.0, - height: 56.0, - fadeInDuration: const Duration(seconds: 0), - fadeOutDuration: const Duration(seconds: 0), - image: data.avatarUrls.first, - placeholder: Platform.isAndroid ? Strings.IMAGES_DK_PROFILE : Strings.IMAGES_DK_IOS_PROFILE, - fit: BoxFit.fitHeight, + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: new Border.all(color: Colors.grey, width: 1)), + child: ClipOval( + child: FadeInImage( + width: 56.0, + height: 56.0, + fadeInDuration: const Duration(seconds: 0), + fadeOutDuration: const Duration(seconds: 0), + image: CachedNetworkImageProvider(data.avatarUrls.first), + placeholder: AssetImage(Platform.isAndroid + ? Strings.IMAGES_DK_PROFILE + : Strings.IMAGES_DK_IOS_PROFILE), + fit: BoxFit.fitHeight, + ), ), ), Padding(padding: const EdgeInsets.symmetric(horizontal: 6.0)), - Flexible( + Expanded( child: Container( constraints: BoxConstraints(minHeight: 60.0), child: Column( @@ -148,6 +168,13 @@ class SchedulePage extends StatelessWidget { ), ), ), + IconButton( + icon: Icon(liked ? Icons.favorite : Icons.favorite_border), + onPressed: () { + liked + ? _likeBloc.removeLike(data.title + data.time) + : _likeBloc.addLike(data.title + data.time); + }) ], ), ), diff --git a/lib/pages/session_detail_dialog.dart b/lib/pages/session_detail_dialog.dart index 3c52004..fd6d26f 100644 --- a/lib/pages/session_detail_dialog.dart +++ b/lib/pages/session_detail_dialog.dart @@ -1,9 +1,10 @@ +import 'dart:io' show Platform; import 'dart:math'; +import 'package:cached_network_image/cached_network_image.dart'; import 'package:droidknights/models/track_schedule.dart'; import 'package:droidknights/res/strings.dart'; import 'package:flutter/material.dart'; -import 'dart:io' show Platform; class SessionDetailDialog extends ModalRoute { final ScheduleModel sessionData; @@ -153,15 +154,15 @@ class SessionDetailDialog extends ModalRoute { Widget get profileImage { return new Hero( - tag: sessionData, - child: Row( - children: sessionData.speakers - .sublist(0, min(sessionData.speakers.length, 2)) - .map(avatarContainer) - .expand((w) => [w, Padding(padding: EdgeInsets.only(left: 8.0))]) - .toList()..removeLast(), - ) - ); + tag: sessionData, + child: Row( + children: sessionData.speakers + .sublist(0, min(sessionData.speakers.length, 2)) + .map(avatarContainer) + .expand((w) => [w, Padding(padding: EdgeInsets.only(left: 8.0))]) + .toList() + ..removeLast(), + )); } Widget avatarContainer(SpeakerModel speaker) { @@ -181,4 +182,4 @@ class SessionDetailDialog extends ModalRoute { onTap: () => {} ); } -} \ No newline at end of file +} diff --git a/lib/pages/splash_screen.dart b/lib/pages/splash_screen.dart index 0246da6..034852a 100644 --- a/lib/pages/splash_screen.dart +++ b/lib/pages/splash_screen.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:math'; import 'package:droidknights/const/route.dart'; @@ -7,6 +6,7 @@ import 'package:flutter/material.dart'; import 'dart:io' show Platform; const NUMBER_OF_STARS = 30; +const ANIMATION_TIME_MILL = 2500; class SplashScreen extends StatefulWidget { @@ -17,40 +17,33 @@ class SplashScreen extends StatefulWidget { } class SplashScreenState extends State with SingleTickerProviderStateMixin { + List> _animations; AnimationController _controller; - List> _topAndLefts; - - initState() { - try { - super.initState(); - - Random random = new Random(); - _topAndLefts = new List.generate(NUMBER_OF_STARS, (int i) => i) - .map((int i) => random.nextInt(500)) - .map((int left) => left.toDouble()) - .map((double left) => [left, random.nextInt(1000)]) - .map((size) => {'left': size[0], 'top': size[1] / 3.5}) - .toList(); + List> _topAndLefts; - _initAnimation(); + @override + void initState() { + super.initState(); - Timer(Duration(milliseconds: 2500), () async { - Navigator.pushReplacementNamed(context, Routes.HOME); - }); - } catch (e) { - print(e.toString()); - Navigator.pushReplacementNamed(context, Routes.HOME); - } + Random random = new Random(); + _topAndLefts = new List.generate(NUMBER_OF_STARS, (int i) => i) + .map((int i) => Point(random.nextInt(500).toDouble(), random.nextInt(1000) / 3.5)) + .toList(); + + _initAnimation(); } - dispose() { - _disposeAnimation(); - super.dispose(); + void animationListener(AnimationStatus status) { + if(status == AnimationStatus.completed) { + Navigator.pushReplacementNamed(context, Routes.HOME); + } } void _initAnimation() { - _controller = AnimationController(duration: Duration(milliseconds: 2000), vsync: this); + _controller = AnimationController(duration: Duration(milliseconds: ANIMATION_TIME_MILL), vsync: this); + + _controller.addStatusListener(animationListener); _animations = [ makeTweenAnimation(controller: _controller, begin: 0.2, end: 0.6), @@ -60,6 +53,12 @@ class SplashScreenState extends State with SingleTickerProviderSta _controller.forward(); } + @override + void dispose() { + _disposeAnimation(); + super.dispose(); + } + void _disposeAnimation() { _controller.dispose(); } @@ -122,8 +121,8 @@ class SplashScreenState extends State with SingleTickerProviderSta List _buildStars() => _topAndLefts.map((topAndLeft) => Positioned( - top: calculateAnimationValue(_animations[1], topAndLeft['top'] - 20, topAndLeft['top']), - left: topAndLeft['left'], + top: calculateAnimationValue(_animations[1], topAndLeft.y - 20, topAndLeft.y), + left: topAndLeft.x, child: Image.asset( 'assets/images/dk19_appicon_star.png', width: 6, diff --git a/pubspec.yaml b/pubspec.yaml index 24be7df..d204c78 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,7 +7,7 @@ description: A new Flutter application for DroidKnights Festival 2019. # Both the version and the builder number may be overridden in flutter # build by specifying --build-name and --build-number, respectively. # Read more about versioning at semver.org. -version: 1.2.0+4 +version: 1.2.1+5 environment: sdk: ">=2.1.0 <3.0.0" @@ -23,6 +23,8 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^0.1.2 rxdart: ^0.20.0 + shared_preferences: 0.4.0 + cached_network_image: 0.5.0 dev_dependencies: flutter_test: