├── .gitignore ├── .metadata ├── LICENSE ├── README.md ├── android ├── .project ├── .settings │ └── org.eclipse.buildship.core.prefs ├── app │ ├── .classpath │ ├── .project │ ├── .settings │ │ └── org.eclipse.buildship.core.prefs │ ├── build.gradle │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── briefing │ │ │ └── MainActivity.java │ │ └── res │ │ ├── drawable │ │ └── launch_background.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ └── values │ │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets ├── fonts │ ├── Crimson_Text │ │ ├── CrimsonText-Bold.ttf │ │ ├── CrimsonText-BoldItalic.ttf │ │ ├── CrimsonText-Italic.ttf │ │ ├── CrimsonText-Regular.ttf │ │ ├── CrimsonText-SemiBold.ttf │ │ ├── CrimsonText-SemiBoldItalic.ttf │ │ └── OFL.txt │ └── Libre_Franklin │ │ ├── LibreFranklin-Bold.ttf │ │ └── LibreFranklin-Regular.ttf └── images │ └── no_internet.png ├── ios ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ └── contents.xcworkspacedata └── Runner │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-1024x1024@1x.png │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ └── Icon-App-83.5x83.5@2x.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── main.m ├── lib ├── bloc │ ├── bloc_article.dart │ ├── bloc_bookmark_article.dart │ └── bloc_provider.dart ├── bookmarked_article_list.dart ├── briefing_card.dart ├── briefing_sliver_list.dart ├── main.dart ├── model │ └── article.dart ├── repository │ └── repository.dart ├── service │ ├── api_service.dart │ └── database.dart ├── theme │ ├── colors.dart │ └── theme.dart ├── util │ └── rate_limiter.dart └── widget │ ├── app_menu.dart │ ├── article_bottom_section.dart │ ├── article_menu.dart │ ├── article_thumbnail.dart │ ├── article_title_section.dart │ └── main_sliverappbar.dart ├── pubspec.yaml ├── screenshots ├── ui_bottomsheet.jpg ├── ui_list.jpg └── ui_main_list.jpg └── test └── widget_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.lock 4 | *.log 5 | *.pyc 6 | *.swp 7 | .DS_Store 8 | .atom/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | .gitignore 13 | 14 | # IntelliJ related 15 | *.iml 16 | *.ipr 17 | *.iws 18 | .idea/ 19 | 20 | # Visual Studio Code related 21 | .vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .packages 28 | .pub-cache/ 29 | .pub/ 30 | build/ 31 | .pubspec.lock 32 | 33 | # Android related 34 | **/android/**/gradle-wrapper.jar 35 | **/android/.gradle 36 | **/android/captures/ 37 | **/android/gradlew 38 | **/android/gradlew.bat 39 | **/android/local.properties 40 | **/android/**/GeneratedPluginRegistrant.java 41 | 42 | # iOS/XCode related 43 | **/ios/**/*.mode1v3 44 | **/ios/**/*.mode2v3 45 | **/ios/**/*.moved-aside 46 | **/ios/**/*.pbxuser 47 | **/ios/**/*.perspectivev3 48 | **/ios/**/*sync/ 49 | **/ios/**/.sconsign.dblite 50 | **/ios/**/.tags* 51 | **/ios/**/.vagrant/ 52 | **/ios/**/DerivedData/ 53 | **/ios/**/Icon? 54 | **/ios/**/Pods/ 55 | **/ios/**/.symlinks/ 56 | **/ios/**/profile 57 | **/ios/**/xcuserdata 58 | **/ios/.generated/ 59 | **/ios/Flutter/App.framework 60 | **/ios/Flutter/Flutter.framework 61 | **/ios/Flutter/Generated.xcconfig 62 | **/ios/Flutter/app.flx 63 | **/ios/Flutter/app.zip 64 | **/ios/Flutter/flutter_assets/ 65 | **/ios/ServiceDefinitions.json 66 | **/ios/Runner/GeneratedPluginRegistrant.* 67 | 68 | # Exceptions to above rules. 69 | !**/ios/**/default.mode1v3 70 | !**/ios/**/default.mode2v3 71 | !**/ios/**/default.pbxuser 72 | !**/ios/**/default.perspectivev3 73 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 74 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 5391447fae6209bb21a89e6a5a6583cac1af9b4b 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Kakiang Kyanguinbo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Briefing 2 | 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 4 | 5 | An attempt to clone part of Google News app design using Flutter 6 | 7 | > Hard-coded data 8 | 9 | ## Screenshots 10 |
11 |

12 | 13 |   14 | 15 |   16 | 17 |

18 |
19 | -------------------------------------------------------------------------------- /android/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | android 4 | Project android created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.buildship.core.gradleprojectbuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.buildship.core.gradleprojectnature 16 | 17 | 18 | -------------------------------------------------------------------------------- /android/.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | connection.project.dir= 2 | eclipse.preferences.version=1 3 | -------------------------------------------------------------------------------- /android/app/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/app/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | app 4 | Project app created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.buildship.core.gradleprojectbuilder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.jdt.core.javanature 21 | org.eclipse.buildship.core.gradleprojectnature 22 | 23 | 24 | -------------------------------------------------------------------------------- /android/app/.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | connection.project.dir=.. 2 | eclipse.preferences.version=1 3 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 26 | 27 | android { 28 | compileSdkVersion 28 29 | 30 | lintOptions { 31 | disable 'InvalidPackage' 32 | } 33 | 34 | defaultConfig { 35 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 36 | applicationId "com.kakiang.briefing" 37 | minSdkVersion 16 38 | targetSdkVersion 28 39 | versionCode flutterVersionCode.toInteger() 40 | versionName flutterVersionName 41 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 42 | } 43 | 44 | buildTypes { 45 | release { 46 | // TODO: Add your own signing config for the release build. 47 | // Signing with the debug keys for now, so `flutter run --release` works. 48 | signingConfig signingConfigs.debug 49 | } 50 | } 51 | } 52 | 53 | flutter { 54 | source '../..' 55 | } 56 | 57 | dependencies { 58 | testImplementation 'junit:junit:4.12' 59 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 60 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 61 | } 62 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 10 | 15 | 19 | 26 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/example/briefing/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.briefing; 2 | 3 | import android.os.Bundle; 4 | import io.flutter.app.FlutterActivity; 5 | import io.flutter.plugins.GeneratedPluginRegistrant; 6 | 7 | public class MainActivity extends FlutterActivity { 8 | @Override 9 | protected void onCreate(Bundle savedInstanceState) { 10 | super.onCreate(savedInstanceState); 11 | GeneratedPluginRegistrant.registerWith(this); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakiang/briefing/bbab6e8743c4a749a873fce4b9c46b2b4415a62a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakiang/briefing/bbab6e8743c4a749a873fce4b9c46b2b4415a62a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakiang/briefing/bbab6e8743c4a749a873fce4b9c46b2b4415a62a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakiang/briefing/bbab6e8743c4a749a873fce4b9c46b2b4415a62a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakiang/briefing/bbab6e8743c4a749a873fce4b9c46b2b4415a62a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:3.2.1' 9 | // classpath 'com.android.tools.build:gradle:2.2.0' 10 | } 11 | } 12 | 13 | allprojects { 14 | repositories { 15 | google() 16 | jcenter() 17 | } 18 | } 19 | 20 | rootProject.buildDir = '../build' 21 | subprojects { 22 | project.buildDir = "${rootProject.buildDir}/${project.name}" 23 | } 24 | subprojects { 25 | project.evaluationDependsOn(':app') 26 | } 27 | 28 | task clean(type: Delete) { 29 | delete rootProject.buildDir 30 | } 31 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | # distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-all.zip 7 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip 8 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /assets/fonts/Crimson_Text/CrimsonText-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakiang/briefing/bbab6e8743c4a749a873fce4b9c46b2b4415a62a/assets/fonts/Crimson_Text/CrimsonText-Bold.ttf -------------------------------------------------------------------------------- /assets/fonts/Crimson_Text/CrimsonText-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakiang/briefing/bbab6e8743c4a749a873fce4b9c46b2b4415a62a/assets/fonts/Crimson_Text/CrimsonText-BoldItalic.ttf -------------------------------------------------------------------------------- /assets/fonts/Crimson_Text/CrimsonText-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakiang/briefing/bbab6e8743c4a749a873fce4b9c46b2b4415a62a/assets/fonts/Crimson_Text/CrimsonText-Italic.ttf -------------------------------------------------------------------------------- /assets/fonts/Crimson_Text/CrimsonText-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakiang/briefing/bbab6e8743c4a749a873fce4b9c46b2b4415a62a/assets/fonts/Crimson_Text/CrimsonText-Regular.ttf -------------------------------------------------------------------------------- /assets/fonts/Crimson_Text/CrimsonText-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakiang/briefing/bbab6e8743c4a749a873fce4b9c46b2b4415a62a/assets/fonts/Crimson_Text/CrimsonText-SemiBold.ttf -------------------------------------------------------------------------------- /assets/fonts/Crimson_Text/CrimsonText-SemiBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakiang/briefing/bbab6e8743c4a749a873fce4b9c46b2b4415a62a/assets/fonts/Crimson_Text/CrimsonText-SemiBoldItalic.ttf -------------------------------------------------------------------------------- /assets/fonts/Crimson_Text/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, Sebastian Kosch (sebastian@aldusleaf.org), 2 | with Reserved Font Name "Crimson" and "Crimson Text". 3 | 4 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 5 | This license is copied below, and is also available with a FAQ at: 6 | http://scripts.sil.org/OFL 7 | 8 | 9 | ----------------------------------------------------------- 10 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 11 | ----------------------------------------------------------- 12 | 13 | PREAMBLE 14 | The goals of the Open Font License (OFL) are to stimulate worldwide 15 | development of collaborative font projects, to support the font creation 16 | efforts of academic and linguistic communities, and to provide a free and 17 | open framework in which fonts may be shared and improved in partnership 18 | with others. 19 | 20 | The OFL allows the licensed fonts to be used, studied, modified and 21 | redistributed freely as long as they are not sold by themselves. The 22 | fonts, including any derivative works, can be bundled, embedded, 23 | redistributed and/or sold with any software provided that any reserved 24 | names are not used by derivative works. The fonts and derivatives, 25 | however, cannot be released under any other type of license. The 26 | requirement for fonts to remain under this license does not apply 27 | to any document created using the fonts or their derivatives. 28 | 29 | DEFINITIONS 30 | "Font Software" refers to the set of files released by the Copyright 31 | Holder(s) under this license and clearly marked as such. This may 32 | include source files, build scripts and documentation. 33 | 34 | "Reserved Font Name" refers to any names specified as such after the 35 | copyright statement(s). 36 | 37 | "Original Version" refers to the collection of Font Software components as 38 | distributed by the Copyright Holder(s). 39 | 40 | "Modified Version" refers to any derivative made by adding to, deleting, 41 | or substituting -- in part or in whole -- any of the components of the 42 | Original Version, by changing formats or by porting the Font Software to a 43 | new environment. 44 | 45 | "Author" refers to any designer, engineer, programmer, technical 46 | writer or other person who contributed to the Font Software. 47 | 48 | PERMISSION & CONDITIONS 49 | Permission is hereby granted, free of charge, to any person obtaining 50 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 51 | redistribute, and sell modified and unmodified copies of the Font 52 | Software, subject to the following conditions: 53 | 54 | 1) Neither the Font Software nor any of its individual components, 55 | in Original or Modified Versions, may be sold by itself. 56 | 57 | 2) Original or Modified Versions of the Font Software may be bundled, 58 | redistributed and/or sold with any software, provided that each copy 59 | contains the above copyright notice and this license. These can be 60 | included either as stand-alone text files, human-readable headers or 61 | in the appropriate machine-readable metadata fields within text or 62 | binary files as long as those fields can be easily viewed by the user. 63 | 64 | 3) No Modified Version of the Font Software may use the Reserved Font 65 | Name(s) unless explicit written permission is granted by the corresponding 66 | Copyright Holder. This restriction only applies to the primary font name as 67 | presented to the users. 68 | 69 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 70 | Software shall not be used to promote, endorse or advertise any 71 | Modified Version, except to acknowledge the contribution(s) of the 72 | Copyright Holder(s) and the Author(s) or with their explicit written 73 | permission. 74 | 75 | 5) The Font Software, modified or unmodified, in part or in whole, 76 | must be distributed entirely under this license, and must not be 77 | distributed under any other license. The requirement for fonts to 78 | remain under this license does not apply to any document created 79 | using the Font Software. 80 | 81 | TERMINATION 82 | This license becomes null and void if any of the above conditions are 83 | not met. 84 | 85 | DISCLAIMER 86 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 87 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 88 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 89 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 90 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 91 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 92 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 93 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 94 | OTHER DEALINGS IN THE FONT SOFTWARE. 95 | -------------------------------------------------------------------------------- /assets/fonts/Libre_Franklin/LibreFranklin-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakiang/briefing/bbab6e8743c4a749a873fce4b9c46b2b4415a62a/assets/fonts/Libre_Franklin/LibreFranklin-Bold.ttf -------------------------------------------------------------------------------- /assets/fonts/Libre_Franklin/LibreFranklin-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakiang/briefing/bbab6e8743c4a749a873fce4b9c46b2b4415a62a/assets/fonts/Libre_Franklin/LibreFranklin-Regular.ttf -------------------------------------------------------------------------------- /assets/images/no_internet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakiang/briefing/bbab6e8743c4a749a873fce4b9c46b2b4415a62a/assets/images/no_internet.png -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAA1A9400D5DBA9 /* flutter_assets */; }; 12 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 13 | 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; 14 | 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 15 | 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; 16 | 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 17 | 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; 18 | 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 19 | 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 20 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 21 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 22 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 23 | /* End PBXBuildFile section */ 24 | 25 | /* Begin PBXCopyFilesBuildPhase section */ 26 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 27 | isa = PBXCopyFilesBuildPhase; 28 | buildActionMask = 2147483647; 29 | dstPath = ""; 30 | dstSubfolderSpec = 10; 31 | files = ( 32 | 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, 33 | 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, 34 | ); 35 | name = "Embed Frameworks"; 36 | runOnlyForDeploymentPostprocessing = 0; 37 | }; 38 | /* End PBXCopyFilesBuildPhase section */ 39 | 40 | /* Begin PBXFileReference section */ 41 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 42 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 43 | 2D5378251FAA1A9400D5DBA9 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = flutter_assets; path = Flutter/flutter_assets; sourceTree = SOURCE_ROOT; }; 44 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 45 | 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; 46 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 47 | 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 48 | 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 49 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 50 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 51 | 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; 52 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 53 | 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 54 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 55 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 56 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 57 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 58 | /* End PBXFileReference section */ 59 | 60 | /* Begin PBXFrameworksBuildPhase section */ 61 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 62 | isa = PBXFrameworksBuildPhase; 63 | buildActionMask = 2147483647; 64 | files = ( 65 | 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, 66 | 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, 67 | ); 68 | runOnlyForDeploymentPostprocessing = 0; 69 | }; 70 | /* End PBXFrameworksBuildPhase section */ 71 | 72 | /* Begin PBXGroup section */ 73 | 9740EEB11CF90186004384FC /* Flutter */ = { 74 | isa = PBXGroup; 75 | children = ( 76 | 2D5378251FAA1A9400D5DBA9 /* flutter_assets */, 77 | 3B80C3931E831B6300D905FE /* App.framework */, 78 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 79 | 9740EEBA1CF902C7004384FC /* Flutter.framework */, 80 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 81 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 82 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 83 | ); 84 | name = Flutter; 85 | sourceTree = ""; 86 | }; 87 | 97C146E51CF9000F007C117D = { 88 | isa = PBXGroup; 89 | children = ( 90 | 9740EEB11CF90186004384FC /* Flutter */, 91 | 97C146F01CF9000F007C117D /* Runner */, 92 | 97C146EF1CF9000F007C117D /* Products */, 93 | CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, 94 | ); 95 | sourceTree = ""; 96 | }; 97 | 97C146EF1CF9000F007C117D /* Products */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | 97C146EE1CF9000F007C117D /* Runner.app */, 101 | ); 102 | name = Products; 103 | sourceTree = ""; 104 | }; 105 | 97C146F01CF9000F007C117D /* Runner */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, 109 | 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, 110 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 111 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 112 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 113 | 97C147021CF9000F007C117D /* Info.plist */, 114 | 97C146F11CF9000F007C117D /* Supporting Files */, 115 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 116 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 117 | ); 118 | path = Runner; 119 | sourceTree = ""; 120 | }; 121 | 97C146F11CF9000F007C117D /* Supporting Files */ = { 122 | isa = PBXGroup; 123 | children = ( 124 | 97C146F21CF9000F007C117D /* main.m */, 125 | ); 126 | name = "Supporting Files"; 127 | sourceTree = ""; 128 | }; 129 | /* End PBXGroup section */ 130 | 131 | /* Begin PBXNativeTarget section */ 132 | 97C146ED1CF9000F007C117D /* Runner */ = { 133 | isa = PBXNativeTarget; 134 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 135 | buildPhases = ( 136 | 9740EEB61CF901F6004384FC /* Run Script */, 137 | 97C146EA1CF9000F007C117D /* Sources */, 138 | 97C146EB1CF9000F007C117D /* Frameworks */, 139 | 97C146EC1CF9000F007C117D /* Resources */, 140 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 141 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 142 | ); 143 | buildRules = ( 144 | ); 145 | dependencies = ( 146 | ); 147 | name = Runner; 148 | productName = Runner; 149 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 150 | productType = "com.apple.product-type.application"; 151 | }; 152 | /* End PBXNativeTarget section */ 153 | 154 | /* Begin PBXProject section */ 155 | 97C146E61CF9000F007C117D /* Project object */ = { 156 | isa = PBXProject; 157 | attributes = { 158 | LastUpgradeCheck = 0910; 159 | ORGANIZATIONNAME = "The Chromium Authors"; 160 | TargetAttributes = { 161 | 97C146ED1CF9000F007C117D = { 162 | CreatedOnToolsVersion = 7.3.1; 163 | }; 164 | }; 165 | }; 166 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 167 | compatibilityVersion = "Xcode 3.2"; 168 | developmentRegion = English; 169 | hasScannedForEncodings = 0; 170 | knownRegions = ( 171 | en, 172 | Base, 173 | ); 174 | mainGroup = 97C146E51CF9000F007C117D; 175 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 176 | projectDirPath = ""; 177 | projectRoot = ""; 178 | targets = ( 179 | 97C146ED1CF9000F007C117D /* Runner */, 180 | ); 181 | }; 182 | /* End PBXProject section */ 183 | 184 | /* Begin PBXResourcesBuildPhase section */ 185 | 97C146EC1CF9000F007C117D /* Resources */ = { 186 | isa = PBXResourcesBuildPhase; 187 | buildActionMask = 2147483647; 188 | files = ( 189 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 190 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 191 | 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, 192 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 193 | 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */, 194 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 195 | ); 196 | runOnlyForDeploymentPostprocessing = 0; 197 | }; 198 | /* End PBXResourcesBuildPhase section */ 199 | 200 | /* Begin PBXShellScriptBuildPhase section */ 201 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 202 | isa = PBXShellScriptBuildPhase; 203 | buildActionMask = 2147483647; 204 | files = ( 205 | ); 206 | inputPaths = ( 207 | ); 208 | name = "Thin Binary"; 209 | outputPaths = ( 210 | ); 211 | runOnlyForDeploymentPostprocessing = 0; 212 | shellPath = /bin/sh; 213 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; 214 | }; 215 | 9740EEB61CF901F6004384FC /* Run Script */ = { 216 | isa = PBXShellScriptBuildPhase; 217 | buildActionMask = 2147483647; 218 | files = ( 219 | ); 220 | inputPaths = ( 221 | ); 222 | name = "Run Script"; 223 | outputPaths = ( 224 | ); 225 | runOnlyForDeploymentPostprocessing = 0; 226 | shellPath = /bin/sh; 227 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 228 | }; 229 | /* End PBXShellScriptBuildPhase section */ 230 | 231 | /* Begin PBXSourcesBuildPhase section */ 232 | 97C146EA1CF9000F007C117D /* Sources */ = { 233 | isa = PBXSourcesBuildPhase; 234 | buildActionMask = 2147483647; 235 | files = ( 236 | 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, 237 | 97C146F31CF9000F007C117D /* main.m in Sources */, 238 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 239 | ); 240 | runOnlyForDeploymentPostprocessing = 0; 241 | }; 242 | /* End PBXSourcesBuildPhase section */ 243 | 244 | /* Begin PBXVariantGroup section */ 245 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 246 | isa = PBXVariantGroup; 247 | children = ( 248 | 97C146FB1CF9000F007C117D /* Base */, 249 | ); 250 | name = Main.storyboard; 251 | sourceTree = ""; 252 | }; 253 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 254 | isa = PBXVariantGroup; 255 | children = ( 256 | 97C147001CF9000F007C117D /* Base */, 257 | ); 258 | name = LaunchScreen.storyboard; 259 | sourceTree = ""; 260 | }; 261 | /* End PBXVariantGroup section */ 262 | 263 | /* Begin XCBuildConfiguration section */ 264 | 249021D3217E4FDB00AE95B9 /* Profile */ = { 265 | isa = XCBuildConfiguration; 266 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 267 | buildSettings = { 268 | ALWAYS_SEARCH_USER_PATHS = NO; 269 | CLANG_ANALYZER_NONNULL = YES; 270 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 271 | CLANG_CXX_LIBRARY = "libc++"; 272 | CLANG_ENABLE_MODULES = YES; 273 | CLANG_ENABLE_OBJC_ARC = YES; 274 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 275 | CLANG_WARN_BOOL_CONVERSION = YES; 276 | CLANG_WARN_COMMA = YES; 277 | CLANG_WARN_CONSTANT_CONVERSION = YES; 278 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 279 | CLANG_WARN_EMPTY_BODY = YES; 280 | CLANG_WARN_ENUM_CONVERSION = YES; 281 | CLANG_WARN_INFINITE_RECURSION = YES; 282 | CLANG_WARN_INT_CONVERSION = YES; 283 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 284 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 285 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 286 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 287 | CLANG_WARN_STRICT_PROTOTYPES = YES; 288 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 289 | CLANG_WARN_UNREACHABLE_CODE = YES; 290 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 291 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 292 | COPY_PHASE_STRIP = NO; 293 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 294 | ENABLE_NS_ASSERTIONS = NO; 295 | ENABLE_STRICT_OBJC_MSGSEND = YES; 296 | GCC_C_LANGUAGE_STANDARD = gnu99; 297 | GCC_NO_COMMON_BLOCKS = YES; 298 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 299 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 300 | GCC_WARN_UNDECLARED_SELECTOR = YES; 301 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 302 | GCC_WARN_UNUSED_FUNCTION = YES; 303 | GCC_WARN_UNUSED_VARIABLE = YES; 304 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 305 | MTL_ENABLE_DEBUG_INFO = NO; 306 | SDKROOT = iphoneos; 307 | TARGETED_DEVICE_FAMILY = "1,2"; 308 | VALIDATE_PRODUCT = YES; 309 | }; 310 | name = Profile; 311 | }; 312 | 249021D4217E4FDB00AE95B9 /* Profile */ = { 313 | isa = XCBuildConfiguration; 314 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 315 | buildSettings = { 316 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 317 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 318 | DEVELOPMENT_TEAM = S8QB4VV633; 319 | ENABLE_BITCODE = NO; 320 | FRAMEWORK_SEARCH_PATHS = ( 321 | "$(inherited)", 322 | "$(PROJECT_DIR)/Flutter", 323 | ); 324 | INFOPLIST_FILE = Runner/Info.plist; 325 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 326 | LIBRARY_SEARCH_PATHS = ( 327 | "$(inherited)", 328 | "$(PROJECT_DIR)/Flutter", 329 | ); 330 | PRODUCT_BUNDLE_IDENTIFIER = com.example.briefing; 331 | PRODUCT_NAME = "$(TARGET_NAME)"; 332 | VERSIONING_SYSTEM = "apple-generic"; 333 | }; 334 | name = Profile; 335 | }; 336 | 97C147031CF9000F007C117D /* Debug */ = { 337 | isa = XCBuildConfiguration; 338 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 339 | buildSettings = { 340 | ALWAYS_SEARCH_USER_PATHS = NO; 341 | CLANG_ANALYZER_NONNULL = YES; 342 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 343 | CLANG_CXX_LIBRARY = "libc++"; 344 | CLANG_ENABLE_MODULES = YES; 345 | CLANG_ENABLE_OBJC_ARC = YES; 346 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 347 | CLANG_WARN_BOOL_CONVERSION = YES; 348 | CLANG_WARN_COMMA = YES; 349 | CLANG_WARN_CONSTANT_CONVERSION = YES; 350 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 351 | CLANG_WARN_EMPTY_BODY = YES; 352 | CLANG_WARN_ENUM_CONVERSION = YES; 353 | CLANG_WARN_INFINITE_RECURSION = YES; 354 | CLANG_WARN_INT_CONVERSION = YES; 355 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 356 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 357 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 358 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 359 | CLANG_WARN_STRICT_PROTOTYPES = YES; 360 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 361 | CLANG_WARN_UNREACHABLE_CODE = YES; 362 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 363 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 364 | COPY_PHASE_STRIP = NO; 365 | DEBUG_INFORMATION_FORMAT = dwarf; 366 | ENABLE_STRICT_OBJC_MSGSEND = YES; 367 | ENABLE_TESTABILITY = YES; 368 | GCC_C_LANGUAGE_STANDARD = gnu99; 369 | GCC_DYNAMIC_NO_PIC = NO; 370 | GCC_NO_COMMON_BLOCKS = YES; 371 | GCC_OPTIMIZATION_LEVEL = 0; 372 | GCC_PREPROCESSOR_DEFINITIONS = ( 373 | "DEBUG=1", 374 | "$(inherited)", 375 | ); 376 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 377 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 378 | GCC_WARN_UNDECLARED_SELECTOR = YES; 379 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 380 | GCC_WARN_UNUSED_FUNCTION = YES; 381 | GCC_WARN_UNUSED_VARIABLE = YES; 382 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 383 | MTL_ENABLE_DEBUG_INFO = YES; 384 | ONLY_ACTIVE_ARCH = YES; 385 | SDKROOT = iphoneos; 386 | TARGETED_DEVICE_FAMILY = "1,2"; 387 | }; 388 | name = Debug; 389 | }; 390 | 97C147041CF9000F007C117D /* Release */ = { 391 | isa = XCBuildConfiguration; 392 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 393 | buildSettings = { 394 | ALWAYS_SEARCH_USER_PATHS = NO; 395 | CLANG_ANALYZER_NONNULL = YES; 396 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 397 | CLANG_CXX_LIBRARY = "libc++"; 398 | CLANG_ENABLE_MODULES = YES; 399 | CLANG_ENABLE_OBJC_ARC = YES; 400 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 401 | CLANG_WARN_BOOL_CONVERSION = YES; 402 | CLANG_WARN_COMMA = YES; 403 | CLANG_WARN_CONSTANT_CONVERSION = YES; 404 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 405 | CLANG_WARN_EMPTY_BODY = YES; 406 | CLANG_WARN_ENUM_CONVERSION = YES; 407 | CLANG_WARN_INFINITE_RECURSION = YES; 408 | CLANG_WARN_INT_CONVERSION = YES; 409 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 410 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 411 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 412 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 413 | CLANG_WARN_STRICT_PROTOTYPES = YES; 414 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 415 | CLANG_WARN_UNREACHABLE_CODE = YES; 416 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 417 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 418 | COPY_PHASE_STRIP = NO; 419 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 420 | ENABLE_NS_ASSERTIONS = NO; 421 | ENABLE_STRICT_OBJC_MSGSEND = YES; 422 | GCC_C_LANGUAGE_STANDARD = gnu99; 423 | GCC_NO_COMMON_BLOCKS = YES; 424 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 425 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 426 | GCC_WARN_UNDECLARED_SELECTOR = YES; 427 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 428 | GCC_WARN_UNUSED_FUNCTION = YES; 429 | GCC_WARN_UNUSED_VARIABLE = YES; 430 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 431 | MTL_ENABLE_DEBUG_INFO = NO; 432 | SDKROOT = iphoneos; 433 | TARGETED_DEVICE_FAMILY = "1,2"; 434 | VALIDATE_PRODUCT = YES; 435 | }; 436 | name = Release; 437 | }; 438 | 97C147061CF9000F007C117D /* Debug */ = { 439 | isa = XCBuildConfiguration; 440 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 441 | buildSettings = { 442 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 443 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 444 | ENABLE_BITCODE = NO; 445 | FRAMEWORK_SEARCH_PATHS = ( 446 | "$(inherited)", 447 | "$(PROJECT_DIR)/Flutter", 448 | ); 449 | INFOPLIST_FILE = Runner/Info.plist; 450 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 451 | LIBRARY_SEARCH_PATHS = ( 452 | "$(inherited)", 453 | "$(PROJECT_DIR)/Flutter", 454 | ); 455 | PRODUCT_BUNDLE_IDENTIFIER = com.example.briefing; 456 | PRODUCT_NAME = "$(TARGET_NAME)"; 457 | VERSIONING_SYSTEM = "apple-generic"; 458 | }; 459 | name = Debug; 460 | }; 461 | 97C147071CF9000F007C117D /* Release */ = { 462 | isa = XCBuildConfiguration; 463 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 464 | buildSettings = { 465 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 466 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 467 | ENABLE_BITCODE = NO; 468 | FRAMEWORK_SEARCH_PATHS = ( 469 | "$(inherited)", 470 | "$(PROJECT_DIR)/Flutter", 471 | ); 472 | INFOPLIST_FILE = Runner/Info.plist; 473 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 474 | LIBRARY_SEARCH_PATHS = ( 475 | "$(inherited)", 476 | "$(PROJECT_DIR)/Flutter", 477 | ); 478 | PRODUCT_BUNDLE_IDENTIFIER = com.example.briefing; 479 | PRODUCT_NAME = "$(TARGET_NAME)"; 480 | VERSIONING_SYSTEM = "apple-generic"; 481 | }; 482 | name = Release; 483 | }; 484 | /* End XCBuildConfiguration section */ 485 | 486 | /* Begin XCConfigurationList section */ 487 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 488 | isa = XCConfigurationList; 489 | buildConfigurations = ( 490 | 97C147031CF9000F007C117D /* Debug */, 491 | 97C147041CF9000F007C117D /* Release */, 492 | 249021D3217E4FDB00AE95B9 /* Profile */, 493 | ); 494 | defaultConfigurationIsVisible = 0; 495 | defaultConfigurationName = Release; 496 | }; 497 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 498 | isa = XCConfigurationList; 499 | buildConfigurations = ( 500 | 97C147061CF9000F007C117D /* Debug */, 501 | 97C147071CF9000F007C117D /* Release */, 502 | 249021D4217E4FDB00AE95B9 /* Profile */, 503 | ); 504 | defaultConfigurationIsVisible = 0; 505 | defaultConfigurationName = Release; 506 | }; 507 | /* End XCConfigurationList section */ 508 | }; 509 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 510 | } 511 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 56 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 77 | 83 | 84 | 85 | 86 | 88 | 89 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #include "AppDelegate.h" 2 | #include "GeneratedPluginRegistrant.h" 3 | 4 | @implementation AppDelegate 5 | 6 | - (BOOL)application:(UIApplication *)application 7 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 8 | [GeneratedPluginRegistrant registerWithRegistry:self]; 9 | // Override point for customization after application launch. 10 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 11 | } 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakiang/briefing/bbab6e8743c4a749a873fce4b9c46b2b4415a62a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakiang/briefing/bbab6e8743c4a749a873fce4b9c46b2b4415a62a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakiang/briefing/bbab6e8743c4a749a873fce4b9c46b2b4415a62a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakiang/briefing/bbab6e8743c4a749a873fce4b9c46b2b4415a62a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakiang/briefing/bbab6e8743c4a749a873fce4b9c46b2b4415a62a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakiang/briefing/bbab6e8743c4a749a873fce4b9c46b2b4415a62a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakiang/briefing/bbab6e8743c4a749a873fce4b9c46b2b4415a62a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakiang/briefing/bbab6e8743c4a749a873fce4b9c46b2b4415a62a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakiang/briefing/bbab6e8743c4a749a873fce4b9c46b2b4415a62a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakiang/briefing/bbab6e8743c4a749a873fce4b9c46b2b4415a62a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakiang/briefing/bbab6e8743c4a749a873fce4b9c46b2b4415a62a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakiang/briefing/bbab6e8743c4a749a873fce4b9c46b2b4415a62a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakiang/briefing/bbab6e8743c4a749a873fce4b9c46b2b4415a62a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakiang/briefing/bbab6e8743c4a749a873fce4b9c46b2b4415a62a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakiang/briefing/bbab6e8743c4a749a873fce4b9c46b2b4415a62a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakiang/briefing/bbab6e8743c4a749a873fce4b9c46b2b4415a62a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakiang/briefing/bbab6e8743c4a749a873fce4b9c46b2b4415a62a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakiang/briefing/bbab6e8743c4a749a873fce4b9c46b2b4415a62a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | briefing 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /ios/Runner/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char* argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/bloc/bloc_article.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:briefing/bloc/bloc_provider.dart'; 4 | import 'package:briefing/model/article.dart'; 5 | import 'package:briefing/repository/repository.dart'; 6 | import 'package:briefing/util/rate_limiter.dart'; 7 | import 'package:rxdart/rxdart.dart'; 8 | 9 | class ArticleListBloc extends BlocBase { 10 | RateLimiter rateLimit = getRateLimiter; 11 | final menuSubject = BehaviorSubject.seeded(Menu.local); 12 | final _articleListSubject = BehaviorSubject>(); 13 | final _articleCategorySubject = BehaviorSubject.seeded('All'); 14 | List
_articleList =
[]; 15 | 16 | Stream> get articleListObservable => _articleListSubject.stream; 17 | 18 | Stream get categoryObservable => _articleCategorySubject.stream; 19 | 20 | Sink get categorySink => _articleCategorySubject.sink; 21 | 22 | ArticleListBloc() { 23 | _articleListSubject.add(_articleList); 24 | 25 | categoryListener(); 26 | } 27 | 28 | categoryListener() { 29 | categoryObservable.listen((category) async { 30 | print('categoryObservable.listen($category)'); 31 | await _fetchDataAndPushToStream(category: categories[category]); 32 | }); 33 | } 34 | 35 | refresh() async { 36 | print(':::refresh:::'); 37 | var key = await categoryObservable.last; 38 | await rateLimit.reset(key); 39 | await _fetchDataAndPushToStream(); 40 | } 41 | 42 | Future _fetchDataAndPushToStream({category}) async { 43 | await _loadFromDatabase(category: category); 44 | if (_articleList.isEmpty || await shouldFetch(category)) { 45 | await _fetchFromNetwork(category: category); 46 | } 47 | } 48 | 49 | sendToStream(List
articles) { 50 | _articleList.clear(); 51 | _articleList.addAll(articles); 52 | _articleListSubject.sink.add(_articleList); 53 | } 54 | 55 | @override 56 | dispose() { 57 | _articleListSubject.close(); 58 | _articleCategorySubject.close(); 59 | menuSubject.close(); 60 | } 61 | 62 | Future _loadFromDatabase({String category}) async { 63 | try { 64 | var localData = await RepositoryArticle.getAllArticleByCategory(category); 65 | 66 | if (localData.isNotEmpty) { 67 | sendToStream(localData); 68 | } else { 69 | sendErrorMessage('No $category articles'); 70 | } 71 | } catch (e) { 72 | print('=== _loadFromDb $e'); 73 | } 74 | } 75 | 76 | Future _fetchFromNetwork({country = 'us', category}) async { 77 | var articles = 78 | await RepositoryArticle.getArticleListFromNetwork(country, category); 79 | if (articles.isNotEmpty) { 80 | try { 81 | await RepositoryArticle.insertArticleList(articles, category: category); 82 | } catch (error) { 83 | print('DB Error ${error.toString()}'); 84 | } 85 | await _loadFromDatabase(category: category); 86 | } 87 | } 88 | 89 | Future shouldFetch(key) async { 90 | return await rateLimit.shouldFetch(key); 91 | } 92 | 93 | void sendErrorMessage([String message = "Can't connect to the internet!"]) { 94 | print('sendErrorMessage'); 95 | _articleListSubject.addError(BriefingError(message)); 96 | _articleList.clear(); 97 | } 98 | } 99 | 100 | class BriefingError extends Error { 101 | final String message; 102 | 103 | BriefingError(this.message); 104 | 105 | @override 106 | String toString() { 107 | return '$message'; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /lib/bloc/bloc_bookmark_article.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:briefing/bloc/bloc_provider.dart'; 4 | import 'package:briefing/model/article.dart'; 5 | import 'package:briefing/repository/repository.dart'; 6 | import 'package:rxdart/rxdart.dart'; 7 | 8 | class BookmarkArticleListBloc extends BlocBase { 9 | final _articleListSubject = BehaviorSubject>(); 10 | List
_articleList =
[]; 11 | 12 | Stream> get articleListObservable => _articleListSubject.stream; 13 | 14 | BookmarkArticleListBloc() { 15 | _articleListSubject.add(_articleList); 16 | 17 | _initList(); 18 | } 19 | 20 | _initList() async { 21 | print('menu == Menu.favorites'); 22 | await _loadBookmarkedArticlesFromDatabase(); 23 | } 24 | 25 | sendToStream(List
articles) { 26 | _articleList.clear(); 27 | _articleList.addAll(articles); 28 | _articleListSubject.sink.add(_articleList); 29 | } 30 | 31 | @override 32 | dispose() { 33 | _articleListSubject.close(); 34 | } 35 | 36 | Future _loadBookmarkedArticlesFromDatabase() async { 37 | try { 38 | var localData = await RepositoryArticle.getBookmarkedArticles(); 39 | if (localData.isNotEmpty) { 40 | sendToStream(localData); 41 | } else { 42 | sendErrorMessage('No bookmarked articles'); 43 | } 44 | } catch (e) { 45 | print(e); 46 | } 47 | } 48 | 49 | void sendErrorMessage([String message = "Can't connect to the internet!"]) { 50 | print('sendErrorMessage'); 51 | _articleListSubject.addError(BriefingError(message)); 52 | _articleList.clear(); 53 | } 54 | } 55 | 56 | class BriefingError extends Error { 57 | final String message; 58 | 59 | BriefingError(this.message); 60 | 61 | @override 62 | String toString() { 63 | return '$message'; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/bloc/bloc_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | //to enforce the implementation of the dispose() method inside the bloc 4 | abstract class BlocBase { 5 | void dispose(); 6 | } 7 | 8 | class BlocProvider extends StatefulWidget { 9 | final Widget child; 10 | final T bloc; 11 | 12 | const BlocProvider({Key key, @required this.bloc, @required this.child}) 13 | : super(key: key); 14 | 15 | @override 16 | _BlocProviderState createState() => _BlocProviderState(); 17 | 18 | static T of(BuildContext context) { 19 | final type = _typeOf<_BlocProviderInherited>(); 20 | _BlocProviderInherited provider = 21 | context.ancestorInheritedElementForWidgetOfExactType(type)?.widget; 22 | return provider?.bloc; 23 | } 24 | } 25 | 26 | class _BlocProviderState extends State> { 27 | @override 28 | void dispose() { 29 | widget.bloc.dispose(); 30 | super.dispose(); 31 | } 32 | 33 | @override 34 | Widget build(BuildContext context) { 35 | return _BlocProviderInherited( 36 | bloc: widget.bloc, 37 | child: widget.child, 38 | ); 39 | } 40 | } 41 | 42 | class _BlocProviderInherited extends InheritedWidget { 43 | final T bloc; 44 | 45 | _BlocProviderInherited({Key key, @required Widget child, @required this.bloc}) 46 | : super(key: key, child: child); 47 | 48 | @override 49 | bool updateShouldNotify(_BlocProviderInherited oldWidget) { 50 | return oldWidget.bloc != bloc; 51 | } 52 | } 53 | 54 | Type _typeOf() => T; 55 | -------------------------------------------------------------------------------- /lib/bookmarked_article_list.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:briefing/bloc/bloc_article.dart'; 4 | import 'package:briefing/bloc/bloc_bookmark_article.dart'; 5 | import 'package:briefing/briefing_card.dart'; 6 | import 'package:briefing/model/article.dart'; 7 | import 'package:flutter/material.dart'; 8 | 9 | class BookmarkArticleList extends StatefulWidget { 10 | const BookmarkArticleList({Key key}) : super(key: key); 11 | 12 | @override 13 | _BookmarkArticleListState createState() => _BookmarkArticleListState(); 14 | } 15 | 16 | class _BookmarkArticleListState extends State { 17 | final GlobalKey _refreshIndicatorKey = 18 | new GlobalKey(); 19 | BookmarkArticleListBloc _bloc; 20 | 21 | @override 22 | void initState() { 23 | super.initState(); 24 | _bloc = BookmarkArticleListBloc(); 25 | WidgetsBinding.instance 26 | .addPostFrameCallback((_) => _refreshIndicatorKey.currentState.show()); 27 | } 28 | 29 | Future _onRefresh() async { 30 | await Future.delayed(Duration(seconds: 1)); 31 | } 32 | 33 | @override 34 | void dispose() { 35 | _bloc.dispose(); 36 | super.dispose(); 37 | } 38 | 39 | @override 40 | Widget build(BuildContext context) { 41 | return SliverList( 42 | delegate: SliverChildListDelegate([ 43 | StreamBuilder>( 44 | stream: _bloc.articleListObservable, 45 | initialData: List(), 46 | builder: (context, snapshot) { 47 | return RefreshIndicator( 48 | key: _refreshIndicatorKey, 49 | displacement: 5.0, 50 | backgroundColor: Colors.white, 51 | onRefresh: _onRefresh, 52 | child: snapshot.hasData && snapshot.data.length > 0 53 | ? ListView.separated( 54 | padding: EdgeInsets.symmetric( 55 | horizontal: 12.0, vertical: 18.0), 56 | physics: ScrollPhysics(), 57 | shrinkWrap: true, 58 | itemCount: snapshot.data.length, 59 | separatorBuilder: (BuildContext context, int index) { 60 | return Divider(); 61 | }, 62 | itemBuilder: (BuildContext context, int index) { 63 | return BriefingCard( 64 | article: snapshot.data.elementAt(index)); 65 | }) 66 | : snapshot.hasError 67 | ? Center( 68 | child: ErrorWidget(message: ['${snapshot.error}'])) 69 | : Center( 70 | child: Container( 71 | margin: EdgeInsets.all(16.0), 72 | width: 30, 73 | height: 30, 74 | child: CircularProgressIndicator()), 75 | ), 76 | ); 77 | }), 78 | ]), 79 | ); 80 | } 81 | } 82 | 83 | class ErrorWidget extends StatelessWidget { 84 | final List message; 85 | 86 | const ErrorWidget({Key key, this.message}) : super(key: key); 87 | 88 | @override 89 | Widget build(BuildContext context) { 90 | return Container( 91 | margin: EdgeInsets.only(top: 92.0), 92 | child: Column( 93 | mainAxisAlignment: MainAxisAlignment.center, 94 | children: [ 95 | Icon(Icons.cloud_off, size: 55.0), 96 | Text('Woops...', 97 | style: Theme.of(context).textTheme.subhead, 98 | textAlign: TextAlign.center), 99 | Text(message.join('\n'), 100 | textAlign: TextAlign.center, 101 | style: Theme.of(context) 102 | .textTheme 103 | .subhead 104 | .copyWith(fontWeight: FontWeight.w600)), 105 | ], 106 | ), 107 | ); 108 | } 109 | } 110 | 111 | class LoadingWidget extends StatelessWidget { 112 | final Stream _isLoading; 113 | 114 | const LoadingWidget(this._isLoading); 115 | 116 | @override 117 | Widget build(BuildContext context) { 118 | return StreamBuilder( 119 | stream: _isLoading, 120 | initialData: true, 121 | builder: (context, snapshot) { 122 | debugPrint("_bloc.isLoading: ${snapshot.data}"); 123 | return snapshot.data 124 | ? Center( 125 | child: Container( 126 | margin: EdgeInsets.only(top: 92.0), 127 | width: 30, 128 | height: 30, 129 | child: CircularProgressIndicator( 130 | backgroundColor: Colors.white, 131 | valueColor: AlwaysStoppedAnimation(Colors.blue), 132 | ), 133 | ), 134 | ) 135 | : Container(); 136 | }, 137 | ); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /lib/briefing_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:briefing/model/article.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:briefing/widget/article_bottom_section.dart'; 4 | import 'package:briefing/widget/article_title_section.dart'; 5 | import 'package:flutter_custom_tabs/flutter_custom_tabs.dart'; 6 | 7 | class BriefingCard extends StatefulWidget { 8 | final Article article; 9 | 10 | const BriefingCard({Key key, this.article}) : super(key: key); 11 | 12 | @override 13 | BriefingCardState createState() { 14 | return BriefingCardState(); 15 | } 16 | } 17 | 18 | class BriefingCardState extends State { 19 | @override 20 | Widget build(BuildContext context) { 21 | return Container( 22 | margin: EdgeInsets.only(bottom: 4.0), 23 | padding: EdgeInsets.fromLTRB(4.0, 0.0, 4.0, 4.0), 24 | child: Column( 25 | children: [ 26 | InkWell( 27 | borderRadius: BorderRadius.circular(20.0), 28 | child: ArticleTitleSection(article: widget.article), 29 | onTap: () { 30 | _launchURL(context, widget.article.url); 31 | }, 32 | ), 33 | ArticleBottomSection(article: widget.article), 34 | ], 35 | ), 36 | ); 37 | } 38 | 39 | void _launchURL(BuildContext context, String link) async { 40 | try { 41 | await launch( 42 | link, 43 | option: new CustomTabsOption( 44 | toolbarColor: Theme.of(context).primaryColor, 45 | enableDefaultShare: true, 46 | enableUrlBarHiding: true, 47 | showPageTitle: true, 48 | animation: CustomTabsAnimation.slideIn(), 49 | extraCustomTabs: [ 50 | // ref. https://play.google.com/store/apps/details?id=org.mozilla.firefox 51 | 'org.mozilla.firefox', 52 | ], 53 | ), 54 | ); 55 | } catch (e) { 56 | // An exception is thrown if browser app is not installed on Android device. 57 | debugPrint(e.toString()); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/briefing_sliver_list.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:briefing/bloc/bloc_article.dart'; 4 | import 'package:briefing/briefing_card.dart'; 5 | import 'package:briefing/model/article.dart'; 6 | import 'package:flutter/material.dart'; 7 | 8 | class BriefingSliverList extends StatefulWidget { 9 | final Menu menu; 10 | 11 | const BriefingSliverList({Key key, this.menu}) : super(key: key); 12 | 13 | @override 14 | _BriefingSliverListState createState() => _BriefingSliverListState(); 15 | } 16 | 17 | class _BriefingSliverListState extends State { 18 | final GlobalKey _refreshIndicatorKey = 19 | new GlobalKey(); 20 | ArticleListBloc _bloc; 21 | 22 | @override 23 | void initState() { 24 | super.initState(); 25 | _bloc = ArticleListBloc(); 26 | WidgetsBinding.instance 27 | .addPostFrameCallback((_) => _refreshIndicatorKey.currentState.show()); 28 | } 29 | 30 | Future _onRefresh() async { 31 | // await _bloc.refresh() 32 | await Future.delayed(Duration(seconds: 3)); 33 | } 34 | 35 | @override 36 | void dispose() { 37 | _bloc.dispose(); 38 | super.dispose(); 39 | } 40 | 41 | @override 42 | Widget build(BuildContext context) { 43 | _bloc.menuSubject.sink.add(widget.menu); 44 | if (widget.menu != null && widget.menu == Menu.local) { 45 | _bloc.categorySink.add('local'); 46 | } else { 47 | _bloc.categorySink.add('All'); 48 | } 49 | 50 | return SliverList( 51 | delegate: SliverChildListDelegate([ 52 | if (widget.menu == Menu.headlines) 53 | StreamBuilder( 54 | stream: _bloc.categoryObservable, 55 | builder: (context, snapshot) { 56 | return Card( 57 | margin: EdgeInsets.zero, 58 | shape: RoundedRectangleBorder( 59 | borderRadius: BorderRadius.circular(0.0)), 60 | elevation: 1.0, 61 | child: Container( 62 | margin: EdgeInsets.symmetric(vertical: 12.0), 63 | height: 30.0, 64 | width: MediaQuery.of(context).size.width, 65 | child: ListView( 66 | physics: ScrollPhysics(), 67 | shrinkWrap: true, 68 | scrollDirection: Axis.horizontal, 69 | children: categories.keys 70 | .where((category) => category != 'local') 71 | .map( 72 | (category) => Padding( 73 | padding: 74 | const EdgeInsets.symmetric(horizontal: 4.0), 75 | child: ChoiceChip( 76 | selectedColor: Theme.of(context).accentColor, 77 | label: Text(category), 78 | selected: snapshot.data == category, 79 | onSelected: (val) { 80 | _refreshIndicatorKey.currentState.show(); 81 | _bloc.categorySink.add(category); 82 | }), 83 | ), 84 | ) 85 | .toList(), 86 | ), 87 | ), 88 | ); 89 | }), 90 | StreamBuilder>( 91 | stream: _bloc.articleListObservable, 92 | initialData: List(), 93 | builder: (context, snapshot) { 94 | debugPrint("!!!snapshot state: ${snapshot.connectionState}!!!"); 95 | return RefreshIndicator( 96 | key: _refreshIndicatorKey, 97 | displacement: 5.0, 98 | backgroundColor: Colors.white, 99 | onRefresh: _onRefresh, 100 | child: snapshot.hasData && snapshot.data.length > 0 101 | ? ListView.separated( 102 | padding: EdgeInsets.symmetric( 103 | horizontal: 12.0, vertical: 18.0), 104 | physics: ScrollPhysics(), 105 | shrinkWrap: true, 106 | itemCount: snapshot.data.length, 107 | separatorBuilder: (BuildContext context, int index) { 108 | return Divider(); 109 | }, 110 | itemBuilder: (BuildContext context, int index) { 111 | return BriefingCard( 112 | article: snapshot.data.elementAt(index)); 113 | }, 114 | ) 115 | : snapshot.hasError 116 | ? Center( 117 | child: GestureDetector( 118 | onTap: _onRefresh, 119 | child: 120 | ErrorWidget(message: ['${snapshot.error}']), 121 | ), 122 | ) 123 | : Center( 124 | child: Container( 125 | margin: EdgeInsets.all(16.0), 126 | width: 30, 127 | height: 30, 128 | child: CircularProgressIndicator(), 129 | ), 130 | ), 131 | ); 132 | }), 133 | ]), 134 | ); 135 | } 136 | } 137 | 138 | class ErrorWidget extends StatelessWidget { 139 | final List message; 140 | 141 | const ErrorWidget({Key key, this.message}) : super(key: key); 142 | 143 | @override 144 | Widget build(BuildContext context) { 145 | return Container( 146 | margin: EdgeInsets.only(top: 92.0), 147 | child: Column( 148 | mainAxisAlignment: MainAxisAlignment.center, 149 | children: [ 150 | Icon(Icons.cloud_off, size: 55.0), 151 | Text('Woops...', 152 | style: Theme.of(context).textTheme.subhead, 153 | textAlign: TextAlign.center), 154 | Text( 155 | message.join('\n'), 156 | textAlign: TextAlign.center, 157 | style: Theme.of(context) 158 | .textTheme 159 | .subhead 160 | .copyWith(fontWeight: FontWeight.w600), 161 | ), 162 | ], 163 | ), 164 | ); 165 | } 166 | } 167 | 168 | class LoadingWidget extends StatelessWidget { 169 | final Stream _isLoading; 170 | 171 | const LoadingWidget(this._isLoading); 172 | 173 | @override 174 | Widget build(BuildContext context) { 175 | return StreamBuilder( 176 | stream: _isLoading, 177 | initialData: true, 178 | builder: (context, snapshot) { 179 | debugPrint("_bloc.isLoading: ${snapshot.data}"); 180 | return snapshot.data 181 | ? Center( 182 | child: Container( 183 | margin: EdgeInsets.only(top: 92.0), 184 | width: 30, 185 | height: 30, 186 | child: CircularProgressIndicator( 187 | backgroundColor: Colors.white, 188 | valueColor: AlwaysStoppedAnimation(Colors.blue), 189 | ), 190 | ), 191 | ) 192 | : Container(); 193 | }, 194 | ); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:briefing/bookmarked_article_list.dart'; 2 | import 'package:briefing/briefing_sliver_list.dart'; 3 | import 'package:briefing/model/article.dart'; 4 | import 'package:briefing/theme/theme.dart'; 5 | import 'package:briefing/widget/main_sliverappbar.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter/services.dart'; 8 | 9 | void main() { 10 | runApp(MyApp()); 11 | } 12 | 13 | class MyApp extends StatelessWidget { 14 | @override 15 | Widget build(BuildContext context) { 16 | return MaterialApp( 17 | debugShowCheckedModeBanner: false, 18 | title: 'Briefing', 19 | theme: buildAppTheme(), 20 | home: MyHomePage(title: 'Briefing'), 21 | ); 22 | } 23 | } 24 | 25 | class MyHomePage extends StatefulWidget { 26 | MyHomePage({Key key, this.title}) : super(key: key); 27 | final String title; 28 | 29 | @override 30 | _MyHomePageState createState() => _MyHomePageState(); 31 | } 32 | 33 | class _MyHomePageState extends State { 34 | int _selectedIndex = 0; 35 | final menus = [Menu.local, Menu.headlines, Menu.favorites, Menu.agencies]; 36 | 37 | @override 38 | void initState() { 39 | super.initState(); 40 | } 41 | 42 | @override 43 | void dispose() { 44 | super.dispose(); 45 | } 46 | 47 | final GlobalKey _scaffoldKey = GlobalKey(); 48 | 49 | @override 50 | Widget build(BuildContext context) { 51 | Widget getScreen() { 52 | if (menus[_selectedIndex] == Menu.favorites) { 53 | return BookmarkArticleList(); 54 | } 55 | if (menus[_selectedIndex] == Menu.local || 56 | menus[_selectedIndex] == Menu.headlines) { 57 | return BriefingSliverList(menu: menus[_selectedIndex]); 58 | } 59 | return SliverList( 60 | delegate: SliverChildListDelegate([ 61 | Padding( 62 | padding: const EdgeInsets.symmetric(vertical: 64.0), 63 | child: Center( 64 | child: Text('Agencies(sources) comming soon...', 65 | style: TextStyle(fontSize: 22)), 66 | ), 67 | ) 68 | ])); 69 | } 70 | 71 | return AnnotatedRegion( 72 | value: SystemUiOverlayStyle( 73 | statusBarIconBrightness: Brightness.dark, 74 | statusBarBrightness: Brightness.light, 75 | statusBarColor: Theme.of(context).primaryColor, 76 | systemNavigationBarColor: Colors.white, 77 | systemNavigationBarIconBrightness: Brightness.dark, 78 | ), 79 | child: SafeArea( 80 | child: Scaffold( 81 | key: _scaffoldKey, 82 | body: CustomScrollView( 83 | slivers: [ 84 | MainSliverAppBar(title: 'Briefing'), 85 | getScreen(), 86 | ], 87 | ), 88 | bottomNavigationBar: BottomAppBar( 89 | color: Theme.of(context).primaryColor, 90 | child: Container( 91 | decoration: BoxDecoration(boxShadow: [ 92 | BoxShadow( 93 | color: Colors.cyan[100], 94 | offset: Offset(-2.0, 2.0), 95 | blurRadius: 2.0, 96 | spreadRadius: 2.0) 97 | ]), 98 | height: 72.0, 99 | child: BottomNavigationBar( 100 | selectedItemColor: Theme.of(context).accentColor, 101 | currentIndex: _selectedIndex, 102 | onTap: (val) => _onItemTapped(val), 103 | type: BottomNavigationBarType.fixed, 104 | backgroundColor: Theme.of(context).primaryColor, 105 | selectedFontSize: 18.0, 106 | unselectedFontSize: 17.0, 107 | items: [ 108 | BottomNavigationBarItem( 109 | icon: Icon(Icons.local_library), title: Text('Local')), 110 | BottomNavigationBarItem( 111 | icon: Icon(Icons.language), title: Text('Headlines')), 112 | BottomNavigationBarItem( 113 | icon: Icon(Icons.bookmark_border), 114 | title: Text('Favorites')), 115 | BottomNavigationBarItem( 116 | icon: Icon(Icons.filter_none), title: Text('Agencies')) 117 | ], 118 | elevation: 5.0, 119 | ), 120 | ), 121 | ), 122 | ), 123 | ), 124 | ); 125 | } 126 | 127 | void _onItemTapped(int index) { 128 | setState(() { 129 | _selectedIndex = index; 130 | }); 131 | // Navigator.pop(context); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /lib/model/article.dart: -------------------------------------------------------------------------------- 1 | import 'package:intl/intl.dart'; 2 | 3 | class Article { 4 | num id; 5 | final String title; 6 | final String description; 7 | final String url; 8 | final String publishedAt; 9 | final String author; 10 | final String source; 11 | final String content; 12 | final String imageUrl; 13 | final String category; 14 | bool bookmarked = false; 15 | 16 | Article( 17 | {this.id, 18 | this.title, 19 | this.description, 20 | this.url, 21 | this.publishedAt, 22 | this.author, 23 | this.source, 24 | this.content, 25 | this.imageUrl, 26 | this.category, 27 | this.bookmarked}); 28 | 29 | factory Article.fromMap(Map data, {network = false}) { 30 | return Article( 31 | id: data['id'], 32 | title: data['title'], 33 | description: data['description'], 34 | url: data['url'], 35 | publishedAt: data['publishedAt'], 36 | author: data['author'], 37 | source: network ? data['source']['name'] : data['source'], 38 | content: data['content'], 39 | imageUrl: data['urlToImage'], 40 | category: data['category'] ?? '', 41 | bookmarked: (data['bookmarked'] ?? false) == 1, 42 | ); 43 | } 44 | 45 | Map toMap({category}) { 46 | return { 47 | 'title': title, 48 | 'description': description, 49 | 'url': url, 50 | 'publishedAt': publishedAt, 51 | 'author': author, 52 | 'source': source, 53 | 'content': content, 54 | 'urlToImage': imageUrl, 55 | 'category': category ?? this.category, 56 | 'bookmarked': bookmarked, 57 | }; 58 | } 59 | 60 | String get timeAgo { 61 | var formatter = DateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); 62 | // var formatter = DateFormat("EEE, d MMM yyyy HH:mm:ss zzz"); 63 | 64 | DateTime parsedDate; 65 | 66 | try { 67 | parsedDate = formatter.parse(publishedAt); 68 | } catch (error) { 69 | try { 70 | parsedDate = 71 | DateFormat("EEE, d MMM yyyy HH:mm:ss zzz").parse(publishedAt); 72 | } catch (error) { 73 | print('${error.toString()}'); 74 | } 75 | } 76 | if (parsedDate != null) { 77 | Duration duration = DateTime.now().difference(parsedDate); 78 | 79 | if (duration.inDays > 7 || duration.isNegative) { 80 | return DateFormat.MMMMd().format(parsedDate); 81 | } else if (duration.inDays >= 1 && duration.inDays <= 7) { 82 | return duration.inDays == 1 83 | ? "1 day ago" 84 | : "${duration.inDays} days ago"; 85 | } else if (duration.inHours >= 1) { 86 | return duration.inHours == 1 87 | ? "1 hour ago" 88 | : "${duration.inHours} hours ago"; 89 | } else { 90 | return duration.inMinutes == 1 91 | ? "1 minute ago" 92 | : "${duration.inMinutes} minutes ago"; 93 | } 94 | } else { 95 | return publishedAt; 96 | } 97 | } 98 | 99 | bool isNew() { 100 | var formatter = DateFormat("EEE, d MMM yyyy HH:mm:ss zzz"); 101 | 102 | DateTime parsedDate = formatter.parse(publishedAt); 103 | Duration duration = DateTime.now().difference(parsedDate); 104 | if (duration.inHours < 24) { 105 | return true; 106 | } 107 | return false; 108 | } 109 | 110 | bool get isValid => title != null && title.length > 3 && url != null; 111 | } 112 | 113 | const categories = { 114 | 'All': '', 115 | 'Technology': 'technology', 116 | 'Business': 'business', 117 | 'Entertainment': 'entertainment', 118 | 'Health': 'health', 119 | 'Science': 'science', 120 | 'Sports': 'sports', 121 | 'General': 'general', 122 | 'local': 'local' 123 | }; 124 | 125 | enum Menu { local, headlines, favorites, agencies } 126 | -------------------------------------------------------------------------------- /lib/repository/repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:briefing/model/article.dart'; 2 | import 'package:briefing/service/api_service.dart'; 3 | import 'package:briefing/service/database.dart'; 4 | 5 | class RepositoryCommon { 6 | static close() { 7 | DatabaseService.db.close(); 8 | } 9 | 10 | static Future getValue(String id) async { 11 | return await DatabaseService.db.getValue(id); 12 | } 13 | 14 | static Future insertMetadata(String id) async { 15 | return await DatabaseService.db.insertMetadata(id); 16 | } 17 | 18 | static Future deleteMetadata(String id) async { 19 | return await DatabaseService.db.deleteMetadata(id); 20 | } 21 | } 22 | 23 | class RepositoryArticle { 24 | //DatabaseService 25 | static Future insertArticle(Article article) async { 26 | return await DatabaseService.db.insertArticle(article); 27 | } 28 | 29 | static Future insertArticleList(List
articles, 30 | {category}) async { 31 | return await DatabaseService.db 32 | .insertArticleList(articles, category: category); 33 | } 34 | 35 | static Future> getBookmarkedArticles() async { 36 | return await DatabaseService.db.getBookmarkedArticles(); 37 | } 38 | 39 | static Future> _getArticleFromDatabase() async { 40 | return await DatabaseService.db.getAllArticle(); 41 | } 42 | 43 | static Future> getAllArticleByCategory(String category) async { 44 | if (category != null && category.isNotEmpty) { 45 | return await DatabaseService.db.getAllArticleByCategory(category); 46 | } 47 | return await _getArticleFromDatabase(); 48 | } 49 | 50 | //ApiService 51 | static Future> getArticleListFromNetwork( 52 | String country, String category) async { 53 | if (category != null && category == 'local') { 54 | return getLocalNewsFromNetwork(); 55 | } 56 | return ApiService.getArticlesFromNetwork(country, category); 57 | } 58 | 59 | static Future> getLocalNewsFromNetwork() async { 60 | return ApiService.getLocalNewsFromNetwork(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/service/api_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:http/http.dart' as http; 5 | import 'package:xml/xml.dart' as xml; 6 | import 'package:briefing/model/article.dart'; 7 | import 'package:xml/xml.dart'; 8 | 9 | const base_url = 'https://newsapi.org/v2'; 10 | const api_key = '11cd66d3a6994c108e7fb7d92cee5e12'; 11 | const local_news_url = 'https://news.google.com/rss'; 12 | 13 | String getUrl(String country, String category) { 14 | var url = '$base_url/top-headlines?page=1'; 15 | if (country != null && country.isNotEmpty) { 16 | url += '&country=$country'; 17 | } 18 | if (category != null && category.isNotEmpty) { 19 | url += '&category=$category'; 20 | } 21 | return url += '&apiKey=$api_key'; 22 | } 23 | 24 | class ApiService { 25 | static Future> getArticlesFromNetwork(country, category) async { 26 | var articles = []; 27 | try { 28 | final response = await http.get(getUrl(country, category)); 29 | if (response.statusCode == 200) { 30 | articles = await compute(parseArticles, response.body); 31 | } 32 | } catch (e) { 33 | print('=== API::getArticlesFromNetwork Error ${e.toString()}'); 34 | } 35 | return articles; 36 | } 37 | 38 | static Future> getLocalNewsFromNetwork() async { 39 | var articles = []; 40 | try { 41 | final response = await http.get(local_news_url); 42 | if (response.statusCode == 200) { 43 | articles = await compute(parseArticlesXml, response.body); 44 | } 45 | } catch (error) { 46 | print('=== API::LocalNewsFromNetwork::Error ${error.toString()}'); 47 | } 48 | return articles; 49 | } 50 | } 51 | 52 | List
parseArticles(String responseBody) { 53 | var articles = []; 54 | final parsed = json.decode(responseBody); 55 | if (parsed['totalResults'] > 0) { 56 | articles = List
.from(parsed['articles'] 57 | .map((article) => Article.fromMap(article, network: true))); 58 | } 59 | return articles; 60 | } 61 | 62 | List
parseArticlesXml(String responseBody) { 63 | var document = xml.parse(responseBody); 64 | 65 | var channelElement = document.findAllElements("channel")?.first; 66 | var source = findElementOrNull(channelElement, 'title')?.text; 67 | 68 | return channelElement.findAllElements('item').map((element) { 69 | var title = findElementOrNull(element, 'title')?.text; 70 | var description = findElementOrNull(element, "description")?.text; 71 | var source2 = element.findElements("source").first.getAttribute('url'); 72 | var link = findElementOrNull(element, "link")?.text; 73 | // var category = 74 | // element.findElements("category").first.getAttribute('domain'); 75 | var pubDate = findElementOrNull(element, "pubDate")?.text; 76 | var author = findElementOrNull(element, "author")?.text; 77 | var image = 78 | findElementOrNull(element, "enclosure")?.getAttribute("url") ?? null; 79 | 80 | return Article( 81 | title: title, 82 | category: 'local', 83 | author: author, 84 | content: description, 85 | imageUrl: image, 86 | publishedAt: pubDate, 87 | url: link, 88 | source: source2?.replaceAll('https://www.', '') ?? '', 89 | description: description); 90 | }).toList(); 91 | } 92 | 93 | XmlElement findElementOrNull(XmlElement element, String name) { 94 | try { 95 | return element.findAllElements(name).first; 96 | } on StateError { 97 | return null; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /lib/service/database.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:briefing/model/article.dart'; 5 | import 'package:flutter/foundation.dart'; 6 | import 'package:path/path.dart'; 7 | import 'package:path_provider/path_provider.dart'; 8 | import 'package:sqflite/sqflite.dart'; 9 | 10 | class DatabaseService { 11 | DatabaseService._(); 12 | 13 | static final DatabaseService db = DatabaseService._(); 14 | 15 | Database _database; 16 | 17 | init() async { 18 | await database; 19 | } 20 | 21 | Future get database async { 22 | if (_database != null) return _database; 23 | _database = await initDB(); 24 | return _database; 25 | } 26 | 27 | Future initDB() async { 28 | print('DBProvider.initDB() start'); 29 | Directory documentsDirectory = await getApplicationDocumentsDirectory(); 30 | String path = join(documentsDirectory.path, "briefing.db"); 31 | var db = await openDatabase(path, version: 1, onCreate: populateDb); 32 | // print('DBProvider.initDB() end'); 33 | return db; 34 | } 35 | 36 | populateDb(Database db, int version) async { 37 | await Future(() async { 38 | // print(' DBProvider.await populateDb start'); 39 | 40 | var articleTable = "CREATE TABLE articles" 41 | "(id INTEGER PRIMARY KEY autoincrement, " 42 | "title TEXT," 43 | "description TEXT," 44 | "url TEXT," 45 | "publishedAt TEXT," 46 | "author TEXT," 47 | "source TEXT," 48 | "content TEXT," 49 | "urlToImage TEXT," 50 | "category TEXT," 51 | "bookmarked BIT," 52 | "UNIQUE(url));"; 53 | 54 | var metadata = "CREATE TABLE metadata" 55 | "(id TEXT PRIMARY KEY," 56 | "value INTEGER);"; 57 | 58 | await db.transaction((txn) async { 59 | await txn.execute('$articleTable'); 60 | await txn.execute('$metadata'); 61 | 62 | // print('DBProvider.await populateDb end (txn)'); 63 | }); 64 | }); 65 | } 66 | 67 | Future insertArticle(Article article) async { 68 | final db = await database; 69 | var res; 70 | res = await db.insert('articles', article.toMap(), 71 | conflictAlgorithm: ConflictAlgorithm.replace); 72 | 73 | return res; 74 | } 75 | 76 | Future> insertArticleList(List
articles, 77 | {category}) async { 78 | final db = await database; 79 | var res; 80 | 81 | await db.transaction((txn) async { 82 | Batch batch = txn.batch(); 83 | articles.forEach((article) async { 84 | batch.insert( 85 | 'articles', 86 | article.toMap(category: category), 87 | conflictAlgorithm: ConflictAlgorithm.replace, 88 | ); 89 | }); 90 | res = await batch.commit(continueOnError: true); 91 | }); 92 | print('article $res inserted'); 93 | return res; 94 | } 95 | 96 | Future> getAllArticle() async { 97 | final db = await database; 98 | List res = await db.query("articles", 99 | where: "category != 'local'", orderBy: "publishedAt DESC"); 100 | return res.isNotEmpty ? await compute(prepareArticles, res) : []; 101 | } 102 | 103 | Future> getAllArticleByCategory(category) async { 104 | final db = await database; 105 | List res = await db.query("articles", 106 | where: "category = ?", 107 | orderBy: "publishedAt DESC", 108 | whereArgs: [category]); 109 | return res.isNotEmpty ? await compute(prepareArticles, res) : []; 110 | } 111 | 112 | Future> getBookmarkedArticles() async { 113 | final db = await database; 114 | List res = await db.query("articles", 115 | where: "bookmarked = ?", orderBy: "publishedAt DESC", whereArgs: [1]); 116 | print('@@@@@ getBookmarkedArticles count ${res.length}'); 117 | return res.isNotEmpty ? await compute(prepareArticles, res) : []; 118 | } 119 | 120 | Future
getArticle(int id) async { 121 | final db = await database; 122 | var res = await db.query("articles", where: "id = ?", whereArgs: [id]); 123 | return res.isNotEmpty ? Article.fromMap(res.first) : null; 124 | } 125 | 126 | Future updateArticle(Article article) async { 127 | final db = await database; 128 | var res = await db.update("articles", article.toMap(), 129 | where: "id = ?", whereArgs: [article.id]); 130 | return res; 131 | } 132 | 133 | Future deleteArticle(int id) async { 134 | final db = await database; 135 | return db.delete("articles", where: "id = ?", whereArgs: [id]); 136 | } 137 | 138 | Future deleteAllArticle() async { 139 | final db = await database; 140 | return db.rawDelete("DELETE FROM articles"); 141 | } 142 | 143 | Future close() async => db.close(); 144 | 145 | Future insertMetadata(String id) async { 146 | final db = await database; 147 | return await db.insert( 148 | 'metadata', {"id": id, "value": DateTime.now().millisecondsSinceEpoch}, 149 | conflictAlgorithm: ConflictAlgorithm.replace); 150 | } 151 | 152 | Future deleteMetadata(String id) async { 153 | final db = await database; 154 | return await db.delete('metadata', where: "id = ?", whereArgs: [id]); 155 | } 156 | 157 | Future getValue(String id) async { 158 | final db = await database; 159 | var res = 160 | await db.query("metadata", where: "id = ?", whereArgs: [id], limit: 1); 161 | return res.isNotEmpty ? res.single['value'] : 0; 162 | } 163 | } 164 | 165 | List
prepareArticles(List list) { 166 | return list.map((a) => Article.fromMap(a)).toList(); 167 | } 168 | -------------------------------------------------------------------------------- /lib/theme/colors.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | const themeAppWhite100 = const Color(0xFFFFFFFF); 4 | const themeAppWhite300 = const Color(0xFFFBB8AC); 5 | const themeAppWhite400 = const Color(0xFFEAA4A4); 6 | 7 | const themeAppGrey500 = Colors.grey; 8 | const themeAppGrey600 = const Color(0xFF757575); 9 | const themeAppGrey700 = const Color(0xFF616161); 10 | const themeAppGrey800 = const Color(0xFF424242); 11 | 12 | const themeAppBlue500 = Color(0xFF1E88E5); 13 | 14 | const themeAppErrorRed = const Color(0xFFC5032B); 15 | 16 | const themeAppAccentColor = Colors.cyan; 17 | 18 | const themeAppSurfaceWhite = const Color(0xFFFFFBFA); 19 | const themeAppBackgroundWhite = Colors.white; 20 | -------------------------------------------------------------------------------- /lib/theme/theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:briefing/theme/colors.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | ThemeData buildAppTheme() { 5 | final ThemeData base = ThemeData.light(); 6 | return base.copyWith( 7 | accentColor: themeAppBlue500, 8 | primaryColor: themeAppWhite100, 9 | primaryColorBrightness: Brightness.light, 10 | bottomAppBarColor: themeAppWhite100, 11 | buttonTheme: base.buttonTheme.copyWith( 12 | buttonColor: themeAppWhite100, textTheme: ButtonTextTheme.normal), 13 | scaffoldBackgroundColor: themeAppBackgroundWhite, 14 | cardColor: themeAppBackgroundWhite, 15 | textSelectionColor: themeAppGrey600, 16 | errorColor: themeAppErrorRed, 17 | canvasColor: Colors.transparent, 18 | textTheme: _buildAppTextTheme(base.textTheme), 19 | primaryTextTheme: _buildAppTextTheme(base.primaryTextTheme), 20 | accentTextTheme: _buildAppTextTheme(base.accentTextTheme), 21 | accentIconTheme: base.accentIconTheme.copyWith(color: themeAppBlue500), 22 | primaryIconTheme: base.iconTheme.copyWith(color: themeAppGrey600), 23 | inputDecorationTheme: InputDecorationTheme(border: OutlineInputBorder()), 24 | iconTheme: base.iconTheme.copyWith(color: themeAppGrey700), 25 | chipTheme: base.chipTheme.copyWith( 26 | secondaryLabelStyle: base.chipTheme.secondaryLabelStyle 27 | .copyWith(color: Colors.white, fontWeight: FontWeight.w500)), 28 | ); 29 | } 30 | 31 | TextTheme _buildAppTextTheme(TextTheme base) { 32 | return base 33 | .copyWith( 34 | title: base.title.copyWith( 35 | fontFamily: 'Libre_Franklin', 36 | fontWeight: FontWeight.bold, 37 | fontSize: 24.0), 38 | subhead: base.subhead.copyWith( 39 | fontFamily: 'Crimson_Text', 40 | fontSize: 18.0, 41 | ), 42 | caption: base.caption.copyWith(fontFamily: 'Libre_Franklin'), 43 | button: base.button.copyWith(fontFamily: 'Libre_Franklin'), 44 | subtitle: base.subtitle.copyWith( 45 | fontFamily: 'Libre_Franklin', 46 | fontSize: 18.0, 47 | )) 48 | .apply( 49 | fontFamily: 'Crimson_Text', 50 | displayColor: themeAppGrey800, 51 | bodyColor: themeAppGrey800); 52 | } 53 | -------------------------------------------------------------------------------- /lib/util/rate_limiter.dart: -------------------------------------------------------------------------------- 1 | ///Utility class that decides whether we should fetch some data or not. 2 | import 'package:briefing/repository/repository.dart'; 3 | 4 | class RateLimiter { 5 | Map timestamps = new Map(); 6 | 7 | final int _timeout; 8 | 9 | ///[_timeout] : rate at which we should fetch data in minutes 10 | // RateLimiter(this._timeout); 11 | 12 | static final RateLimiter _rateLimiter = RateLimiter._internal(); 13 | 14 | factory RateLimiter() { 15 | return _rateLimiter; 16 | } 17 | 18 | RateLimiter._internal([this._timeout = 2]); 19 | 20 | Future shouldFetch(String key) async { 21 | int lastFetched = await RepositoryCommon.getValue(key); 22 | // print('lastFetched $lastFetched'); 23 | var now = DateTime.now(); 24 | // print('now ${now.millisecondsSinceEpoch}'); 25 | if (lastFetched == 0) { 26 | // print('shouldFetch yes lastFetched == null'); 27 | await RepositoryCommon.insertMetadata(key); 28 | return true; 29 | } 30 | 31 | if (now.millisecondsSinceEpoch - lastFetched > 32 | Duration(minutes: _timeout).inMilliseconds) { 33 | // print('shouldFetch yes timeout'); 34 | await RepositoryCommon.insertMetadata(key); 35 | return true; 36 | } 37 | 38 | // print('shouldFetch false'); 39 | return false; 40 | } 41 | 42 | Future reset(String key) async { 43 | return await RepositoryCommon.deleteMetadata(key); 44 | } 45 | } 46 | 47 | RateLimiter getRateLimiter = RateLimiter(); 48 | -------------------------------------------------------------------------------- /lib/widget/app_menu.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class BottomSheetMenu extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) { 6 | return Container( 7 | color: Colors.transparent, 8 | child: Container( 9 | padding: const EdgeInsets.only(bottom: 8.0, left: 8.0), 10 | decoration: new BoxDecoration( 11 | color: Colors.white, 12 | borderRadius: BorderRadius.only( 13 | topLeft: const Radius.circular(15.0), 14 | topRight: const Radius.circular(15.0))), 15 | child: Wrap( 16 | children: [ 17 | ListTile( 18 | leading: IconButton( 19 | padding: EdgeInsets.symmetric(vertical: 4.0), 20 | icon: Icon(Icons.account_circle, size: 48.0), 21 | onPressed: () {}), 22 | title: Text('Username', 23 | style: Theme.of(context) 24 | .textTheme 25 | .subhead 26 | .copyWith(fontWeight: FontWeight.w500)), 27 | subtitle: Text("username@gmail.com"), 28 | onTap: () {}), 29 | Divider(), 30 | ListTile( 31 | leading: Icon(Icons.notifications), 32 | title: Text('Notifications'), 33 | onTap: () {}), 34 | ListTile( 35 | leading: Icon(Icons.settings), 36 | title: Text('Settings'), 37 | onTap: () {}), 38 | ListTile( 39 | leading: Icon(Icons.help), 40 | title: Text('Help & Feedback'), 41 | onTap: () {}), 42 | ], 43 | ), 44 | ), 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/widget/article_bottom_section.dart: -------------------------------------------------------------------------------- 1 | import 'package:briefing/model/article.dart'; 2 | import 'package:briefing/widget/article_menu.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class ArticleBottomSection extends StatelessWidget { 6 | final Article article; 7 | 8 | const ArticleBottomSection({Key key, @required this.article}) 9 | : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | void _modalBottomSheetMenu() { 14 | showModalBottomSheet( 15 | context: context, 16 | builder: (BuildContext context) { 17 | return BottomSheetArticleMenu(article: article); 18 | }, 19 | ); 20 | } 21 | 22 | return Container( 23 | padding: const EdgeInsets.only(top: 8.0, bottom: 4.0), 24 | child: Row( 25 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 26 | children: [ 27 | Row( 28 | children: [ 29 | Text(article.timeAgo, style: Theme.of(context).textTheme.subtitle) 30 | ], 31 | ), 32 | InkWell( 33 | child: Icon(Icons.more_vert), 34 | onTap: () { 35 | _modalBottomSheetMenu(); 36 | }, 37 | ), 38 | ], 39 | ), 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/widget/article_menu.dart: -------------------------------------------------------------------------------- 1 | import 'package:briefing/model/article.dart'; 2 | import 'package:briefing/service/database.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:share/share.dart'; 5 | 6 | class BottomSheetArticleMenu extends StatefulWidget { 7 | final Article article; 8 | 9 | const BottomSheetArticleMenu({Key key, @required this.article}) 10 | : super(key: key); 11 | 12 | @override 13 | _BottomSheetArticleMenuState createState() => _BottomSheetArticleMenuState(); 14 | } 15 | 16 | class _BottomSheetArticleMenuState extends State { 17 | @override 18 | Widget build(BuildContext context) { 19 | return Container( 20 | color: Colors.transparent, 21 | child: Container( 22 | padding: EdgeInsets.all(8.0), 23 | decoration: new BoxDecoration( 24 | color: Colors.white, 25 | borderRadius: BorderRadius.only( 26 | topLeft: const Radius.circular(10.0), 27 | topRight: const Radius.circular(10.0))), 28 | child: Wrap(children: [ 29 | ListTile( 30 | leading: Icon(Icons.share, size: 28.0), 31 | title: Text('Share'), 32 | onTap: () { 33 | Share.share('check out ${widget.article.url}'); 34 | }, 35 | ), 36 | ListTile( 37 | leading: Icon( 38 | widget.article.bookmarked 39 | ? Icons.bookmark 40 | : Icons.bookmark_border, 41 | size: 28.0, 42 | ), 43 | title: Text(widget.article.bookmarked ? 'Bookmarked' : 'Bookmark'), 44 | onTap: () async { 45 | setState(() { 46 | widget.article.bookmarked = !widget.article.bookmarked; 47 | }); 48 | 49 | int id = await DatabaseService.db.updateArticle(widget.article); 50 | print('Article $id updated'); 51 | Navigator.of(context).pop(); 52 | }, 53 | ), 54 | ]), 55 | ), 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/widget/article_thumbnail.dart: -------------------------------------------------------------------------------- 1 | import 'package:briefing/model/article.dart'; 2 | import 'package:cached_network_image/cached_network_image.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class ArticleThumbnail extends StatefulWidget { 6 | final Article article; 7 | 8 | const ArticleThumbnail({Key key, @required this.article}) : super(key: key); 9 | @override 10 | _ArticleThumbnailState createState() => _ArticleThumbnailState(); 11 | } 12 | 13 | class _ArticleThumbnailState extends State { 14 | @override 15 | Widget build(BuildContext context) { 16 | return Container( 17 | child: CachedNetworkImage( 18 | imageUrl: widget.article.imageUrl, 19 | imageBuilder: (context, imageProvider) => Container( 20 | width: 92.0, 21 | height: 92.0, 22 | decoration: BoxDecoration( 23 | shape: BoxShape.rectangle, 24 | borderRadius: BorderRadius.circular(5.0), 25 | image: DecorationImage( 26 | image: imageProvider, fit: BoxFit.cover)), 27 | ), 28 | placeholder: (context, url) => Container(), 29 | errorWidget: (context, url, error) => Container()), 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/widget/article_title_section.dart: -------------------------------------------------------------------------------- 1 | import 'package:briefing/model/article.dart'; 2 | import 'package:briefing/widget/article_thumbnail.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_custom_tabs/flutter_custom_tabs.dart'; 5 | 6 | class ArticleTitleSection extends StatelessWidget { 7 | final Article article; 8 | 9 | const ArticleTitleSection({Key key, @required this.article}) 10 | : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Row( 15 | children: [ 16 | Expanded( 17 | child: Column( 18 | crossAxisAlignment: CrossAxisAlignment.start, 19 | children: [ 20 | Row(children: [ 21 | Expanded( 22 | child: Text( 23 | article.source, 24 | maxLines: 1, 25 | overflow: TextOverflow.ellipsis, 26 | style: Theme.of(context).textTheme.subtitle, 27 | ), 28 | ), 29 | ]), 30 | Container( 31 | padding: EdgeInsets.fromLTRB(0.0, 4.0, 8.0, 4.0), 32 | child: ListTile( 33 | contentPadding: EdgeInsets.symmetric(horizontal: 1.0), 34 | title: Text(article.title, 35 | softWrap: true, 36 | style: Theme.of(context) 37 | .textTheme 38 | .subhead 39 | .copyWith(fontWeight: FontWeight.bold, fontSize: 20)), 40 | ), 41 | ) 42 | ], 43 | ), 44 | ), 45 | // if (article.imageUrl != null) ArticleThumbnail(article: article), 46 | ], 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/widget/main_sliverappbar.dart: -------------------------------------------------------------------------------- 1 | import 'package:briefing/widget/app_menu.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class MainSliverAppBar extends StatelessWidget { 5 | final String title; 6 | 7 | const MainSliverAppBar({Key key, this.title}) : super(key: key); 8 | @override 9 | Widget build(BuildContext context) { 10 | void _modalMenu() { 11 | showModalBottomSheet( 12 | context: context, 13 | builder: (BuildContext context) { 14 | return BottomSheetMenu(); 15 | }, 16 | ); 17 | } 18 | 19 | return SliverAppBar( 20 | elevation: 1.0, 21 | pinned: true, 22 | forceElevated: true, 23 | leading: IconButton( 24 | icon: Icon( 25 | Icons.search, 26 | semanticLabel: 'search', 27 | ), 28 | onPressed: () { 29 | print('Search menu'); 30 | }, 31 | ), 32 | centerTitle: true, 33 | title: Text(title), 34 | actions: [ 35 | Container( 36 | margin: EdgeInsets.only(right: 6.0), 37 | child: IconButton( 38 | icon: Icon(Icons.account_circle, size: 32.0), 39 | onPressed: () { 40 | _modalMenu(); 41 | }, 42 | ), 43 | ), 44 | ], 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: briefing 2 | description: A new Flutter project. 3 | 4 | version: 1.0.0+1 5 | 6 | environment: 7 | sdk: ">=2.2.2 <3.0.0" 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | sqflite: ^1.1.6+3 13 | # moor_flutter: ^1.6.0 14 | url_launcher: ^5.1.2 15 | share: ^0.6.2+1 16 | intl: ^0.15.8 17 | http: ^0.12.0+2 18 | rxdart: ^0.22.1 19 | xml: ^3.5.0 20 | cached_network_image: ^1.1.1 21 | flutter_custom_tabs: ^0.6.0 22 | cupertino_icons: ^0.1.2 23 | 24 | dev_dependencies: 25 | flutter_test: 26 | sdk: flutter 27 | 28 | flutter: 29 | 30 | uses-material-design: true 31 | assets: 32 | - assets/images/no_internet.png 33 | 34 | fonts: 35 | - family: Libre_Franklin 36 | fonts: 37 | - asset: assets/fonts/Libre_Franklin/LibreFranklin-Regular.ttf 38 | - asset: assets/fonts/Libre_Franklin/LibreFranklin-Bold.ttf 39 | weight: 700 40 | - family: Crimson_Text 41 | fonts: 42 | - asset: assets/fonts/Crimson_Text/CrimsonText-Regular.ttf 43 | - asset: assets/fonts/Crimson_Text/CrimsonText-SemiBold.ttf 44 | weight: 600 45 | - asset: assets/fonts/Crimson_Text/CrimsonText-Bold.ttf 46 | weight: 700 47 | -------------------------------------------------------------------------------- /screenshots/ui_bottomsheet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakiang/briefing/bbab6e8743c4a749a873fce4b9c46b2b4415a62a/screenshots/ui_bottomsheet.jpg -------------------------------------------------------------------------------- /screenshots/ui_list.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakiang/briefing/bbab6e8743c4a749a873fce4b9c46b2b4415a62a/screenshots/ui_list.jpg -------------------------------------------------------------------------------- /screenshots/ui_main_list.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakiang/briefing/bbab6e8743c4a749a873fce4b9c46b2b4415a62a/screenshots/ui_main_list.jpg -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:briefing/main.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | void main() { 12 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 13 | // Build our app and trigger a frame. 14 | await tester.pumpWidget(MyApp()); 15 | 16 | // Verify that our counter starts at 0. 17 | // expect(find.text('0'), findsOneWidget); 18 | // expect(find.text('1'), findsNothing); 19 | // 20 | // // Tap the '+' icon and trigger a frame. 21 | // await tester.tap(find.byIcon(Icons.add)); 22 | // await tester.pump(); 23 | // 24 | // // Verify that our counter has incremented. 25 | // expect(find.text('0'), findsNothing); 26 | // expect(find.text('1'), findsOneWidget); 27 | }); 28 | } 29 | --------------------------------------------------------------------------------