├── res └── values │ └── strings_en.arb ├── android ├── gradle.properties ├── app │ ├── src │ │ └── main │ │ │ ├── res │ │ │ ├── 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 │ │ │ └── drawable │ │ │ │ └── launch_background.xml │ │ │ ├── java │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── hawalnir1 │ │ │ │ └── MainActivity.java │ │ │ └── AndroidManifest.xml │ └── build.gradle ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── settings.gradle └── build.gradle ├── ios ├── Flutter │ ├── .last_build_id │ ├── Debug.xcconfig │ ├── Release.xcconfig │ ├── flutter_export_environment.sh │ ├── Flutter.podspec │ └── AppFrameworkInfo.plist ├── Runner │ ├── AppDelegate.h │ ├── Assets.xcassets │ │ ├── LaunchImage.imageset │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ ├── README.md │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── 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-1024x1024@1x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ └── Contents.json │ ├── main.m │ ├── AppDelegate.m │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── Podfile ├── macos ├── Runner │ ├── Configs │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ ├── Warnings.xcconfig │ │ └── AppInfo.xcconfig │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ ├── app_icon_128.png │ │ │ ├── app_icon_16.png │ │ │ ├── app_icon_256.png │ │ │ ├── app_icon_32.png │ │ │ ├── app_icon_512.png │ │ │ ├── app_icon_64.png │ │ │ ├── app_icon_1024.png │ │ │ └── Contents.json │ ├── Release.entitlements │ ├── AppDelegate.swift │ ├── DebugProfile.entitlements │ ├── MainFlutterWindow.swift │ └── Info.plist ├── .gitignore ├── Flutter │ ├── Flutter-Debug.xcconfig │ ├── Flutter-Release.xcconfig │ └── GeneratedPluginRegistrant.swift ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── RunnerTests │ └── RunnerTests.swift └── Podfile ├── assets └── images │ └── placeholder.png ├── lib ├── src │ ├── fonts │ │ ├── NotoKufiArabic-Regular.ttf │ │ └── NotoSansArabic-Regular.ttf │ ├── view_models │ │ └── app_key.dart │ ├── widgets │ │ ├── hawalnir-date-convertor.dart │ │ ├── bottom_bar.dart │ │ ├── error_indicator.dart │ │ ├── sliver_app_bar.dart │ │ ├── loading_indicator.dart │ │ ├── shimmer_loading.dart │ │ ├── posts_card.dart │ │ ├── eachPost.dart │ │ ├── loading_placeholders.dart │ │ ├── animated_fab.dart │ │ ├── catWidgets.dart │ │ ├── post_card.dart │ │ └── drawerMain.dart │ ├── pages │ │ ├── instagramPage.dart │ │ ├── home.dart │ │ └── listView.dart │ ├── models │ │ ├── category.dart │ │ ├── users.dart │ │ ├── settings.dart │ │ ├── media.dart │ │ └── post.dart │ ├── app.dart │ ├── providers │ │ ├── settings_provider.dart │ │ ├── categories_provider.dart │ │ └── posts_provider.dart │ ├── config.dart │ ├── db │ │ ├── functions.dart │ │ └── database_helper.dart │ ├── theme │ │ └── app_theme.dart │ └── screens │ │ ├── post_details_screen.dart │ │ ├── category_posts_screen.dart │ │ ├── categories_screen.dart │ │ └── home_screen.dart ├── wordpress_client.dart └── main.dart ├── devtools_options.yaml ├── .metadata ├── test └── widget_test.dart ├── analysis_options.yaml ├── .gitignore ├── pubspec.yaml ├── .flutter-plugins-dependencies └── README.md /res/values/strings_en.arb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | -------------------------------------------------------------------------------- /ios/Flutter/.last_build_id: -------------------------------------------------------------------------------- 1 | 042cb29e1885aec0ee67961cc9956487 -------------------------------------------------------------------------------- /macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /assets/images/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hooshyar/Flutter-Wordpress-Client/HEAD/assets/images/placeholder.png -------------------------------------------------------------------------------- /macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/dgph 7 | **/xcuserdata/ 8 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /lib/src/fonts/NotoKufiArabic-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hooshyar/Flutter-Wordpress-Client/HEAD/lib/src/fonts/NotoKufiArabic-Regular.ttf -------------------------------------------------------------------------------- /lib/src/fonts/NotoSansArabic-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hooshyar/Flutter-Wordpress-Client/HEAD/lib/src/fonts/NotoSansArabic-Regular.ttf -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hooshyar/Flutter-Wordpress-Client/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hooshyar/Flutter-Wordpress-Client/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hooshyar/Flutter-Wordpress-Client/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hooshyar/Flutter-Wordpress-Client/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hooshyar/Flutter-Wordpress-Client/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/java/com/example/hawalnir1/MainActivity.java: -------------------------------------------------------------------------------- 1 | import io.flutter.embedding.android.FlutterActivity; 2 | public class MainActivity extends FlutterActivity { 3 | 4 | } -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hooshyar/Flutter-Wordpress-Client/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hooshyar/Flutter-Wordpress-Client/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hooshyar/Flutter-Wordpress-Client/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hooshyar/Flutter-Wordpress-Client/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hooshyar/Flutter-Wordpress-Client/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hooshyar/Flutter-Wordpress-Client/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hooshyar/Flutter-Wordpress-Client/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hooshyar/Flutter-Wordpress-Client/HEAD/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/hooshyar/Flutter-Wordpress-Client/HEAD/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/hooshyar/Flutter-Wordpress-Client/HEAD/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/hooshyar/Flutter-Wordpress-Client/HEAD/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/hooshyar/Flutter-Wordpress-Client/HEAD/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/hooshyar/Flutter-Wordpress-Client/HEAD/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/hooshyar/Flutter-Wordpress-Client/HEAD/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/hooshyar/Flutter-Wordpress-Client/HEAD/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/hooshyar/Flutter-Wordpress-Client/HEAD/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/hooshyar/Flutter-Wordpress-Client/HEAD/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/hooshyar/Flutter-Wordpress-Client/HEAD/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/hooshyar/Flutter-Wordpress-Client/HEAD/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/hooshyar/Flutter-Wordpress-Client/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hooshyar/Flutter-Wordpress-Client/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hooshyar/Flutter-Wordpress-Client/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hooshyar/Flutter-Wordpress-Client/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hooshyar/Flutter-Wordpress-Client/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hooshyar/Flutter-Wordpress-Client/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /devtools_options.yaml: -------------------------------------------------------------------------------- 1 | description: This file stores settings for Dart & Flutter DevTools. 2 | documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states 3 | extensions: 4 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.4-all.zip 7 | -------------------------------------------------------------------------------- /macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/src/view_models/app_key.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class Keys with ChangeNotifier { 4 | final GlobalKey appScaffoldKey = GlobalKey(); 5 | // int _count = 0; 6 | // int get count => _count; 7 | 8 | // void increment() { 9 | // _count++; 10 | // notifyListeners(); 11 | // } 12 | } 13 | -------------------------------------------------------------------------------- /lib/wordpress_client.dart: -------------------------------------------------------------------------------- 1 | /// Support for doing something awesome. 2 | /// 3 | /// More dartdocs go here. 4 | library wordpress_client; 5 | 6 | export 'src/models/category.dart'; 7 | export 'src/models/media.dart'; 8 | export 'src/models/post.dart'; 9 | export 'src/models/users.dart'; 10 | export 'src/models/settings.dart'; 11 | export 'src/client.dart'; 12 | -------------------------------------------------------------------------------- /macos/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @main 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | 10 | override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { 11 | return true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | com.apple.security.network.server 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | class MainFlutterWindow: NSWindow { 5 | override func awakeFromNib() { 6 | let flutterViewController = FlutterViewController() 7 | let windowFrame = self.frame 8 | self.contentViewController = flutterViewController 9 | self.setFrame(windowFrame, display: true) 10 | 11 | RegisterGeneratedPlugins(registry: flutterViewController) 12 | 13 | super.awakeFromNib() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/src/widgets/hawalnir-date-convertor.dart: -------------------------------------------------------------------------------- 1 | import 'package:intl/intl.dart'; 2 | 3 | 4 | dynamic dateConvertor(String value) { 5 | 6 | //value= "hello"; 7 | String convertedValue; 8 | 9 | 10 | 11 | convertedValue = 12 | DateFormat('y/M/d H:m') 13 | .format(DateTime.parse(value)); 14 | 15 | return convertedValue; 16 | 17 | 18 | } 19 | 20 | //if (!value.contains('@')) { 21 | // return 'Please enter a valid email'; 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:3.1.2' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | google() 15 | jcenter() 16 | } 17 | } 18 | 19 | rootProject.buildDir = '../build' 20 | subprojects { 21 | project.buildDir = "${rootProject.buildDir}/${project.name}" 22 | } 23 | subprojects { 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = hawalnir1 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = com.example.hawalnir1 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. 15 | -------------------------------------------------------------------------------- /macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import connectivity_plus 9 | import path_provider_foundation 10 | import shared_preferences_foundation 11 | import sqflite_darwin 12 | 13 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 14 | ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) 15 | PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) 16 | SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) 17 | SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) 18 | } 19 | -------------------------------------------------------------------------------- /ios/Flutter/flutter_export_environment.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # This is a generated file; do not edit or check into version control. 3 | export "FLUTTER_ROOT=/Users/hooshyar/Desktop/development/flutter" 4 | export "FLUTTER_APPLICATION_PATH=/Users/hooshyar/Desktop/development/Flutter-Wordpress-Client" 5 | export "COCOAPODS_PARALLEL_CODE_SIGN=true" 6 | export "FLUTTER_TARGET=/Users/hooshyar/Desktop/development/Flutter-Wordpress-Client/lib/main.dart" 7 | export "FLUTTER_BUILD_DIR=build" 8 | export "FLUTTER_BUILD_NAME=1.0.0" 9 | export "FLUTTER_BUILD_NUMBER=1" 10 | export "DART_OBFUSCATION=false" 11 | export "TRACK_WIDGET_CREATION=true" 12 | export "TREE_SHAKE_ICONS=false" 13 | export "PACKAGE_CONFIG=/Users/hooshyar/Desktop/development/Flutter-Wordpress-Client/.dart_tool/package_config.json" 14 | -------------------------------------------------------------------------------- /ios/Flutter/Flutter.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # This podspec is NOT to be published. It is only used as a local source! 3 | # This is a generated file; do not edit or check into version control. 4 | # 5 | 6 | Pod::Spec.new do |s| 7 | s.name = 'Flutter' 8 | s.version = '1.0.0' 9 | s.summary = 'A UI toolkit for beautiful and fast apps.' 10 | s.homepage = 'https://flutter.dev' 11 | s.license = { :type => 'BSD' } 12 | s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } 13 | s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s } 14 | s.ios.deployment_target = '12.0' 15 | # Framework linking is handled by Flutter tooling, not CocoaPods. 16 | # Add a placeholder to satisfy `s.dependency 'Flutter'` plugin podspecs. 17 | s.vendored_frameworks = 'path/to/nothing' 18 | end 19 | -------------------------------------------------------------------------------- /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 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/src/widgets/bottom_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | 4 | class BottomBar extends StatelessWidget { 5 | BottomBar({ 6 | required this.currentIndex, 7 | required this.onTap, 8 | required this.items, 9 | } 10 | ); 11 | 12 | final int currentIndex; 13 | final ValueChanged onTap; 14 | final List items; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | /// Yes - He is using CupertinoTabBar on both Android and iOS. It looks dope. 19 | /// I'm not a designer either, and only God can judge us. (╯°□°)╯︵ ┻━┻ 20 | return CupertinoTabBar( 21 | backgroundColor: Colors.black54, 22 | inactiveColor: Colors.white54, 23 | activeColor: Colors.white, 24 | iconSize: 24.0, 25 | currentIndex: currentIndex, 26 | onTap: onTap, 27 | items: items, 28 | ); 29 | } 30 | } -------------------------------------------------------------------------------- /.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: "c519ee916eaeb88923e67befb89c0f1dabfa83e6" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6 17 | base_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6 18 | - platform: macos 19 | create_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6 20 | base_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /lib/src/pages/instagramPage.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | 4 | 5 | class InstaPage extends StatefulWidget { 6 | _InstaPageState createState() => _InstaPageState(); 7 | } 8 | 9 | class _InstaPageState extends State { 10 | 11 | 12 | Widget build(BuildContext context) { 13 | return Scaffold( 14 | appBar: AppBar( 15 | title: Text("hello") 16 | ), 17 | body: 18 | Container( 19 | child: Stack( 20 | children: [ 21 | Text("text"), 22 | Container( 23 | margin: EdgeInsets.all(50.0), 24 | //width: 300.0 , 25 | //height: 300.0 , 26 | //padding: EdgeInsets.all(20.0), 27 | decoration: 28 | 29 | BoxDecoration( 30 | shape: BoxShape.rectangle, 31 | borderRadius: BorderRadius.all(Radius.circular(20.0)) , 32 | color: Colors.redAccent ), 33 | alignment: Alignment.center, 34 | padding: EdgeInsets.all(20.0) 35 | ), 36 | // BoxDecoration(shape: BoxShape.rectangle), 37 | 38 | 39 | 40 | ], 41 | ), 42 | 43 | ) 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /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 in the flutter_test package. 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:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:hawalnir1/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | // await tester.pumpWidget(const MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | $(PRODUCT_COPYRIGHT) 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /lib/src/widgets/error_indicator.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ErrorIndicator extends StatelessWidget { 4 | final String message; 5 | final VoidCallback? onRetry; 6 | 7 | const ErrorIndicator({ 8 | Key? key, 9 | required this.message, 10 | this.onRetry, 11 | }) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | final theme = Theme.of(context); 16 | 17 | return Center( 18 | child: Column( 19 | mainAxisAlignment: MainAxisAlignment.center, 20 | children: [ 21 | Icon( 22 | Icons.error_outline, 23 | size: 48, 24 | color: theme.colorScheme.error, 25 | ), 26 | const SizedBox(height: 16), 27 | Text( 28 | message, 29 | style: theme.textTheme.titleLarge, 30 | textAlign: TextAlign.center, 31 | ), 32 | if (onRetry != null) ...[ 33 | const SizedBox(height: 16), 34 | ElevatedButton( 35 | onPressed: onRetry, 36 | child: const Text('Retry'), 37 | ), 38 | ], 39 | ], 40 | ), 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/src/models/category.dart: -------------------------------------------------------------------------------- 1 | class Category { 2 | /// Unique identifier for the term. 3 | int? id; 4 | 5 | /// Number of published posts for the term. 6 | int? count; 7 | 8 | /// HTML description of the term. 9 | String? description; 10 | 11 | /// URL of the term. 12 | String? link; 13 | 14 | /// HTML title for the term 15 | String? name; 16 | 17 | /// An alphanumeric identifier for the term unique to its type. 18 | String? slug; 19 | 20 | /// Type attribution for the term. 21 | String? taxonomy; 22 | 23 | /// The parent term ID. 24 | int? parent; 25 | 26 | /// Meta fields 27 | dynamic meta; // List? 28 | 29 | Category(); 30 | 31 | Category.fromMap(Map map) { 32 | id = map['id']; 33 | count = map['count']; 34 | description = map['description']; 35 | link = map['link']; 36 | name = map['name']; 37 | slug = map['slug']; 38 | taxonomy = map['taxonomy']; 39 | parent = map['parent']; 40 | meta = map['meta']; 41 | } 42 | 43 | Map toMap() => { 44 | 'id': id, 45 | 'count': count, 46 | 'description': description, 47 | 'link': link, 48 | 'name': name, 49 | 'slug': slug, 50 | 'taxonomy': taxonomy, 51 | 'parent': parent, 52 | 'meta': meta, 53 | }; 54 | 55 | toString() => "Category => " + toMap().toString(); 56 | } 57 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '12.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at https://dart.dev/lints. 17 | # 18 | # Instead of disabling a lint rule for the entire project in the 19 | # section below, it can also be suppressed for a single line of code 20 | # or a specific dart file by using the `// ignore: name_of_lint` and 21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 22 | # producing the lint. 23 | rules: 24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 26 | 27 | # Additional information about this file can be found at 28 | # https://dart.dev/guides/language/analysis-options 29 | -------------------------------------------------------------------------------- /macos/Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '10.14' 2 | 3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 | 6 | project 'Runner', { 7 | 'Debug' => :debug, 8 | 'Profile' => :release, 9 | 'Release' => :release, 10 | } 11 | 12 | def flutter_root 13 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) 14 | unless File.exist?(generated_xcode_build_settings_path) 15 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" 16 | end 17 | 18 | File.foreach(generated_xcode_build_settings_path) do |line| 19 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 20 | return matches[1].strip if matches 21 | end 22 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" 23 | end 24 | 25 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 26 | 27 | flutter_macos_podfile_setup 28 | 29 | target 'Runner' do 30 | use_frameworks! 31 | use_modular_headers! 32 | 33 | flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) 34 | target 'RunnerTests' do 35 | inherit! :search_paths 36 | end 37 | end 38 | 39 | post_install do |installer| 40 | installer.pods_project.targets.each do |target| 41 | flutter_additional_macos_build_settings(target) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/src/widgets/sliver_app_bar.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:hawalnir1/src/view_models/app_key.dart'; 5 | import 'package:provider/provider.dart' as provider; 6 | 7 | import '../pages/listView.dart'; 8 | 9 | class SliverAppBarCustomized extends StatelessWidget { 10 | const SliverAppBarCustomized({Key? key}) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return SliverAppBar( 15 | backgroundColor: Colors.transparent, 16 | pinned: true, 17 | expandedHeight: 60.0, 18 | leading: IconButton( 19 | icon: Icon( 20 | Icons.menu, 21 | ), 22 | onPressed: () => provider.Provider.of(context, listen: false) 23 | .appScaffoldKey 24 | .currentState! 25 | .openDrawer()), 26 | flexibleSpace: Stack( 27 | children: [ 28 | Container( 29 | color: Colors.deepPurple.withOpacity(0.7), 30 | ), 31 | FlexibleSpaceBar( 32 | collapseMode: CollapseMode.pin, 33 | title: GestureDetector( 34 | child: BackdropFilter( 35 | filter: ImageFilter.blur(sigmaY: 5, sigmaX: 5), 36 | child: Text( 37 | 'Flutter-Wordpress-Client', 38 | style: TextStyle(fontSize: 16), 39 | ), 40 | ), 41 | onTap: scrollToTop, 42 | ), 43 | ), 44 | ], 45 | ), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.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 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # Visual Studio Code related 20 | .vscode/ 21 | 22 | # Flutter/Dart/Pub related 23 | **/doc/api/ 24 | .dart_tool/ 25 | .flutter-plugins 26 | .packages 27 | .pub-cache/ 28 | .pub/ 29 | build/ 30 | 31 | # Android related 32 | **/android/**/gradle-wrapper.jar 33 | **/android/.gradle 34 | **/android/captures/ 35 | **/android/gradlew 36 | **/android/gradlew.bat 37 | **/android/local.properties 38 | **/android/**/GeneratedPluginRegistrant.java 39 | 40 | # iOS/XCode related 41 | **/ios/**/*.mode1v3 42 | **/ios/**/*.mode2v3 43 | **/ios/**/*.moved-aside 44 | **/ios/**/*.pbxuser 45 | **/ios/**/*.perspectivev3 46 | **/ios/**/*sync/ 47 | **/ios/**/.sconsign.dblite 48 | **/ios/**/.tags* 49 | **/ios/**/.vagrant/ 50 | **/ios/**/DerivedData/ 51 | **/ios/**/Icon? 52 | **/ios/**/Pods/ 53 | **/ios/**/.symlinks/ 54 | **/ios/**/profile 55 | **/ios/**/xcuserdata 56 | **/ios/.generated/ 57 | **/ios/Flutter/App.framework 58 | **/ios/Flutter/Flutter.framework 59 | **/ios/Flutter/Generated.xcconfig 60 | **/ios/Flutter/app.flx 61 | **/ios/Flutter/app.zip 62 | **/ios/Flutter/flutter_assets/ 63 | **/ios/ServiceDefinitions.json 64 | **/ios/Runner/GeneratedPluginRegistrant.* 65 | 66 | # Exceptions to above rules. 67 | !**/ios/**/default.mode1v3 68 | !**/ios/**/default.mode2v3 69 | !**/ios/**/default.pbxuser 70 | !**/ios/**/default.perspectivev3 71 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 72 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "app_icon_16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "app_icon_32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "app_icon_32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "app_icon_64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "app_icon_128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "app_icon_256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "app_icon_256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "app_icon_512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "app_icon_512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "app_icon_1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/src/pages/home.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../../wordpress_client.dart'; 3 | import '../widgets/drawerMain.dart'; 4 | import '../widgets/catWidgets.dart'; 5 | import '../models/post.dart'; 6 | import '../config.dart'; 7 | import 'listView.dart'; 8 | 9 | class HomePage extends StatefulWidget { 10 | final WordPressClient client; 11 | 12 | const HomePage({Key? key, required this.client}) : super(key: key); 13 | 14 | @override 15 | _HomePageState createState() => _HomePageState(); 16 | } 17 | 18 | class _HomePageState extends State { 19 | List? _posts; 20 | bool _isLoading = true; 21 | 22 | @override 23 | void initState() { 24 | super.initState(); 25 | _loadPosts(); 26 | } 27 | 28 | Future _loadPosts() async { 29 | try { 30 | final response = await widget.client.getPosts(perPage: defaultPerPage); 31 | setState(() { 32 | _posts = response.items; 33 | _isLoading = false; 34 | }); 35 | } catch (e) { 36 | setState(() { 37 | _isLoading = false; 38 | }); 39 | if (mounted) { 40 | ScaffoldMessenger.of(context).showSnackBar( 41 | SnackBar(content: Text(connectionError)), 42 | ); 43 | } 44 | } 45 | } 46 | 47 | @override 48 | Widget build(BuildContext context) { 49 | return Scaffold( 50 | drawer: DrawerMain(client: widget.client), 51 | body: _isLoading 52 | ? const Center(child: CircularProgressIndicator()) 53 | : _posts == null 54 | ? Center(child: Text(connectionError)) 55 | : ListViewPosts(posts: _posts, client: widget.client), 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/src/pages/listView.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hawalnir1/src/widgets/sliver_app_bar.dart'; 3 | import '../../src/db/database_helper.dart'; 4 | import '../../src/db/functions.dart'; 5 | import '../../src/widgets/catWidgets.dart'; 6 | import '../../wordpress_client.dart'; 7 | import '../config.dart'; 8 | 9 | class ListViewPosts extends StatefulWidget { 10 | final List? posts; 11 | final WordPressClient client; 12 | 13 | const ListViewPosts({Key? key, required this.posts, required this.client}) 14 | : super(key: key); 15 | 16 | @override 17 | ListViewPostsState createState() => ListViewPostsState(); 18 | } 19 | 20 | var scrollCont = 21 | ScrollController(initialScrollOffset: 0.0, keepScrollOffset: true); 22 | 23 | class ListViewPostsState extends State { 24 | var dbHelper = DatabaseHelper(); 25 | 26 | int count = 0; 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | debugPrint('posts count' + widget.posts!.length.toString()); 31 | return Scaffold( 32 | body: Stack( 33 | children: [ 34 | RefreshIndicator( 35 | displacement: 150.0, 36 | onRefresh: () => widget.client.getPosts(perPage: defaultPerPage), 37 | child: CustomScrollView( 38 | controller: scrollCont, 39 | slivers: [ 40 | SliverAppBarCustomized(), 41 | sliverListGlobal(widget.posts!), 42 | ], 43 | ), 44 | ), 45 | ], 46 | ), 47 | ); 48 | } 49 | } 50 | 51 | void scrollToTop() { 52 | scrollCont.animateTo(0.0, 53 | duration: Duration(seconds: 1), curve: Curves.elasticInOut); 54 | } 55 | -------------------------------------------------------------------------------- /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 | hawalnir1 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 | CADisableMinimumFrameDurationOnPhone 45 | 46 | UIApplicationSupportsIndirectInputEvents 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /lib/src/widgets/loading_indicator.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:shimmer/shimmer.dart'; 3 | 4 | class LoadingIndicator extends StatelessWidget { 5 | const LoadingIndicator({Key? key}) : super(key: key); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | final theme = Theme.of(context); 10 | 11 | return Shimmer.fromColors( 12 | baseColor: theme.colorScheme.surfaceVariant, 13 | highlightColor: theme.colorScheme.surface, 14 | child: ListView.builder( 15 | padding: const EdgeInsets.all(16), 16 | itemCount: 3, 17 | itemBuilder: (context, index) => Padding( 18 | padding: const EdgeInsets.only(bottom: 16), 19 | child: Column( 20 | crossAxisAlignment: CrossAxisAlignment.start, 21 | children: [ 22 | Container( 23 | height: 200, 24 | decoration: BoxDecoration( 25 | color: Colors.white, 26 | borderRadius: BorderRadius.circular(8), 27 | ), 28 | ), 29 | const SizedBox(height: 8), 30 | Container( 31 | width: double.infinity, 32 | height: 24, 33 | decoration: BoxDecoration( 34 | color: Colors.white, 35 | borderRadius: BorderRadius.circular(4), 36 | ), 37 | ), 38 | const SizedBox(height: 8), 39 | Container( 40 | width: 200, 41 | height: 16, 42 | decoration: BoxDecoration( 43 | color: Colors.white, 44 | borderRadius: BorderRadius.circular(4), 45 | ), 46 | ), 47 | ], 48 | ), 49 | ), 50 | ), 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/src/app.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:shared_preferences/shared_preferences.dart'; 4 | 5 | import 'client.dart'; 6 | import 'config.dart'; 7 | import 'providers/posts_provider.dart'; 8 | import 'providers/categories_provider.dart'; 9 | import 'providers/settings_provider.dart'; 10 | import 'screens/home_screen.dart'; 11 | import 'theme/app_theme.dart'; 12 | 13 | class App extends StatelessWidget { 14 | final SharedPreferences? prefs; 15 | late final WordPressClient wordPressClient; 16 | 17 | App({Key? key, this.prefs}) : super(key: key) { 18 | wordPressClient = WordPressClient( 19 | baseUrl: baseUrl, 20 | prefs: prefs, 21 | cacheValidDuration: defaultCacheDuration, 22 | ); 23 | } 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return MultiProvider( 28 | providers: [ 29 | ChangeNotifierProvider( 30 | create: (_) => SettingsProvider(prefs), 31 | ), 32 | Provider.value(value: wordPressClient), 33 | ChangeNotifierProvider( 34 | create: (context) => PostsProvider( 35 | client: context.read(), 36 | settings: context.read(), 37 | ), 38 | ), 39 | ChangeNotifierProvider( 40 | create: (context) => CategoriesProvider( 41 | client: context.read(), 42 | ), 43 | ), 44 | ], 45 | child: Consumer( 46 | builder: (context, settings, _) => MaterialApp( 47 | title: 'WordPress Blog', 48 | theme: AppTheme.lightTheme, 49 | darkTheme: AppTheme.darkTheme, 50 | themeMode: settings.themeMode, 51 | home: const HomeScreen(), 52 | ), 53 | ), 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/src/providers/settings_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart' show ThemeMode; 3 | import 'package:shared_preferences/shared_preferences.dart'; 4 | 5 | class SettingsProvider with ChangeNotifier { 6 | final SharedPreferences? _prefs; 7 | static const String _themeKey = 'theme_mode'; 8 | static const String _perPageKey = 'posts_per_page'; 9 | static const String _cacheTimeKey = 'cache_duration_hours'; 10 | 11 | SettingsProvider(this._prefs) { 12 | _loadSettings(); 13 | } 14 | 15 | // Theme settings 16 | ThemeMode _themeMode = ThemeMode.system; 17 | ThemeMode get themeMode => _themeMode; 18 | 19 | // Posts per page 20 | int _postsPerPage = 10; 21 | int get postsPerPage => _postsPerPage; 22 | 23 | // Cache duration in hours 24 | int _cacheDurationHours = 24; 25 | int get cacheDurationHours => _cacheDurationHours; 26 | 27 | void _loadSettings() { 28 | if (_prefs == null) return; 29 | 30 | // Load theme 31 | final themeModeIndex = _prefs?.getInt(_themeKey) ?? 0; 32 | _themeMode = ThemeMode.values[themeModeIndex]; 33 | 34 | // Load posts per page 35 | _postsPerPage = _prefs?.getInt(_perPageKey) ?? 10; 36 | 37 | // Load cache duration 38 | _cacheDurationHours = _prefs?.getInt(_cacheTimeKey) ?? 24; 39 | } 40 | 41 | Future setThemeMode(ThemeMode mode) async { 42 | if (_themeMode == mode) return; 43 | _themeMode = mode; 44 | await _prefs?.setInt(_themeKey, mode.index); 45 | notifyListeners(); 46 | } 47 | 48 | Future setPostsPerPage(int count) async { 49 | if (_postsPerPage == count) return; 50 | _postsPerPage = count; 51 | await _prefs?.setInt(_perPageKey, count); 52 | notifyListeners(); 53 | } 54 | 55 | Future setCacheDuration(int hours) async { 56 | if (_cacheDurationHours == hours) return; 57 | _cacheDurationHours = hours; 58 | await _prefs?.setInt(_cacheTimeKey, hours); 59 | notifyListeners(); 60 | } 61 | 62 | Duration get cacheDuration => Duration(hours: _cacheDurationHours); 63 | } 64 | -------------------------------------------------------------------------------- /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 27 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.example.hawalnir1" 37 | minSdkVersion 16 38 | targetSdkVersion 27 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 | -------------------------------------------------------------------------------- /lib/src/widgets/shimmer_loading.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ShimmerLoading extends StatefulWidget { 4 | final Widget child; 5 | final bool isLoading; 6 | 7 | const ShimmerLoading({ 8 | Key? key, 9 | required this.child, 10 | required this.isLoading, 11 | }) : super(key: key); 12 | 13 | @override 14 | State createState() => _ShimmerLoadingState(); 15 | } 16 | 17 | class _ShimmerLoadingState extends State 18 | with SingleTickerProviderStateMixin { 19 | late AnimationController _controller; 20 | late Animation _animation; 21 | 22 | @override 23 | void initState() { 24 | super.initState(); 25 | _controller = AnimationController( 26 | vsync: this, 27 | duration: const Duration(milliseconds: 1500), 28 | )..repeat(); 29 | 30 | _animation = Tween(begin: -2, end: 2).animate( 31 | CurvedAnimation(parent: _controller, curve: Curves.easeInOutSine), 32 | ); 33 | } 34 | 35 | @override 36 | void dispose() { 37 | _controller.dispose(); 38 | super.dispose(); 39 | } 40 | 41 | @override 42 | Widget build(BuildContext context) { 43 | if (!widget.isLoading) { 44 | return widget.child; 45 | } 46 | 47 | return AnimatedBuilder( 48 | animation: _animation, 49 | builder: (context, child) { 50 | return ShaderMask( 51 | blendMode: BlendMode.srcATop, 52 | shaderCallback: (bounds) { 53 | return LinearGradient( 54 | colors: const [ 55 | Color(0xFFE0E0E0), 56 | Color(0xFFEEEEEE), 57 | Color(0xFFE0E0E0), 58 | ], 59 | stops: const [0.0, 0.5, 1.0], 60 | begin: Alignment( 61 | _animation.value - 1, 62 | 0.0, 63 | ), 64 | end: Alignment( 65 | _animation.value + 1, 66 | 0.0, 67 | ), 68 | ).createShader(bounds); 69 | }, 70 | child: child, 71 | ); 72 | }, 73 | child: widget.child, 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 10 | 15 | 18 | 21 | 28 | 32 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /lib/src/providers/categories_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import '../client.dart'; 3 | import '../models/category.dart' as wp; 4 | 5 | class CategoriesProvider with ChangeNotifier { 6 | final WordPressClient _client; 7 | 8 | List _categories = []; 9 | bool _isLoading = false; 10 | String? _error; 11 | 12 | List get categories => List.unmodifiable(_categories); 13 | bool get isLoading => _isLoading; 14 | String? get error => _error; 15 | 16 | CategoriesProvider({ 17 | required WordPressClient client, 18 | }) : _client = client { 19 | _loadCategories(); 20 | } 21 | 22 | Future _loadCategories() async { 23 | try { 24 | _isLoading = true; 25 | notifyListeners(); 26 | 27 | _categories = await _client.getCategories( 28 | hideEmpty: true, 29 | page: 1, 30 | perPage: 10, 31 | ); 32 | _error = null; 33 | } catch (e) { 34 | _error = 'Failed to load categories: $e'; 35 | print('Categories error: $e'); 36 | } finally { 37 | _isLoading = false; 38 | notifyListeners(); 39 | } 40 | } 41 | 42 | Future refreshCategories() async { 43 | return _loadCategories(); 44 | } 45 | 46 | wp.Category? getCategoryById(int id) { 47 | try { 48 | return _categories.firstWhere((cat) => cat.id == id); 49 | } catch (_) { 50 | return null; 51 | } 52 | } 53 | 54 | List getCategoriesByIds(List ids) { 55 | return _categories.where((cat) => ids.contains(cat.id)).toList(); 56 | } 57 | 58 | Future fetchCategories() async { 59 | try { 60 | _isLoading = true; 61 | notifyListeners(); 62 | 63 | final categories = await _client.getCategories( 64 | hideEmpty: true, 65 | page: 1, 66 | perPage: 10, 67 | ); 68 | 69 | if (categories.isNotEmpty) { 70 | _categories = categories; 71 | _error = null; 72 | } else { 73 | _error = 'No categories found'; 74 | } 75 | } catch (e) { 76 | _error = 'Failed to load categories: $e'; 77 | } finally { 78 | _isLoading = false; 79 | notifyListeners(); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /lib/src/models/users.dart: -------------------------------------------------------------------------------- 1 | class User { 2 | int? id; 3 | String? name; 4 | String? url; 5 | String? description; 6 | String? link; 7 | String? slug; 8 | AvatarUrls? avatarUrls; 9 | List? meta; 10 | 11 | 12 | User( 13 | {this.id, 14 | this.name, 15 | this.url, 16 | this.description, 17 | this.link, 18 | this.slug, 19 | this.avatarUrls, 20 | this.meta, 21 | }); 22 | 23 | User.fromJson(Map json) { 24 | id = json['id']; 25 | name = json['name']; 26 | url = json['url']; 27 | description = json['description']; 28 | link = json['link']; 29 | slug = json['slug']; 30 | avatarUrls = json['avatar_urls'] != null 31 | ? new AvatarUrls.fromJson(json['avatar_urls']) 32 | : null; 33 | 34 | 35 | } 36 | 37 | User.fromMap(Map userMap); 38 | 39 | Map toJson() { 40 | final Map data = new Map(); 41 | data['id'] = this.id; 42 | data['name'] = this.name; 43 | data['url'] = this.url; 44 | data['description'] = this.description; 45 | data['link'] = this.link; 46 | data['slug'] = this.slug; 47 | if (this.avatarUrls != null) { 48 | data['avatar_urls'] = this.avatarUrls!.toJson(); 49 | } 50 | 51 | return data; 52 | } 53 | } 54 | 55 | class AvatarUrls { 56 | String? s24; 57 | String? s48; 58 | String? s96; 59 | 60 | AvatarUrls({this.s24, this.s48, this.s96}); 61 | 62 | AvatarUrls.fromJson(Map json) { 63 | s24 = json['24']; 64 | s48 = json['48']; 65 | s96 = json['96']; 66 | } 67 | 68 | Map toJson() { 69 | final Map data = new Map(); 70 | data['24'] = this.s24; 71 | data['48'] = this.s48; 72 | data['96'] = this.s96; 73 | return data; 74 | } 75 | } 76 | 77 | 78 | 79 | class Self { 80 | String? href; 81 | 82 | Self({this.href}); 83 | 84 | Self.fromJson(Map json) { 85 | href = json['href']; 86 | } 87 | 88 | Map toJson() { 89 | final Map data = new Map(); 90 | data['href'] = this.href; 91 | return data; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /lib/src/models/settings.dart: -------------------------------------------------------------------------------- 1 | class Settings { 2 | /// Site title. 3 | String? title; 4 | 5 | /// Site tagline. 6 | String? description; 7 | 8 | /// A city in the same timezone as you. 9 | String? timezone; 10 | 11 | /// A date format for all date strings. 12 | String? dateFormat; 13 | 14 | /// A time format for all time strings. 15 | String? timeFormat; 16 | 17 | /// A day number of the week that the week should start on. 18 | int? startOfWeek; 19 | 20 | /// WordPress locale code. 21 | String? language; 22 | 23 | /// Convert emoticons like :-) and :-P to graphics on display. 24 | bool? useSmilies; 25 | 26 | /// Default post category. 27 | int? defaultCategory; 28 | 29 | /// Default post format. 30 | String? defaultPostFormat; 31 | 32 | /// Blog pages show at most. 33 | int? postsPerPage; 34 | 35 | /// Allow link notifications from other blogs (pingbacks and trackbacks) on new articles. 36 | String? defaultPingStatus; 37 | 38 | /// Allow people to post comments on new articles. 39 | String? defaultCommentStatus; 40 | 41 | Settings.fromMap(Map map) { 42 | title = map['title']; 43 | description = map['description']; 44 | timezone = map['timezone']; 45 | dateFormat = map['date_format']; 46 | timeFormat = map['time_format']; 47 | startOfWeek = map['start_of_week']; 48 | language = map['language']; 49 | useSmilies = map['use_smilies']; 50 | defaultCategory = map['default_category']; 51 | defaultPostFormat = map['default_post_format']; 52 | postsPerPage = map['posts_per_page']; 53 | defaultPingStatus = map['default_ping_status']; 54 | defaultCommentStatus = map['default_comment_status']; 55 | } 56 | 57 | Map toMap() => { 58 | 'title': title, 59 | 'description': description, 60 | 'timezone': timezone, 61 | 'date_format': dateFormat, 62 | 'time_format': timeFormat, 63 | 'start_of_week': startOfWeek, 64 | 'language': language, 65 | 'use_smilies': useSmilies, 66 | 'default_category': defaultCategory, 67 | 'default_post_format': defaultPostFormat, 68 | 'posts_per_page': postsPerPage, 69 | 'default_ping_status': defaultPingStatus, 70 | 'default_comment_status': defaultCommentStatus, 71 | }; 72 | 73 | toString() => "Settings => " + toMap().toString(); 74 | } 75 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'dart:async'; // Add import for TimeoutException 4 | import 'package:shared_preferences/shared_preferences.dart'; 5 | import 'src/app.dart'; 6 | import 'dart:math'; 7 | 8 | Future main() async { 9 | // Ensure Flutter bindings are initialized before any platform channels 10 | WidgetsFlutterBinding.ensureInitialized(); 11 | 12 | SharedPreferences? prefs; 13 | try { 14 | // Initialize shared preferences with platform check 15 | if (!kIsWeb) { 16 | // Add exponential backoff retry mechanism 17 | int maxRetries = 3; // Reduced from 5 to 3 18 | int currentTry = 0; 19 | Duration timeout = const Duration(seconds: 3); // Reduced from 5 to 3 20 | 21 | while (currentTry < maxRetries) { 22 | try { 23 | prefs = await SharedPreferences.getInstance().timeout( 24 | timeout, 25 | onTimeout: () { 26 | throw TimeoutException( 27 | 'SharedPreferences initialization timed out after ${timeout.inSeconds}s', 28 | timeout, 29 | ); 30 | }, 31 | ); 32 | 33 | if (prefs != null) { 34 | debugPrint( 35 | 'SharedPreferences initialized successfully on attempt ${currentTry + 1}'); 36 | break; 37 | } 38 | } catch (e) { 39 | currentTry++; 40 | if (currentTry >= maxRetries) { 41 | debugPrint( 42 | 'Failed to initialize SharedPreferences after $maxRetries attempts'); 43 | break; // Don't rethrow, just break and continue without prefs 44 | } 45 | // Exponential backoff with shorter delays 46 | final waitTime = Duration(seconds: pow(2, currentTry).toInt()); 47 | debugPrint( 48 | 'Retrying SharedPreferences initialization in ${waitTime.inSeconds}s'); 49 | await Future.delayed(waitTime); 50 | } 51 | } 52 | } else { 53 | debugPrint('Running on web platform - using alternative storage'); 54 | } 55 | } catch (e) { 56 | // Log error but don't prevent app from starting 57 | debugPrint('SharedPreferences initialization failed:'); 58 | debugPrint('Error type: ${e.runtimeType}'); 59 | debugPrint('Error details: $e'); 60 | debugPrint('App will continue without caching support'); 61 | } 62 | 63 | // Run app with or without SharedPreferences 64 | runApp(App(prefs: prefs)); 65 | } 66 | -------------------------------------------------------------------------------- /lib/src/widgets/posts_card.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:hawalnir1/src/models/post.dart'; 5 | 6 | import '../pages/listView.dart'; 7 | import 'catWidgets.dart'; 8 | import 'eachPost.dart'; 9 | 10 | class PostsCard extends StatefulWidget { 11 | PostsCard({Key? key, this.post}) : super(key: key); 12 | final Post? post ; 13 | 14 | _PostsCardState createState() => _PostsCardState(); 15 | } 16 | 17 | class _PostsCardState extends State { 18 | @override 19 | Widget build(BuildContext context) { 20 | return Card( 21 | shape: RoundedRectangleBorder( 22 | borderRadius: BorderRadius.all(Radius.circular(10.0))), 23 | clipBehavior: Clip.hardEdge, 24 | child: Column( 25 | children: [ 26 | Stack( 27 | children: [ 28 | Hero(tag: 'hero${widget.post!.id}', child: hawalImage(widget.post!)), 29 | Positioned( 30 | bottom: 2.0, 31 | left: 5.0, 32 | child: new ButtonTheme( 33 | child: hawalBtnBar(), 34 | ), 35 | ), 36 | ], 37 | ), 38 | new Padding( 39 | padding: EdgeInsets.all(5.0), 40 | child: new ListTile( 41 | onTap: () { 42 | Navigator.push( 43 | context, 44 | MaterialPageRoute( 45 | builder: (context) => HawalnirPost(post: widget.post), 46 | ), 47 | ); 48 | }, 49 | title: hawalTitle(widget.post!), 50 | subtitle: Row( 51 | // crossAxisAlignment: CrossAxisAlignment.center, 52 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 53 | children: [ 54 | hawalAuthor(widget.post!), 55 | hawalDate(widget.post!), 56 | ], 57 | ), 58 | ), 59 | ), 60 | ], 61 | ), 62 | ); 63 | } 64 | 65 | 66 | sliverAppBarGlobal() { 67 | return SliverAppBar( 68 | backgroundColor: Colors.deepPurple, 69 | pinned: true, 70 | expandedHeight: 70.0, 71 | flexibleSpace: FlexibleSpaceBar( 72 | collapseMode: CollapseMode.pin, 73 | title: GestureDetector( 74 | child: Text('WPFlutter'), 75 | onTap: scrollToTop, 76 | ), 77 | ), 78 | actions: [ 79 | IconButton( 80 | icon: const Icon(Icons.add_circle), 81 | tooltip: 'Add new entry', 82 | onPressed: () {}, 83 | ), 84 | ], 85 | ); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /lib/src/config.dart: -------------------------------------------------------------------------------- 1 | /// WordPress site configuration 2 | const String baseUrl = 'https://www.ferbon.com'; 3 | const String wordPressUrl = '$baseUrl/wp-json/wp/v2'; 4 | 5 | /// Default pagination settings 6 | const int defaultPerPage = 10; 7 | const int maxPerPage = 100; 8 | 9 | /// Cache settings 10 | const Duration defaultCacheDuration = Duration(hours: 24); 11 | 12 | /// Error messages 13 | const String connectionError = 'Internet connection problem'; 14 | const String serverError = 'Server error occurred'; 15 | const String noPostsError = 'No posts found'; 16 | 17 | /// UI constants 18 | const double defaultPadding = 16.0; 19 | const double defaultSpacing = 8.0; 20 | const double defaultRadius = 4.0; 21 | const double barIconSize = 40.0; 22 | 23 | /// Featured categories 24 | const Map categories = { 25 | 'News': CategoryInfo(id: 176, name: 'News'), 26 | 'Technology': CategoryInfo(id: 9875, name: 'Technology'), 27 | 'Culture': CategoryInfo(id: 207, name: 'Culture'), 28 | 'Health': CategoryInfo(id: 208, name: 'Health'), 29 | 'Kurdistan': CategoryInfo(id: 188, name: 'Kurdistan'), 30 | 'Iraq': CategoryInfo(id: 6098, name: 'Iraq'), 31 | 'Woman': CategoryInfo(id: 9102, name: 'Woman'), 32 | 'Jihan': CategoryInfo(id: 195, name: 'Jihan'), 33 | 'Abori': CategoryInfo(id: 196, name: 'Abori'), 34 | }; 35 | 36 | /// Category information 37 | class CategoryInfo { 38 | final int id; 39 | final String name; 40 | 41 | const CategoryInfo({required this.id, required this.name}); 42 | } 43 | 44 | //Show how many per_page ? 45 | final String perPage = "10"; 46 | 47 | //Categories ids 48 | final String grngId = "176"; 49 | final String grngName = "176"; 50 | 51 | final String jihanId = "195"; 52 | final String jihanName = "195"; 53 | 54 | final String iraqId = "6098"; 55 | final String iraqName = "6098"; 56 | 57 | final String kurdistanId = "188"; 58 | final String kurdistanName = "188"; 59 | 60 | final String aboriId = "196"; 61 | final String aboriName = "196"; 62 | 63 | final String healthId = "208"; 64 | final String healthName = "208"; 65 | 66 | final String techId = "9875"; 67 | final String techName = "9875"; 68 | 69 | final String cultureId = "207"; 70 | final String cultureName = "207"; 71 | 72 | final String womanId = "9102"; 73 | final String womanName = "9102"; 74 | 75 | final String mainPageId = ""; 76 | final String mainApiUrl = "$baseUrl/wp-json"; 77 | final String connectionProblemError = ' Internet Connection Problem '; 78 | 79 | /// API endpoints 80 | const String postsEndpoint = 'posts'; 81 | const String categoriesEndpoint = 'categories'; 82 | const String mediaEndpoint = 'media'; 83 | 84 | /// Query parameters 85 | const String embedParam = '_embed=true'; 86 | const String perPageParam = 'per_page=10'; 87 | const String statusParam = 'status=publish'; 88 | const String orderByDateDesc = 'orderby=date&order=desc'; 89 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: hawalnir1 2 | description: Flutter Wordpress Client. 3 | 4 | # The following defines the version and build number for your application. 5 | # A version number is three numbers separated by dots, like 1.2.43 6 | # followed by an optional build number separated by a +. 7 | # Both the version and the builder number may be overridden in flutter 8 | # build by specifying --build-name and --build-number, respectively. 9 | # Read more about versioning at semver.org. 10 | version: 1.0.0+1 11 | 12 | environment: 13 | sdk: '>=3.2.0 <4.0.0' 14 | 15 | dependencies: 16 | flutter: 17 | sdk: flutter 18 | flutter_localizations: 19 | sdk: flutter 20 | 21 | # State Management 22 | provider: ^6.1.1 23 | 24 | # Network & Caching 25 | http: ^1.2.0 26 | dio: ^5.4.0 27 | pretty_dio_logger: ^1.3.1 28 | cached_network_image: ^3.3.1 29 | connectivity_plus: ^5.0.2 30 | 31 | # Storage 32 | shared_preferences: ^2.2.2 33 | sqflite: ^2.3.2 34 | path_provider: ^2.1.2 35 | 36 | # UI Components 37 | flutter_html: ^3.0.0-beta.2 38 | shimmer: ^3.0.0 39 | pull_to_refresh: ^2.0.0 40 | infinite_scroll_pagination: ^4.1.0 41 | 42 | # Utils 43 | intl: ^0.19.0 44 | logger: ^2.0.2+1 45 | cupertino_icons: ^1.0.6 46 | google_fonts: ^6.2.1 47 | timeago: ^3.7.0 48 | 49 | dev_dependencies: 50 | flutter_test: 51 | sdk: flutter 52 | flutter_lints: ^5.0.0 53 | json_serializable: ^6.7.1 54 | build_runner: ^2.4.8 55 | 56 | # For information on the generic Dart part of this file, see the 57 | # following page: https://www.dartlang.org/tools/pub/pubspec 58 | 59 | # The following section is specific to Flutter. 60 | flutter: 61 | 62 | # The following line ensures that the Material Icons font is 63 | # included with your application, so that you can use the icons in 64 | # the material Icons class. 65 | uses-material-design: true 66 | 67 | # To add assets to your application, add an assets section, like this: 68 | assets: 69 | # - images/a_dot_burr.jpeg 70 | - assets/images/placeholder.png 71 | 72 | # An image asset can refer to one or more resolution-specific "variants", see 73 | # https://flutter.io/assets-and-images/#resolution-aware. 74 | 75 | # For details regarding adding assets from package dependencies, see 76 | # https://flutter.io/assets-and-images/#from-packages 77 | 78 | # To add custom fonts to your application, add a fonts section here, 79 | # in this "flutter" section. Each entry in this list should have a 80 | # "family" key with the font family name, and a "fonts" key with a 81 | # list giving the asset and other descriptors for the font. For 82 | # example: 83 | fonts: 84 | - family: NotoKufiArabic 85 | fonts: 86 | - asset: lib/src/fonts/NotoKufiArabic-Regular.ttf 87 | - family: NotoSansArabic 88 | fonts: 89 | - asset: lib/src/fonts/NotoSansArabic-Regular.ttf 90 | weight: 700 91 | # 92 | # For details regarding fonts from package dependencies, 93 | # see https://flutter.io/custom-fonts/#from-packages 94 | -------------------------------------------------------------------------------- /lib/src/db/functions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'dart:async'; 3 | import 'dart:io'; 4 | import 'package:shared_preferences/shared_preferences.dart'; 5 | 6 | import '../config.dart'; 7 | import '../client.dart'; 8 | import '../models/post.dart'; 9 | import 'database_helper.dart'; 10 | 11 | class DatabaseFunctions { 12 | final WordPressClient client; 13 | final DatabaseHelper dbHelper; 14 | final SharedPreferences prefs; 15 | 16 | DatabaseFunctions({ 17 | required this.client, 18 | required this.dbHelper, 19 | required this.prefs, 20 | }); 21 | 22 | List? _cachedPosts; 23 | List? _posts; 24 | bool _hasNetworkConnection = false; 25 | 26 | Future checkNetworkConnection() async { 27 | try { 28 | final result = await InternetAddress.lookup('google.com'); 29 | _hasNetworkConnection = 30 | result.isNotEmpty && result[0].rawAddress.isNotEmpty; 31 | return _hasNetworkConnection; 32 | } on SocketException catch (_) { 33 | _hasNetworkConnection = false; 34 | return false; 35 | } 36 | } 37 | 38 | Future clearDatabase() async { 39 | final count = await dbHelper.getCount(); 40 | debugPrint('Clearing database with $count items'); 41 | 42 | if (_cachedPosts != null) { 43 | for (final post in _cachedPosts!) { 44 | await dbHelper.deletePost(post.id); 45 | debugPrint('Deleted post ${post.id} from database'); 46 | } 47 | } 48 | } 49 | 50 | Future fillDatabase(List posts) async { 51 | for (final post in posts) { 52 | await dbHelper.insertPost(post); 53 | debugPrint('Inserted post ${post.id} to database'); 54 | } 55 | } 56 | 57 | Future> getPosts() async { 58 | final hasNetwork = await checkNetworkConnection(); 59 | _cachedPosts = await dbHelper.getPostList(); 60 | 61 | if (!hasNetwork) { 62 | debugPrint('No network connection, using cached posts'); 63 | if (_cachedPosts?.isEmpty ?? true) { 64 | throw Exception('No cached posts available and no network connection'); 65 | } 66 | _posts = _cachedPosts; 67 | } else { 68 | debugPrint('Network available, fetching fresh posts'); 69 | final response = await client.getPosts(perPage: defaultPerPage); 70 | _posts = response.items; 71 | 72 | // Update cache if new posts are available 73 | if (!_arePostsEqual(_posts!, _cachedPosts ?? [])) { 74 | await clearDatabase(); 75 | await fillDatabase(_posts!); 76 | debugPrint('Updated cache with new posts'); 77 | } 78 | } 79 | 80 | // Sort posts by ID in descending order 81 | _posts?.sort((a, b) => b.id.compareTo(a.id)); 82 | return _posts ?? []; 83 | } 84 | 85 | bool _arePostsEqual(List newPosts, List cachedPosts) { 86 | if (newPosts.length != cachedPosts.length) return false; 87 | 88 | for (var i = 0; i < newPosts.length; i++) { 89 | if (newPosts[i].id != cachedPosts[i].id) return false; 90 | } 91 | 92 | return true; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/src/widgets/eachPost.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_html/flutter_html.dart'; 3 | import '../models/post.dart'; 4 | import 'package:hawalnir1/wordpress_client.dart'; 5 | 6 | import 'catWidgets.dart'; 7 | import 'hawalnir-date-convertor.dart'; 8 | 9 | class HawalnirPost extends StatelessWidget { 10 | HawalnirPost({Key? key, required var this.post}) : super(key: key); 11 | final Post? post; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | debugPrint(post!.id.toString()); 16 | 17 | return new Scaffold( 18 | appBar: new AppBar( 19 | elevation: 0, 20 | title: Container( 21 | width: double.infinity, 22 | child: hawalTitle(post!), 23 | ), 24 | backgroundColor: Colors.transparent, 25 | ), 26 | body: new Padding( 27 | padding: EdgeInsets.all(16.0), 28 | child: new ListView( 29 | children: [ 30 | Column( 31 | children: [ 32 | Stack( 33 | children: [ 34 | Hero(tag: 'hero${post!.id}', child: hawalImage(post!)), 35 | Positioned( 36 | bottom: 0.0, 37 | left: 0.0, 38 | child: hawalBtnBar(), 39 | ), 40 | ], 41 | ), 42 | hawalTitle(post!), 43 | Row( 44 | children: [ 45 | Expanded( 46 | child: hawalAuthor(post!), 47 | ), 48 | Expanded( 49 | child: hawalDate(post!), 50 | ), 51 | ], 52 | ), 53 | Divider(), 54 | contentRendered(post!), 55 | ], 56 | ), 57 | ], 58 | )), 59 | ); 60 | } 61 | 62 | Widget mainImage(Post post) { 63 | return FadeInImage.assetNetwork( 64 | placeholder: 'https://via.placeholder.com/300.png/09f/fff', 65 | image: post.featuredMediaUrl?.toString() ?? 'src/images/placeholder.png', 66 | ); 67 | } 68 | 69 | Widget titleRendered(Post post) { 70 | return Html( 71 | data: post.title.rendered, 72 | style: { 73 | "div": Style( 74 | fontSize: FontSize(20), 75 | ), 76 | }, 77 | ); 78 | } 79 | 80 | Widget contentRendered(Post post) { 81 | return Html( 82 | data: post.content.rendered, 83 | style: { 84 | "div": Style( 85 | fontSize: FontSize(20), 86 | ), 87 | }, 88 | ); 89 | } 90 | 91 | Widget authorEmbedded(Post post) { 92 | return Text( 93 | "author: " + post.author!, 94 | textAlign: TextAlign.right, 95 | ); 96 | } 97 | 98 | Widget dateMain(Post post) { 99 | return Text( 100 | dateConvertor(post.date.toString()), 101 | textAlign: TextAlign.left, 102 | ); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/src/providers/posts_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; 3 | 4 | import '../client.dart'; 5 | import '../models/post.dart'; 6 | import 'settings_provider.dart'; 7 | 8 | class PostsProvider with ChangeNotifier { 9 | final WordPressClient _client; 10 | final SettingsProvider _settings; 11 | late final PagingController pagingController; 12 | 13 | bool _isLoading = false; 14 | String? _error; 15 | final Set _loadedPostIds = {}; 16 | int? _selectedCategoryId; 17 | 18 | bool get isLoading => _isLoading; 19 | String? get error => _error; 20 | int? get selectedCategoryId => _selectedCategoryId; 21 | 22 | PostsProvider({ 23 | required WordPressClient client, 24 | required SettingsProvider settings, 25 | }) : _client = client, 26 | _settings = settings { 27 | pagingController = PagingController(firstPageKey: 1) 28 | ..addPageRequestListener((pageKey) { 29 | // Use Future.microtask to avoid state changes during build 30 | Future.microtask(() => _fetchPage(pageKey)); 31 | }); 32 | } 33 | 34 | Future _fetchPage(int pageKey) async { 35 | if (_isLoading) return; 36 | 37 | try { 38 | _isLoading = true; 39 | // Don't notify here to avoid build-time changes 40 | 41 | final filters = PostFilters( 42 | categoryIds: 43 | _selectedCategoryId != null ? [_selectedCategoryId!] : null, 44 | orderBy: PostOrdering.date, 45 | order: OrderDirection.desc, 46 | embed: true, 47 | ); 48 | 49 | final response = await _client.getPosts( 50 | filters: filters, 51 | page: pageKey, 52 | perPage: _settings.postsPerPage, 53 | ); 54 | 55 | // Process posts and filter duplicates 56 | final newPosts = response.items.where((post) { 57 | if (_loadedPostIds.contains(post.id)) return false; 58 | _loadedPostIds.add(post.id); 59 | return true; 60 | }).toList(); 61 | 62 | final isLastPage = newPosts.length < _settings.postsPerPage; 63 | 64 | // Update error state before modifying the controller 65 | _error = null; 66 | 67 | if (!pagingController.hasListeners) return; 68 | 69 | if (isLastPage || newPosts.isEmpty) { 70 | pagingController.appendLastPage(newPosts); 71 | } else { 72 | pagingController.appendPage(newPosts, pageKey + 1); 73 | } 74 | } catch (e) { 75 | _error = e.toString(); 76 | if (pagingController.hasListeners) { 77 | pagingController.error = e; 78 | } 79 | } finally { 80 | _isLoading = false; 81 | // Notify after all state changes are complete 82 | notifyListeners(); 83 | } 84 | } 85 | 86 | void filterByCategory(int? categoryId) { 87 | _selectedCategoryId = categoryId; 88 | refreshPosts(); 89 | } 90 | 91 | Future refreshPosts() async { 92 | _loadedPostIds.clear(); 93 | _error = null; 94 | _isLoading = false; 95 | notifyListeners(); 96 | 97 | if (pagingController.hasListeners) { 98 | pagingController.refresh(); 99 | } 100 | } 101 | 102 | @override 103 | void dispose() { 104 | pagingController.dispose(); 105 | super.dispose(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /lib/src/theme/app_theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:google_fonts/google_fonts.dart'; 3 | 4 | // Custom theme extension for brand colors 5 | @immutable 6 | class BrandColors extends ThemeExtension { 7 | const BrandColors({ 8 | required this.primary, 9 | required this.secondary, 10 | required this.tertiary, 11 | required this.surface, 12 | }); 13 | 14 | final Color primary; 15 | final Color secondary; 16 | final Color tertiary; 17 | final Color surface; 18 | 19 | @override 20 | BrandColors copyWith({ 21 | Color? primary, 22 | Color? secondary, 23 | Color? tertiary, 24 | Color? surface, 25 | }) { 26 | return BrandColors( 27 | primary: primary ?? this.primary, 28 | secondary: secondary ?? this.secondary, 29 | tertiary: tertiary ?? this.tertiary, 30 | surface: surface ?? this.surface, 31 | ); 32 | } 33 | 34 | @override 35 | BrandColors lerp(ThemeExtension? other, double t) { 36 | if (other is! BrandColors) return this; 37 | return BrandColors( 38 | primary: Color.lerp(primary, other.primary, t)!, 39 | secondary: Color.lerp(secondary, other.secondary, t)!, 40 | tertiary: Color.lerp(tertiary, other.tertiary, t)!, 41 | surface: Color.lerp(surface, other.surface, t)!, 42 | ); 43 | } 44 | } 45 | 46 | class AppTheme { 47 | static const _primaryLight = Color(0xFF6750A4); 48 | static const _secondaryLight = Color(0xFF625B71); 49 | static const _tertiaryLight = Color(0xFF7D5260); 50 | static const _surfaceLight = Color(0xFFFFFBFE); 51 | 52 | static const _primaryDark = Color(0xFFD0BCFF); 53 | static const _secondaryDark = Color(0xFFCCC2DC); 54 | static const _tertiaryDark = Color(0xFFEFB8C8); 55 | static const _surfaceDark = Color(0xFF1C1B1F); 56 | 57 | static final lightTheme = ThemeData( 58 | useMaterial3: true, 59 | colorScheme: ColorScheme.light( 60 | primary: _primaryLight, 61 | secondary: _secondaryLight, 62 | tertiary: _tertiaryLight, 63 | surface: _surfaceLight, 64 | background: const Color(0xFFFFFBFE), 65 | ), 66 | textTheme: GoogleFonts.interTextTheme(), 67 | cardTheme: CardTheme( 68 | elevation: 2, 69 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), 70 | clipBehavior: Clip.antiAlias, 71 | ), 72 | appBarTheme: const AppBarTheme( 73 | elevation: 0, 74 | centerTitle: true, 75 | backgroundColor: Colors.transparent, 76 | foregroundColor: _primaryLight, 77 | ), 78 | extensions: const [ 79 | BrandColors( 80 | primary: _primaryLight, 81 | secondary: _secondaryLight, 82 | tertiary: _tertiaryLight, 83 | surface: _surfaceLight, 84 | ), 85 | ], 86 | ); 87 | 88 | static final darkTheme = ThemeData( 89 | useMaterial3: true, 90 | colorScheme: ColorScheme.dark( 91 | primary: _primaryDark, 92 | secondary: _secondaryDark, 93 | tertiary: _tertiaryDark, 94 | surface: _surfaceDark, 95 | background: const Color(0xFF1C1B1F), 96 | ), 97 | textTheme: GoogleFonts.interTextTheme(ThemeData.dark().textTheme), 98 | cardTheme: CardTheme( 99 | elevation: 2, 100 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), 101 | clipBehavior: Clip.antiAlias, 102 | ), 103 | appBarTheme: const AppBarTheme( 104 | elevation: 0, 105 | centerTitle: true, 106 | backgroundColor: Colors.transparent, 107 | foregroundColor: _primaryDark, 108 | ), 109 | extensions: const [ 110 | BrandColors( 111 | primary: _primaryDark, 112 | secondary: _secondaryDark, 113 | tertiary: _tertiaryDark, 114 | surface: _surfaceDark, 115 | ), 116 | ], 117 | ); 118 | } 119 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /lib/src/db/database_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:sqflite/sqflite.dart'; 2 | import 'dart:async'; 3 | import 'dart:io'; 4 | import 'package:path_provider/path_provider.dart'; 5 | import '../models/post.dart'; 6 | 7 | class DatabaseHelper { 8 | static DatabaseHelper? _databaseHelper; // Singleton DB helper 9 | static Database? _database; 10 | 11 | String postTable = 'post_table'; 12 | String colId = 'id'; 13 | String colTitle = 'title'; 14 | String colContent = 'content'; 15 | String colAuthor = 'author'; 16 | String colDate = 'date'; 17 | String colImgUrl = 'featured_media'; 18 | 19 | DatabaseHelper._createInstance(); 20 | 21 | factory DatabaseHelper() { 22 | if (_databaseHelper == null) { 23 | _databaseHelper = DatabaseHelper._createInstance(); 24 | } 25 | return _databaseHelper!; 26 | } 27 | 28 | Future get database async { 29 | if (_database == null) { 30 | _database = await initDatabase(); 31 | } 32 | return _database; 33 | } 34 | 35 | Future initDatabase() async { 36 | //Get the dir 37 | Directory directory = await getApplicationDocumentsDirectory(); 38 | String path = directory.path + 'posts.db'; 39 | 40 | //Open or Create the database using given path 41 | var postsDataBase = openDatabase(path, version: 1, onCreate: _createDb); 42 | return postsDataBase; 43 | } 44 | 45 | void _createDb(Database db, int newVersion) async { 46 | await db.execute( 47 | 'CREATE TABLE $postTable($colId INTEGER PRIMARY KEY, $colTitle TEXT, $colContent TEXT, $colAuthor TEXT, $colDate TEXT, $colImgUrl TEXT )'); 48 | } 49 | 50 | //Fetch : Get all Posts from DB 51 | Future>>getPostMapList() async { 52 | Database db = await (this.database as FutureOr); 53 | 54 | // var result = await db.rawQuery('SELECT * FROM $postTable'); 55 | var result = await db.query(postTable); 56 | 57 | //print(result) ; 58 | return result; 59 | } 60 | 61 | 62 | //Insert: Insert a Post Obj to database 63 | Future insertPost(Post post) async { 64 | Database db = await (this.database as FutureOr) ; 65 | var result = await db.insert(postTable, post.toMap()); //Convert ti map 66 | return result ; 67 | } 68 | 69 | // Update : Update Post Obj and Save it yo DB 70 | Future updatePost(Post post) async { 71 | Database db = await (this.database as FutureOr) ; 72 | var result = await db.update(postTable, post.toMap(), where: '$colId = ?', whereArgs: [post.id] ); //Convert ti map then uodate where id /? 73 | return result ; 74 | } 75 | 76 | //Delete: Delete a Post Obj from DB 77 | Future deletePost(int? id) async { 78 | Database db = await (this.database as FutureOr) ; 79 | var result = await db.rawDelete('DELETE FROM $postTable WHERE $colId = $id '); //Convert ti map then uodate where id /? 80 | return result ; 81 | } 82 | 83 | 84 | //delete DB : Delete entire DB 85 | Future deleteDB() async { 86 | print('database has been deleted') ; 87 | Database db = await (this.database as FutureOr) ; 88 | db.rawQuery('DELETE FROM $postTable'); 89 | return 1 ; 90 | } 91 | 92 | //Get number of Posts in database 93 | Future getCount() async { 94 | Database db = await (this.database as FutureOr) ; 95 | List> x = await db.rawQuery('SELECT COUNT (*) from $postTable'); 96 | int? result = Sqflite.firstIntValue(x); 97 | return result ; 98 | } 99 | 100 | 101 | 102 | 103 | 104 | 105 | Future> getPostList() async { 106 | List postMaps = await getPostMapList() ; 107 | int count = postMaps.length; 108 | 109 | List postsList = []; 110 | 111 | //posts = postMaps.map((postMap) => new Post.fromMap(postMap)).toList(); 112 | for (int i = 0; i < count; i++) { 113 | postsList.add(Post.fromMapObject(postMaps[i] as Map)); 114 | } 115 | //print(postsList.toString()); 116 | return postsList; //from here Code has been cloned to Projects 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /lib/src/widgets/loading_placeholders.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'shimmer_loading.dart'; 3 | 4 | class CategoryLoadingPlaceholder extends StatelessWidget { 5 | const CategoryLoadingPlaceholder({Key? key}) : super(key: key); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | return ShimmerLoading( 10 | isLoading: true, 11 | child: Container( 12 | height: 32, 13 | width: 100, 14 | decoration: BoxDecoration( 15 | color: Colors.white, 16 | borderRadius: BorderRadius.circular(16), 17 | ), 18 | ), 19 | ); 20 | } 21 | } 22 | 23 | class PostCardLoadingPlaceholder extends StatelessWidget { 24 | const PostCardLoadingPlaceholder({Key? key}) : super(key: key); 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | return Card( 29 | clipBehavior: Clip.antiAlias, 30 | child: ShimmerLoading( 31 | isLoading: true, 32 | child: Column( 33 | crossAxisAlignment: CrossAxisAlignment.start, 34 | children: [ 35 | // Featured image placeholder 36 | Container( 37 | height: 200, 38 | color: Colors.white, 39 | ), 40 | Padding( 41 | padding: const EdgeInsets.all(16), 42 | child: Column( 43 | crossAxisAlignment: CrossAxisAlignment.start, 44 | children: [ 45 | // Categories placeholder 46 | Row( 47 | children: List.generate( 48 | 2, 49 | (index) => Container( 50 | margin: const EdgeInsets.only(right: 8), 51 | height: 32, 52 | width: 80, 53 | decoration: BoxDecoration( 54 | color: Colors.white, 55 | borderRadius: BorderRadius.circular(16), 56 | ), 57 | ), 58 | ), 59 | ), 60 | const SizedBox(height: 16), 61 | // Title placeholder 62 | Container( 63 | height: 24, 64 | width: double.infinity, 65 | decoration: BoxDecoration( 66 | color: Colors.white, 67 | borderRadius: BorderRadius.circular(4), 68 | ), 69 | ), 70 | const SizedBox(height: 8), 71 | // Excerpt placeholder 72 | Column( 73 | children: List.generate( 74 | 3, 75 | (index) => Container( 76 | margin: const EdgeInsets.only(bottom: 8), 77 | height: 16, 78 | width: double.infinity, 79 | decoration: BoxDecoration( 80 | color: Colors.white, 81 | borderRadius: BorderRadius.circular(4), 82 | ), 83 | ), 84 | ), 85 | ), 86 | const SizedBox(height: 16), 87 | // Metadata placeholder 88 | Row( 89 | children: [ 90 | Container( 91 | height: 16, 92 | width: 100, 93 | decoration: BoxDecoration( 94 | color: Colors.white, 95 | borderRadius: BorderRadius.circular(4), 96 | ), 97 | ), 98 | const Spacer(), 99 | Container( 100 | height: 36, 101 | width: 100, 102 | decoration: BoxDecoration( 103 | color: Colors.white, 104 | borderRadius: BorderRadius.circular(18), 105 | ), 106 | ), 107 | ], 108 | ), 109 | ], 110 | ), 111 | ), 112 | ], 113 | ), 114 | ), 115 | ); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /.flutter-plugins-dependencies: -------------------------------------------------------------------------------- 1 | {"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"connectivity_plus","path":"/Users/hooshyar/.pub-cache/hosted/pub.dev/connectivity_plus-5.0.2/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/hooshyar/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"shared_preferences_foundation","path":"/Users/hooshyar/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"sqflite_darwin","path":"/Users/hooshyar/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"android":[{"name":"connectivity_plus","path":"/Users/hooshyar/.pub-cache/hosted/pub.dev/connectivity_plus-5.0.2/","native_build":true,"dependencies":[]},{"name":"path_provider_android","path":"/Users/hooshyar/.pub-cache/hosted/pub.dev/path_provider_android-2.2.15/","native_build":true,"dependencies":[]},{"name":"shared_preferences_android","path":"/Users/hooshyar/.pub-cache/hosted/pub.dev/shared_preferences_android-2.4.3/","native_build":true,"dependencies":[]},{"name":"sqflite_android","path":"/Users/hooshyar/.pub-cache/hosted/pub.dev/sqflite_android-2.4.0/","native_build":true,"dependencies":[]}],"macos":[{"name":"connectivity_plus","path":"/Users/hooshyar/.pub-cache/hosted/pub.dev/connectivity_plus-5.0.2/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/hooshyar/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"shared_preferences_foundation","path":"/Users/hooshyar/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"sqflite_darwin","path":"/Users/hooshyar/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"linux":[{"name":"connectivity_plus","path":"/Users/hooshyar/.pub-cache/hosted/pub.dev/connectivity_plus-5.0.2/","native_build":false,"dependencies":[]},{"name":"path_provider_linux","path":"/Users/hooshyar/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[]},{"name":"shared_preferences_linux","path":"/Users/hooshyar/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/","native_build":false,"dependencies":["path_provider_linux"]}],"windows":[{"name":"connectivity_plus","path":"/Users/hooshyar/.pub-cache/hosted/pub.dev/connectivity_plus-5.0.2/","native_build":true,"dependencies":[]},{"name":"path_provider_windows","path":"/Users/hooshyar/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[]},{"name":"shared_preferences_windows","path":"/Users/hooshyar/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1/","native_build":false,"dependencies":["path_provider_windows"]}],"web":[{"name":"connectivity_plus","path":"/Users/hooshyar/.pub-cache/hosted/pub.dev/connectivity_plus-5.0.2/","dependencies":[]},{"name":"shared_preferences_web","path":"/Users/hooshyar/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.2/","dependencies":[]}]},"dependencyGraph":[{"name":"connectivity_plus","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]},{"name":"sqflite","dependencies":["sqflite_android","sqflite_darwin"]},{"name":"sqflite_android","dependencies":[]},{"name":"sqflite_darwin","dependencies":[]}],"date_created":"2025-01-25 20:01:00.535585","version":"3.27.3","swift_package_manager_enabled":false} -------------------------------------------------------------------------------- /lib/src/widgets/animated_fab.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as math; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | class AnimatedFab extends StatefulWidget { 6 | final VoidCallback? onClick; 7 | 8 | const AnimatedFab({Key? key, this.onClick}) : super(key: key); 9 | 10 | @override 11 | _AnimatedFabState createState() => new _AnimatedFabState(); 12 | } 13 | 14 | class _AnimatedFabState extends State 15 | with SingleTickerProviderStateMixin { 16 | late AnimationController _animationController; 17 | late Animation _colorAnimation; 18 | 19 | final double expandedSize = 180.0; 20 | final double hiddenSize = 20.0; 21 | 22 | @override 23 | void initState() { 24 | super.initState(); 25 | _animationController = new AnimationController( 26 | vsync: this, duration: Duration(milliseconds: 200)); 27 | _colorAnimation = new ColorTween(begin: Colors.pink, end: Colors.pink[800]) 28 | .animate(_animationController); 29 | } 30 | 31 | @override 32 | void dispose() { 33 | _animationController.dispose(); 34 | super.dispose(); 35 | } 36 | 37 | @override 38 | Widget build(BuildContext context) { 39 | return new SizedBox( 40 | width: expandedSize, 41 | height: expandedSize, 42 | child: new AnimatedBuilder( 43 | animation: _animationController, 44 | builder: (BuildContext context, Widget? child) { 45 | return new Stack( 46 | alignment: Alignment.center, 47 | children: [ 48 | _buildExpandedBackground(), 49 | _buildOption(Icons.check_circle, 0.0), 50 | _buildOption(Icons.flash_on, -math.pi / 3), 51 | _buildOption(Icons.access_time, -2 * math.pi / 3), 52 | _buildOption(Icons.error_outline, math.pi), 53 | _buildFabCore(), 54 | ], 55 | ); 56 | }, 57 | ), 58 | ); 59 | } 60 | 61 | Widget _buildOption(IconData icon, double angle) { 62 | if (_animationController.isDismissed) { 63 | return Container(); 64 | } 65 | double iconSize = 0.0; 66 | if (_animationController.value > 0.8) { 67 | iconSize = 26.0 * (_animationController.value - 0.8) * 5; 68 | } 69 | return new Transform.rotate( 70 | angle: angle, 71 | child: new Align( 72 | alignment: Alignment.topCenter, 73 | child: new Padding( 74 | padding: new EdgeInsets.only(top: 8.0), 75 | child: new IconButton( 76 | onPressed: _onIconClick, 77 | icon: new Transform.rotate( 78 | angle: -angle, 79 | child: new Icon( 80 | icon, 81 | color: Colors.white, 82 | ), 83 | ), 84 | iconSize: iconSize, 85 | alignment: Alignment.center, 86 | padding: new EdgeInsets.all(0.0), 87 | ), 88 | ), 89 | ), 90 | ); 91 | } 92 | 93 | Widget _buildExpandedBackground() { 94 | double size = 95 | hiddenSize + (expandedSize - hiddenSize) * _animationController.value; 96 | return new Container( 97 | height: size, 98 | width: size, 99 | decoration: new BoxDecoration(shape: BoxShape.circle, color: Colors.pink), 100 | ); 101 | } 102 | 103 | Widget _buildFabCore() { 104 | double scaleFactor = 2 * (_animationController.value - 0.5).abs(); 105 | return new FloatingActionButton( 106 | onPressed: _onFabTap, 107 | child: new Transform( 108 | alignment: Alignment.center, 109 | transform: new Matrix4.identity()..scale(1.0, scaleFactor), 110 | child: new Icon( 111 | _animationController.value > 0.5 ? Icons.close : Icons.filter_list, 112 | color: Colors.white, 113 | size: 26.0, 114 | ), 115 | ), 116 | backgroundColor: _colorAnimation.value, 117 | ); 118 | } 119 | 120 | open() { 121 | if (_animationController.isDismissed) { 122 | _animationController.forward(); 123 | } 124 | } 125 | 126 | close() { 127 | if (_animationController.isCompleted) { 128 | _animationController.reverse(); 129 | } 130 | } 131 | 132 | _onFabTap() { 133 | if (_animationController.isDismissed) { 134 | open(); 135 | } else { 136 | close(); 137 | } 138 | } 139 | 140 | _onIconClick() { 141 | widget.onClick!(); 142 | close(); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /lib/src/widgets/catWidgets.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_html/flutter_html.dart'; 4 | import 'package:hawalnir1/src/widgets/posts_card.dart'; 5 | 6 | import '../../wordpress_client.dart'; 7 | import '../config.dart'; 8 | import 'hawalnir-date-convertor.dart'; 9 | import '../models/post.dart'; 10 | import '../models/category.dart'; 11 | 12 | Widget hawalImage(Post post) { 13 | return Stack( 14 | children: [ 15 | Positioned( 16 | bottom: 5.0, 17 | right: 0, 18 | left: 0, 19 | child: Container( 20 | decoration: BoxDecoration( 21 | boxShadow: [ 22 | BoxShadow( 23 | spreadRadius: 10, 24 | blurRadius: 20, 25 | color: Colors.black, 26 | offset: const Offset(1.0, 1.0), 27 | ), 28 | ], 29 | ), 30 | ), 31 | ), 32 | Container( 33 | foregroundDecoration: BoxDecoration( 34 | backgroundBlendMode: BlendMode.overlay, 35 | gradient: LinearGradient( 36 | begin: Alignment.topCenter, 37 | end: Alignment.bottomCenter, 38 | stops: const [0.1, 0.5, 0.7, 0.9], 39 | colors: [ 40 | Colors.transparent, 41 | Colors.transparent, 42 | Colors.black45, 43 | Colors.black87, 44 | ], 45 | )), 46 | child: ClipRRect( 47 | borderRadius: const BorderRadius.all(Radius.circular(10.0)), 48 | child: CachedNetworkImage( 49 | fadeInCurve: Curves.decelerate, 50 | repeat: ImageRepeat.noRepeat, 51 | fadeInDuration: const Duration(milliseconds: 500), 52 | imageUrl: post.featuredMediaUrl?.toString() ?? 53 | 'https://via.placeholder.com/300x150.png', 54 | placeholder: (context, url) => 55 | Image.asset('assets/images/placeholder.png'), 56 | errorWidget: (context, url, error) => const Icon(Icons.error), 57 | ), 58 | ), 59 | ), 60 | ], 61 | ); 62 | } 63 | 64 | Widget hawalTitle(Post post) { 65 | return Container( 66 | constraints: const BoxConstraints(maxWidth: 800), 67 | child: Text(post.title.rendered, 68 | style: const TextStyle( 69 | fontSize: 20, 70 | )), 71 | ); 72 | } 73 | 74 | Widget hawalAuthor(Post post) { 75 | return Text( 76 | "author: ${post.author ?? 'Unknown'}", 77 | textAlign: TextAlign.right, 78 | ); 79 | } 80 | 81 | Widget hawalDate(Post post) { 82 | return Text( 83 | dateConvertor(post.date?.toString() ?? ''), 84 | textAlign: TextAlign.left, 85 | ); 86 | } 87 | 88 | Widget hawalBtnBar() { 89 | return OverflowBar( 90 | children: [ 91 | IconButton( 92 | icon: const Icon(Icons.save), 93 | splashColor: Colors.blueAccent[200], 94 | color: Colors.blueGrey, 95 | tooltip: 'save', 96 | onPressed: () { 97 | debugPrint("save button tapped"); 98 | }, 99 | ), 100 | IconButton( 101 | icon: const Icon(Icons.favorite), 102 | splashColor: Colors.redAccent, 103 | color: Colors.blueGrey, 104 | tooltip: 'like', 105 | onPressed: () { 106 | debugPrint("favorite button tapped"); 107 | }, 108 | ), 109 | IconButton( 110 | icon: const Icon(Icons.share), 111 | color: Colors.blueGrey, 112 | tooltip: 'share', 113 | onPressed: () { 114 | debugPrint("share button tapped"); 115 | }, 116 | ), 117 | ], 118 | ); 119 | } 120 | 121 | Widget connectionErrorBar() { 122 | return Container( 123 | alignment: Alignment.bottomCenter, 124 | child: SnackBar( 125 | duration: const Duration(milliseconds: 200), 126 | content: Text(connectionError), 127 | ), 128 | ); 129 | } 130 | 131 | enum WhyFarther { harder, smarter, selfStarter, tradingCharter } 132 | 133 | Widget sliverListGlobal(List posts) { 134 | debugPrint('SliverListGlobal received ${posts.length}'); 135 | return SliverList( 136 | delegate: SliverChildBuilderDelegate( 137 | (BuildContext context, index) { 138 | return PostsCard( 139 | post: posts[index], 140 | ); 141 | }, 142 | childCount: posts.length, 143 | addAutomaticKeepAlives: true, 144 | ), 145 | ); 146 | } 147 | 148 | Widget nameRendered(Category category) { 149 | return Container( 150 | constraints: const BoxConstraints(maxWidth: 800), 151 | child: Html( 152 | data: category.name ?? '', 153 | style: { 154 | "*": Style( 155 | margin: Margins.zero, 156 | padding: HtmlPaddings.zero, 157 | fontSize: FontSize(20), 158 | textAlign: TextAlign.start, 159 | width: Width(100, Unit.percent), 160 | ), 161 | }, 162 | shrinkWrap: true, 163 | ), 164 | ); 165 | } 166 | -------------------------------------------------------------------------------- /lib/src/models/media.dart: -------------------------------------------------------------------------------- 1 | class Media { 2 | /// The date the object was published, in the site's timezone. 3 | DateTime? date; 4 | 5 | /// The date the object was published, as GMT. 6 | DateTime? dateGMT; 7 | 8 | /// The globally unique identifier for the object. 9 | Map? guid; 10 | 11 | /// Unique identifier for the object. 12 | int? id; 13 | 14 | /// URL to the object. 15 | String? link; 16 | 17 | /// The date the object was last modified, in the site's timezone. 18 | DateTime? modified; 19 | 20 | /// The date the object was last modified, as GMT. 21 | DateTime? modifiedGMT; 22 | 23 | /// An alphanumeric identifier for the object unique to its type. 24 | String? slug; 25 | 26 | /// A named status for the object. 27 | /// 28 | /// One of: publish, future, draft, pending, private 29 | String? status; 30 | 31 | /// Type of Post for the object. 32 | String? type; 33 | 34 | /// The title for the object. 35 | Map? title; 36 | 37 | /// The ID for the author of the object. 38 | int? author; 39 | 40 | /// Whether or not comments are open on the object. 41 | /// 42 | /// One of: open, closed 43 | String? commentStatus; 44 | 45 | /// Whether or not the object can be pinged. 46 | /// 47 | /// One of: open, closed 48 | String? pingStatus; 49 | 50 | /// Meta fields. 51 | dynamic meta; 52 | 53 | /// The theme file to use to display the object. 54 | String? template; 55 | 56 | /// Alternative text to display when attachment is not displayed. 57 | String? altText; 58 | 59 | /// The attachment caption. 60 | Map? caption; 61 | 62 | /// The attachment description. 63 | Map? description; 64 | 65 | /// Attachment type. 66 | /// 67 | /// One of: image, file 68 | String? mediaType; 69 | 70 | /// The attachment MIME type. 71 | String? mimeType; 72 | 73 | /// Details about the media file, specific to its type. 74 | Map? mediaDetails; 75 | 76 | /// The ID for the associated post of the attachment. 77 | int? post; 78 | 79 | /// URL to the original attachment file. 80 | String? sourceURL; 81 | 82 | /// Convenience method to retrieve thumbnail URL 83 | String? get featuredMediaURLThumbnail => _featuredMediaURLThumbnail(); 84 | 85 | /// Convenience method to retrieve medium URL 86 | String? get featuredMediaURLMedium => _featuredMediaURLMedium(); 87 | 88 | /// Convenience method to retrieve large URL 89 | String? get featuredMediaURLLarge => _featuredMediaURLLarge(); 90 | 91 | Media(); 92 | 93 | Media.fromMap(Map map) { 94 | date = map["date"] != null ? DateTime.parse(map["date"]) : null; 95 | dateGMT = map["date_gmt"] != null ? DateTime.parse(map["date_gmt"]) : null; 96 | guid = map['guid']; 97 | id = map['id']; 98 | link = map['link']; 99 | modified = map["modified"] != null ? DateTime.parse(map["modified"]) : null; 100 | modifiedGMT = 101 | map["modified_gmt"] != null 102 | ? DateTime.parse(map["modified_gmt"]) 103 | : null; 104 | slug = map['slug']; 105 | status = map['status']; 106 | type = map['type']; 107 | title = map['title']; 108 | author = map['author']; 109 | commentStatus = map['comment_status']; 110 | pingStatus = map['ping_status']; 111 | meta = map['meta']; 112 | template = map['template']; 113 | altText = map['alt_text']; 114 | caption = map['caption']; 115 | description = map['description']; 116 | mediaType = map['media_type']; 117 | mimeType = map['mime_type']; 118 | mediaDetails = map['media_details']; 119 | post = map['post']; 120 | sourceURL = map['source_url']; 121 | } 122 | 123 | Map toMap() => { 124 | 'date': date?.toIso8601String(), 125 | 'date_gmt': dateGMT?.toIso8601String(), 126 | 'guid': guid, 127 | 'id': id, 128 | 'link': link, 129 | 'modified': modified?.toIso8601String(), 130 | 'modified_gmt': modifiedGMT?.toIso8601String(), 131 | 'slug': slug, 132 | 'status': status, 133 | 'type': type, 134 | 'title': title, 135 | 'author': author, 136 | 'comment_status': commentStatus, 137 | 'ping_status': pingStatus, 138 | 'meta': meta, 139 | 'template': template, 140 | 'alt_text': altText, 141 | 'caption': caption, 142 | 'description': description, 143 | 'media_type': mediaType, 144 | 'mime_type': mimeType, 145 | 'media_details': mediaDetails, 146 | 'post': post, 147 | 'source_url': sourceURL, 148 | }; 149 | 150 | toString() => "Media => " + toMap().toString(); 151 | 152 | String? _featuredMediaURLThumbnail() { 153 | // Make sure we have what we need 154 | if (mediaDetails == null || 155 | mediaDetails!['sizes'] == null || 156 | mediaDetails!['sizes']['thumbnail'] == null) { 157 | return null; 158 | } 159 | 160 | Map thumbnail = mediaDetails!['sizes']['thumbnail']; 161 | return thumbnail['source_url']; 162 | } 163 | 164 | String? _featuredMediaURLMedium() { 165 | // Make sure we have what we need 166 | if (mediaDetails == null || 167 | mediaDetails!['sizes'] == null || 168 | mediaDetails!['sizes']['medium'] == null) { 169 | return null; 170 | } 171 | 172 | Map medium = mediaDetails!['sizes']['medium']; 173 | return medium['source_url']; 174 | } 175 | 176 | String? _featuredMediaURLLarge() { 177 | // Make sure we have what we need 178 | if (mediaDetails == null || 179 | mediaDetails!['sizes'] == null || 180 | mediaDetails!['sizes']['large'] == null) { 181 | return null; 182 | } 183 | 184 | Map large = mediaDetails!['sizes']['large']; 185 | return large['source_url']; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /lib/src/models/post.dart: -------------------------------------------------------------------------------- 1 | class RenderedText { 2 | final String rendered; 3 | final String? raw; 4 | 5 | RenderedText({required this.rendered, this.raw}); 6 | 7 | factory RenderedText.fromMap(Map map) { 8 | return RenderedText( 9 | rendered: map['rendered'] as String? ?? '', 10 | raw: map['raw'] as String?, 11 | ); 12 | } 13 | } 14 | 15 | class Post { 16 | /// The date the object was published, in the site's timezone. 17 | final DateTime? date; 18 | 19 | /// The date the object was published, as GMT. 20 | //DateTime dateGMT; 21 | 22 | /// The globally unique identifier for the object. 23 | //Map guid; 24 | 25 | /// Unique identifier for the object. 26 | final int id; 27 | 28 | /// URL to the object. 29 | final String? link; 30 | 31 | /// The date the object was last modified, in the site's timezone. 32 | // DateTime modified; 33 | 34 | /// The date the object was last modified, as GMT. 35 | //DateTime modifiedGMT; 36 | 37 | /// An alphanumeric identifier for the object unique to its type. 38 | final String? slug; 39 | 40 | /// A named status for the object. 41 | /// 42 | /// One of: publish, future, draft, pending, private 43 | final String? status; 44 | 45 | /// Type of Post for the object. 46 | //String type; 47 | 48 | /// A password to protect access to the content and excerpt. 49 | //String password; 50 | 51 | /// The title for the object. 52 | final RenderedText title; 53 | 54 | /// The content for the object. 55 | final RenderedText content; 56 | 57 | /// The ID for the author of the object 58 | //int author; 59 | 60 | /// The ID for the author of the object 61 | final String? author; 62 | 63 | /// The excerpt for the object. 64 | final RenderedText excerpt; 65 | 66 | /// The ID of the featured media for the object. 67 | final int? featuredMediaId; 68 | 69 | /// The URL of the featured media for the object. 70 | final String? featuredMediaUrl; 71 | 72 | /// Whether or not comments are open on the object 73 | /// 74 | /// One of: open, closed 75 | //String commentStatus; 76 | 77 | /// Whether or not the object can be pinged. 78 | /// 79 | /// One of: open, close 80 | //String pingStatus; 81 | 82 | /// The format for the object. 83 | //String format; 84 | 85 | /// Meta fields. 86 | //dynamic meta; 87 | 88 | /// Whether or not the object should be treated as sticky. 89 | //bool sticky; 90 | 91 | /// The theme file to use to display the object. 92 | // /String template; 93 | 94 | /// The terms assigned to the object in the category taxonomy. 95 | final List? categories; 96 | 97 | /// The terms assigned to the object in the post_tag taxonomy. 98 | final List? tags; 99 | 100 | // Injected objects 101 | // Media featuredMedia; 102 | //User user; 103 | final int? authorId; 104 | 105 | Post({ 106 | required this.id, 107 | this.date, 108 | required this.title, 109 | required this.content, 110 | required this.excerpt, 111 | this.featuredMediaUrl, 112 | this.categories, 113 | this.tags, 114 | this.author, 115 | this.authorId, 116 | this.link, 117 | this.slug, 118 | this.status, 119 | this.featuredMediaId, 120 | }); 121 | 122 | factory Post.fromMap(Map map) { 123 | return Post( 124 | id: map['id'] as int, 125 | date: map['date'] != null ? DateTime.parse(map['date'] as String) : null, 126 | title: RenderedText.fromMap(map['title'] as Map), 127 | content: RenderedText.fromMap(map['content'] as Map), 128 | excerpt: RenderedText.fromMap(map['excerpt'] as Map), 129 | featuredMediaUrl: 130 | map['_embedded']?['wp:featuredmedia']?[0]?['source_url'] as String?, 131 | categories: (map['_embedded']?['wp:term']?[0] as List?) 132 | ?.map((e) => e['name'] as String) 133 | .toList(), 134 | tags: (map['_embedded']?['wp:term']?[1] as List?) 135 | ?.map((e) => e['name'] as String) 136 | .toList(), 137 | author: map['_embedded']?['author']?[0]?['name'] as String?, 138 | authorId: map['author'] as int?, 139 | link: map['link'] as String?, 140 | slug: map['slug'] as String?, 141 | status: map['status'] as String?, 142 | featuredMediaId: map['featured_media'] as int?, 143 | ); 144 | } 145 | 146 | factory Post.fromMapObject(Map map) { 147 | return Post( 148 | id: map['id'] as int, 149 | date: map['date'] != null ? DateTime.parse(map['date'] as String) : null, 150 | title: RenderedText(rendered: map['title'] as String), 151 | content: RenderedText(rendered: map['content'] as String), 152 | excerpt: RenderedText(rendered: map['excerpt'] as String), 153 | featuredMediaUrl: map['featured_media'] as String?, 154 | categories: null, 155 | tags: null, 156 | author: map['author'] as String?, 157 | authorId: null, 158 | link: map['link'] as String?, 159 | slug: map['slug'] as String?, 160 | status: map['status'] as String?, 161 | featuredMediaId: null, 162 | ); 163 | } 164 | 165 | Map toMap() => { 166 | 'id': id, 167 | 'date': date?.toIso8601String(), 168 | 'title': title.rendered, 169 | 'content': content.rendered, 170 | 'excerpt': excerpt.rendered, 171 | 'author': author, 172 | 'author_id': authorId, 173 | 'featured_media': featuredMediaUrl, 174 | 'link': link, 175 | 'slug': slug, 176 | 'status': status, 177 | }; 178 | 179 | @override 180 | String toString() => 'Post{id: $id, title: ${title.rendered}}'; 181 | } 182 | -------------------------------------------------------------------------------- /lib/src/screens/post_details_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_html/flutter_html.dart'; 3 | import 'package:timeago/timeago.dart' as timeago; 4 | 5 | import '../models/post.dart'; 6 | import '../theme/app_theme.dart'; 7 | 8 | class PostDetailsScreen extends StatelessWidget { 9 | final Post post; 10 | 11 | const PostDetailsScreen({ 12 | Key? key, 13 | required this.post, 14 | }) : super(key: key); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | final theme = Theme.of(context); 19 | 20 | return Scaffold( 21 | body: CustomScrollView( 22 | slivers: [ 23 | // Sliver app bar with hero image 24 | SliverAppBar( 25 | expandedHeight: post.featuredMediaUrl != null ? 300.0 : 0.0, 26 | pinned: true, 27 | flexibleSpace: FlexibleSpaceBar( 28 | background: post.featuredMediaUrl != null 29 | ? Hero( 30 | tag: 'post-image-${post.id}', 31 | child: Image.network( 32 | post.featuredMediaUrl!, 33 | fit: BoxFit.cover, 34 | errorBuilder: (context, error, stackTrace) => Container( 35 | color: theme.colorScheme.surfaceVariant, 36 | child: Center( 37 | child: Icon( 38 | Icons.image_not_supported, 39 | color: theme.colorScheme.onSurfaceVariant, 40 | ), 41 | ), 42 | ), 43 | ), 44 | ) 45 | : null, 46 | ), 47 | ), 48 | // Post content 49 | SliverToBoxAdapter( 50 | child: Padding( 51 | padding: const EdgeInsets.all(16.0), 52 | child: Column( 53 | crossAxisAlignment: CrossAxisAlignment.start, 54 | children: [ 55 | // Categories 56 | if (post.categories?.isNotEmpty ?? false) 57 | Wrap( 58 | spacing: 8, 59 | children: post.categories! 60 | .map((category) => Chip( 61 | label: Text( 62 | category, 63 | style: theme.textTheme.labelSmall?.copyWith( 64 | color: 65 | theme.colorScheme.onSecondaryContainer, 66 | ), 67 | ), 68 | backgroundColor: 69 | theme.colorScheme.secondaryContainer, 70 | padding: 71 | const EdgeInsets.symmetric(horizontal: 8), 72 | )) 73 | .toList(), 74 | ), 75 | const SizedBox(height: 16), 76 | // Title 77 | Text( 78 | post.title.rendered, 79 | style: theme.textTheme.headlineMedium?.copyWith( 80 | fontWeight: FontWeight.bold, 81 | ), 82 | ), 83 | const SizedBox(height: 8), 84 | // Date and author 85 | Row( 86 | children: [ 87 | Icon( 88 | Icons.calendar_today, 89 | size: 16, 90 | color: theme.colorScheme.onSurfaceVariant, 91 | ), 92 | const SizedBox(width: 8), 93 | Text( 94 | timeago.format(post.date ?? DateTime.now()), 95 | style: theme.textTheme.bodySmall?.copyWith( 96 | color: theme.colorScheme.onSurfaceVariant, 97 | ), 98 | ), 99 | if (post.author != null) ...[ 100 | const SizedBox(width: 16), 101 | Icon( 102 | Icons.person_outline, 103 | size: 16, 104 | color: theme.colorScheme.onSurfaceVariant, 105 | ), 106 | const SizedBox(width: 8), 107 | Text( 108 | post.author!, 109 | style: theme.textTheme.bodySmall?.copyWith( 110 | color: theme.colorScheme.onSurfaceVariant, 111 | ), 112 | ), 113 | ], 114 | ], 115 | ), 116 | const SizedBox(height: 24), 117 | // Content 118 | Html( 119 | data: post.content.rendered, 120 | style: { 121 | 'body': Style( 122 | margin: Margins.zero, 123 | padding: HtmlPaddings.zero, 124 | fontSize: FontSize(16), 125 | lineHeight: LineHeight(1.6), 126 | color: theme.colorScheme.onSurface, 127 | ), 128 | 'figure': Style( 129 | margin: Margins.zero, 130 | padding: HtmlPaddings.zero, 131 | ), 132 | 'img': Style( 133 | width: Width(MediaQuery.of(context).size.width - 32), 134 | ), 135 | }, 136 | ), 137 | ], 138 | ), 139 | ), 140 | ), 141 | ], 142 | ), 143 | ); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flutter WordPress Client 2 | 3 | A Flutter client for WordPress sites that doesn't require authentication. Perfect for building mobile apps for WordPress-based blogs and news sites. 4 | 5 | ## 🎉 NEW: Modernized Version Available! 6 | 7 | We've created a **completely modernized version** of this WordPress client with significant improvements: 8 | 9 | **📍 Switch to the modernized branch:** 10 | ```bash 11 | git checkout modernized-wordpress-client 12 | ``` 13 | 14 | ### Why Use the Modernized Version? 15 | 16 | | **Original (master)** | **Modernized (modernized-wordpress-client)** | 17 | |----------------------|---------------------------------------------| 18 | | 37 files, 4,487 lines | 15 files, ~1,500 lines | 19 | | 16 dependencies | 8 core dependencies | 20 | | SQLite database | SharedPreferences caching | 21 | | Complex setup (30+ min) | Quick setup (2 minutes) | 22 | | Multiple providers | Unified provider | 23 | | Legacy patterns | Modern Flutter patterns | 24 | 25 | ### Modernized Features: 26 | - ✨ **Material 3 Design** - Clean, modern UI 27 | - 🔍 **Real-time Search** - Instant search with infinite scroll 28 | - 📱 **Responsive Design** - Perfect on all screen sizes 29 | - 🌍 **Arabic/Kurdish Support** - Built-in font support 30 | - ⚡ **Performance Optimized** - Faster loading and smoother scrolling 31 | - 🧪 **Fully Tested** - Comprehensive test coverage 32 | - 🚀 **Production Ready** - Environment-based configuration 33 | 34 | **🎯 Perfect for**: Anyone who wants a simple, modern WordPress client without complexity. 35 | 36 | --- 37 | 38 | ## Original Implementation (Current Branch) 39 | 40 | ## Features 41 | 42 | - 📱 Clean, Material Design UI 43 | - 🚀 Fast and responsive 44 | - 📄 View posts and categories 45 | - 🖼️ Media support 46 | - 🔍 Search functionality 47 | - 🌐 No authentication required 48 | 49 | ## Getting Started 50 | 51 | ### Prerequisites 52 | 53 | - Flutter SDK (>=3.2.0) 54 | - Dart SDK (>=3.2.0) 55 | - A WordPress site with REST API enabled 56 | 57 | ### Installation 58 | 59 | 1. Clone the repository: 60 | ```bash 61 | git clone https://github.com/yourusername/Flutter-Wordpress-Client.git 62 | ``` 63 | 64 | 2. Install dependencies: 65 | ```bash 66 | flutter pub get 67 | ``` 68 | 69 | 3. Update the WordPress site URL in `lib/src/config.dart` 70 | 71 | 4. Run the app: 72 | ```bash 73 | flutter run 74 | ``` 75 | 76 | ## Configuration 77 | 78 | Edit `lib/src/config.dart` to set your WordPress site URL and other configurations: 79 | 80 | ```dart 81 | final String wordPressUrl = 'https://your-wordpress-site.com'; 82 | ``` 83 | 84 | ## Architecture 85 | 86 | The app follows a clean architecture pattern: 87 | - `/models` - Data models 88 | - `/widgets` - Reusable UI components 89 | - `/db` - Local database handling 90 | - `/view_models` - Business logic 91 | 92 | ## Contributing 93 | 94 | Contributions are welcome! Please feel free to submit a Pull Request. 95 | 96 | ## License 97 | 98 | This project is licensed under the MIT License - see the LICENSE file for details. 99 | 100 |

101 | gif 1 title= 102 | image 2 103 |

104 | for more information about WordPress rest API visit https://developer.wordpress.org/rest-api/ 105 | 106 | For help getting started with Flutter, view Flutter online 107 | [documentation](https://flutter.io/). 108 | 109 | I have used this repository: 110 | https://github.com/kbirch/wordpress_client 111 | 112 | ## Prerequisites 113 | 114 | Flutter 115 | 116 | Make sure your WordPress version is greater or equal to 4.7 117 | 118 | Clone repository 119 | git clone https://github.com/hooshyar/Flutter-Wordpress-Client.git 120 | 121 | and open pubspec.yaml 122 | 123 | run 124 | flutter packages get 125 | 126 | open config.dart and change "https://www.mihrabani.com" to your website address for example if my website is wordpress.com you have to change it to this : "http://www.wordpress.com" 127 | Do not add any additional characters like "/". 128 | 129 | to your WordPress website address 130 | 131 | run app on a simulator 132 | flutter run 133 | 134 | ## Roadmap 135 | - [x] Sliver app bar 136 | - [x] Sliver list view 137 | - [x] Connectivity status, if offline pop a message 138 | - [ ] Cache on device 139 | - [x] Pull to refresh 140 | - [x] Global perPage 141 | - [ ] Global theming 142 | - [ ] Setting page 143 | - [x] Provider 144 | - [ ] Splash screen 145 | - [ ] Nice Categories page screen 146 | - [ ] real time clap button like Medium 147 | - [ ] Share and fav buttons 148 | 149 | --- 150 | 151 | ## 🚀 Quick Start Guide 152 | 153 | ### For Modern, Simple Setup (Recommended): 154 | ```bash 155 | git clone https://github.com/hooshyar/Flutter-Wordpress-Client.git 156 | cd Flutter-Wordpress-Client 157 | git checkout modernized-wordpress-client 158 | ``` 159 | **Setup time**: ~2 minutes | **Best for**: New projects, production apps 160 | 161 | ### For Original Implementation: 162 | ```bash 163 | git clone https://github.com/hooshyar/Flutter-Wordpress-Client.git 164 | cd Flutter-Wordpress-Client 165 | # Stay on master branch 166 | ``` 167 | **Setup time**: ~30 minutes | **Best for**: Learning, customization, legacy support 168 | 169 | --- 170 | 171 | ## 📋 Branch Comparison 172 | 173 | | Feature | Master Branch | Modernized Branch | 174 | |---------|--------------|-------------------| 175 | | **Architecture** | Complex, 37 files | Simple, 15 files | 176 | | **Dependencies** | 16 packages | 8 packages | 177 | | **Database** | SQLite | SharedPreferences | 178 | | **State Management** | 3 separate providers | 1 unified provider | 179 | | **UI Design** | Custom Material | Material 3 | 180 | | **Setup Complexity** | High | Low | 181 | | **Maintenance** | High | Low | 182 | | **Performance** | Good | Optimized | 183 | | **Testing** | Basic | Comprehensive | 184 | | **Documentation** | Basic | Detailed | 185 | 186 | **💡 Recommendation**: Use the **modernized branch** for new projects. It's production-ready, well-tested, and much easier to customize. 187 | -------------------------------------------------------------------------------- /lib/src/widgets/post_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_html/flutter_html.dart'; 3 | import 'package:timeago/timeago.dart' as timeago; 4 | import '../models/post.dart'; 5 | import '../theme/app_theme.dart'; 6 | import '../screens/post_details_screen.dart'; 7 | 8 | class PostCard extends StatelessWidget { 9 | final Post post; 10 | 11 | const PostCard({ 12 | Key? key, 13 | required this.post, 14 | }) : super(key: key); 15 | 16 | void _navigateToPostDetails(BuildContext context) { 17 | Navigator.push( 18 | context, 19 | MaterialPageRoute( 20 | builder: (context) => PostDetailsScreen(post: post), 21 | ), 22 | ); 23 | } 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | final theme = Theme.of(context); 28 | 29 | return Card( 30 | elevation: 0, 31 | clipBehavior: Clip.antiAlias, 32 | shape: RoundedRectangleBorder( 33 | borderRadius: BorderRadius.circular(12), 34 | side: BorderSide( 35 | color: theme.colorScheme.outline.withOpacity(0.1), 36 | ), 37 | ), 38 | child: InkWell( 39 | onTap: () => _navigateToPostDetails(context), 40 | child: Column( 41 | crossAxisAlignment: CrossAxisAlignment.start, 42 | children: [ 43 | if (post.featuredMediaUrl != null) 44 | AspectRatio( 45 | aspectRatio: 16 / 9, 46 | child: Hero( 47 | tag: 'post-image-${post.id}', 48 | child: Container( 49 | decoration: BoxDecoration( 50 | color: theme.colorScheme.surfaceVariant, 51 | ), 52 | child: Image.network( 53 | post.featuredMediaUrl!, 54 | fit: BoxFit.cover, 55 | errorBuilder: (context, error, stackTrace) => Center( 56 | child: Icon( 57 | Icons.image_not_supported, 58 | color: theme.colorScheme.onSurfaceVariant, 59 | size: 32, 60 | ), 61 | ), 62 | ), 63 | ), 64 | ), 65 | ), 66 | Padding( 67 | padding: const EdgeInsets.all(16), 68 | child: Column( 69 | crossAxisAlignment: CrossAxisAlignment.start, 70 | children: [ 71 | if (post.categories?.isNotEmpty ?? false) 72 | Wrap( 73 | spacing: 8, 74 | runSpacing: 8, 75 | children: post.categories! 76 | .map((category) => Container( 77 | padding: const EdgeInsets.symmetric( 78 | horizontal: 12, 79 | vertical: 6, 80 | ), 81 | decoration: BoxDecoration( 82 | color: theme.colorScheme.secondaryContainer 83 | .withOpacity(0.7), 84 | borderRadius: BorderRadius.circular(20), 85 | ), 86 | child: Text( 87 | category, 88 | style: theme.textTheme.labelSmall?.copyWith( 89 | color: 90 | theme.colorScheme.onSecondaryContainer, 91 | fontWeight: FontWeight.w600, 92 | ), 93 | ), 94 | )) 95 | .toList(), 96 | ), 97 | const SizedBox(height: 12), 98 | Text( 99 | post.title.rendered, 100 | style: theme.textTheme.titleLarge?.copyWith( 101 | fontWeight: FontWeight.w800, 102 | letterSpacing: -0.5, 103 | height: 1.2, 104 | ), 105 | ), 106 | const SizedBox(height: 8), 107 | Html( 108 | data: post.excerpt.rendered, 109 | style: { 110 | 'body': Style( 111 | margin: Margins.zero, 112 | padding: HtmlPaddings.zero, 113 | fontSize: FontSize(16), 114 | lineHeight: LineHeight(1.5), 115 | maxLines: 3, 116 | textOverflow: TextOverflow.ellipsis, 117 | color: theme.colorScheme.onSurfaceVariant, 118 | ), 119 | }, 120 | ), 121 | const SizedBox(height: 16), 122 | Row( 123 | children: [ 124 | Icon( 125 | Icons.calendar_today, 126 | size: 16, 127 | color: theme.colorScheme.onSurfaceVariant, 128 | ), 129 | const SizedBox(width: 8), 130 | Text( 131 | timeago.format(post.date ?? DateTime.now()), 132 | style: theme.textTheme.bodySmall?.copyWith( 133 | color: theme.colorScheme.onSurfaceVariant, 134 | fontWeight: FontWeight.w500, 135 | ), 136 | ), 137 | const Spacer(), 138 | TextButton( 139 | onPressed: () => _navigateToPostDetails(context), 140 | style: TextButton.styleFrom( 141 | padding: const EdgeInsets.symmetric( 142 | horizontal: 16, 143 | vertical: 8, 144 | ), 145 | shape: RoundedRectangleBorder( 146 | borderRadius: BorderRadius.circular(20), 147 | ), 148 | ), 149 | child: Row( 150 | mainAxisSize: MainAxisSize.min, 151 | children: [ 152 | Text( 153 | 'Read More', 154 | style: theme.textTheme.labelMedium?.copyWith( 155 | color: theme.colorScheme.primary, 156 | fontWeight: FontWeight.w600, 157 | ), 158 | ), 159 | const SizedBox(width: 4), 160 | Icon( 161 | Icons.arrow_forward, 162 | size: 18, 163 | color: theme.colorScheme.primary, 164 | ), 165 | ], 166 | ), 167 | ), 168 | ], 169 | ), 170 | ], 171 | ), 172 | ), 173 | ], 174 | ), 175 | ), 176 | ); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /lib/src/screens/category_posts_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; 4 | 5 | import '../models/category.dart'; 6 | import '../models/post.dart'; 7 | import '../providers/posts_provider.dart'; 8 | import '../widgets/post_card.dart'; 9 | import '../widgets/loading_placeholders.dart'; 10 | import '../widgets/error_indicator.dart'; 11 | 12 | class CategoryPostsScreen extends StatefulWidget { 13 | final Category category; 14 | 15 | const CategoryPostsScreen({ 16 | Key? key, 17 | required this.category, 18 | }) : super(key: key); 19 | 20 | @override 21 | State createState() => _CategoryPostsScreenState(); 22 | } 23 | 24 | class _CategoryPostsScreenState extends State { 25 | late final PostsProvider _postsProvider; 26 | 27 | @override 28 | void initState() { 29 | super.initState(); 30 | _postsProvider = context.read(); 31 | // Filter posts by category when screen loads 32 | WidgetsBinding.instance.addPostFrameCallback((_) { 33 | _postsProvider.filterByCategory(widget.category.id); 34 | }); 35 | } 36 | 37 | @override 38 | void dispose() { 39 | // Clear category filter when leaving the screen 40 | if (mounted) { 41 | _postsProvider.filterByCategory(null); 42 | } 43 | super.dispose(); 44 | } 45 | 46 | @override 47 | Widget build(BuildContext context) { 48 | final theme = Theme.of(context); 49 | final postsProvider = context.watch(); 50 | 51 | return Scaffold( 52 | appBar: AppBar( 53 | centerTitle: true, 54 | title: Column( 55 | mainAxisSize: MainAxisSize.min, 56 | children: [ 57 | Text( 58 | widget.category.name ?? 'Category', 59 | style: theme.textTheme.titleLarge?.copyWith( 60 | fontWeight: FontWeight.bold, 61 | color: theme.colorScheme.onSurface, 62 | ), 63 | ), 64 | if (widget.category.count != null) 65 | Text( 66 | '${widget.category.count} posts', 67 | style: theme.textTheme.bodySmall?.copyWith( 68 | color: theme.colorScheme.onSurfaceVariant, 69 | ), 70 | ), 71 | ], 72 | ), 73 | leading: IconButton( 74 | icon: const Icon(Icons.arrow_back), 75 | onPressed: () => Navigator.pop(context), 76 | ), 77 | ), 78 | body: RefreshIndicator( 79 | onRefresh: () => postsProvider.refreshPosts(), 80 | child: CustomScrollView( 81 | slivers: [ 82 | if (widget.category.description?.isNotEmpty ?? false) 83 | SliverToBoxAdapter( 84 | child: Container( 85 | padding: const EdgeInsets.all(16), 86 | margin: const EdgeInsets.only(bottom: 8), 87 | decoration: BoxDecoration( 88 | color: theme.colorScheme.surfaceVariant.withOpacity(0.5), 89 | border: Border( 90 | bottom: BorderSide( 91 | color: theme.colorScheme.outlineVariant, 92 | width: 1, 93 | ), 94 | ), 95 | ), 96 | child: Text( 97 | widget.category.description!, 98 | style: theme.textTheme.bodyMedium?.copyWith( 99 | color: theme.colorScheme.onSurfaceVariant, 100 | ), 101 | ), 102 | ), 103 | ), 104 | if (postsProvider.isLoading && 105 | postsProvider.pagingController.itemList?.isEmpty == true) 106 | SliverPadding( 107 | padding: const EdgeInsets.all(16.0), 108 | sliver: SliverList( 109 | delegate: SliverChildBuilderDelegate( 110 | (context, index) => const Padding( 111 | padding: EdgeInsets.only(bottom: 16.0), 112 | child: PostCardLoadingPlaceholder(), 113 | ), 114 | childCount: 3, // Show 3 loading placeholders 115 | ), 116 | ), 117 | ) 118 | else if (postsProvider.error != null) 119 | SliverFillRemaining( 120 | hasScrollBody: false, 121 | child: Center( 122 | child: Column( 123 | mainAxisAlignment: MainAxisAlignment.center, 124 | children: [ 125 | Icon( 126 | Icons.error_outline, 127 | size: 48, 128 | color: theme.colorScheme.error, 129 | ), 130 | const SizedBox(height: 16), 131 | Text( 132 | 'Failed to load posts', 133 | style: theme.textTheme.titleLarge, 134 | ), 135 | const SizedBox(height: 8), 136 | Text( 137 | postsProvider.error!, 138 | style: theme.textTheme.bodyMedium, 139 | ), 140 | const SizedBox(height: 16), 141 | ElevatedButton( 142 | onPressed: () => postsProvider.refreshPosts(), 143 | child: const Text('Retry'), 144 | ), 145 | ], 146 | ), 147 | ), 148 | ) 149 | else 150 | PagedSliverList( 151 | pagingController: postsProvider.pagingController, 152 | builderDelegate: PagedChildBuilderDelegate( 153 | itemBuilder: (context, post, index) => Padding( 154 | padding: const EdgeInsets.symmetric( 155 | horizontal: 16, 156 | vertical: 8, 157 | ), 158 | child: PostCard(post: post), 159 | ), 160 | firstPageErrorIndicatorBuilder: (_) => ErrorIndicator( 161 | message: postsProvider.error ?? 'Failed to load posts', 162 | onRetry: () => postsProvider.refreshPosts(), 163 | ), 164 | noItemsFoundIndicatorBuilder: (_) => Center( 165 | child: Text( 166 | 'No posts found in this category', 167 | style: theme.textTheme.titleLarge, 168 | ), 169 | ), 170 | firstPageProgressIndicatorBuilder: (_) => const SizedBox(), 171 | newPageProgressIndicatorBuilder: (_) => const Padding( 172 | padding: EdgeInsets.all(16), 173 | child: PostCardLoadingPlaceholder(), 174 | ), 175 | newPageErrorIndicatorBuilder: (_) => Padding( 176 | padding: const EdgeInsets.all(16), 177 | child: ElevatedButton( 178 | onPressed: () => postsProvider.pagingController 179 | .retryLastFailedRequest(), 180 | child: const Text('Retry'), 181 | ), 182 | ), 183 | ), 184 | ), 185 | ], 186 | ), 187 | ), 188 | ); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /lib/src/widgets/drawerMain.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hawalnir1/src/app.dart'; 3 | import 'package:hawalnir1/src/models/category.dart'; 4 | import '../../wordpress_client.dart'; 5 | import '../config.dart'; 6 | import 'package:shared_preferences/shared_preferences.dart'; 7 | 8 | class DrawerMain extends StatefulWidget { 9 | final WordPressClient client; 10 | 11 | const DrawerMain({Key? key, required this.client}) : super(key: key); 12 | 13 | @override 14 | _DrawerMainState createState() => _DrawerMainState(); 15 | } 16 | 17 | class _DrawerMainState extends State { 18 | @override 19 | Widget build(BuildContext context) { 20 | return Drawer( 21 | elevation: 10.0, 22 | child: Container( 23 | color: Colors.transparent, 24 | child: SafeArea( 25 | child: Column( 26 | children: [ 27 | Expanded( 28 | child: Container( 29 | child: Column( 30 | children: [ 31 | Row( 32 | mainAxisAlignment: MainAxisAlignment.center, 33 | crossAxisAlignment: CrossAxisAlignment.center, 34 | children: [ 35 | Expanded( 36 | child: Container( 37 | alignment: Alignment.center, 38 | color: Colors.black45, 39 | child: Text('Categories '), 40 | ), 41 | ), 42 | ], 43 | ), 44 | Expanded( 45 | child: FutureBuilder>( 46 | future: widget.client.getCategories(), 47 | builder: (context, snapshot) { 48 | if (!snapshot.hasData) { 49 | return LinearProgressIndicator(); 50 | } else { 51 | return ListView.builder( 52 | itemCount: snapshot.data!.length, 53 | itemBuilder: (context, index) { 54 | List _categories = snapshot.data!; 55 | return Container( 56 | margin: EdgeInsets.only(top: 4), 57 | color: Colors.white12, 58 | height: 60, 59 | child: ListTile( 60 | title: Text(_categories[index].name!), 61 | subtitle: Wrap(children: [ 62 | Text( 63 | _categories[index].description!, 64 | softWrap: false, 65 | overflow: TextOverflow.clip, 66 | ), 67 | ]), 68 | leading: Text( 69 | _categories[index].id.toString(), 70 | ), 71 | trailing: Container( 72 | width: 80, 73 | child: 74 | Text(_categories[index].slug!)), 75 | ), 76 | ); 77 | }, 78 | ); 79 | } 80 | }, 81 | ), 82 | ), 83 | ], 84 | ), 85 | ), 86 | ), 87 | Expanded( 88 | child: Column( 89 | children: [ 90 | Row( 91 | mainAxisAlignment: MainAxisAlignment.center, 92 | crossAxisAlignment: CrossAxisAlignment.center, 93 | children: [ 94 | Expanded( 95 | child: Container( 96 | alignment: Alignment.center, 97 | color: Colors.black45, 98 | child: Text('Images '), 99 | ), 100 | ), 101 | ], 102 | ), 103 | Expanded( 104 | child: Container( 105 | padding: EdgeInsets.all(5), 106 | color: Colors.transparent, 107 | child: FutureBuilder>( 108 | future: widget.client.getMedia(), 109 | builder: (context, snapshot) { 110 | if (!snapshot.hasData) { 111 | return LinearProgressIndicator(); 112 | } else { 113 | return GridView.builder( 114 | gridDelegate: 115 | SliverGridDelegateWithFixedCrossAxisCount( 116 | crossAxisCount: 2), 117 | itemCount: snapshot.data!.length, 118 | itemBuilder: (context, index) { 119 | List _medias = snapshot.data!; 120 | return Container( 121 | margin: EdgeInsets.only(top: 4), 122 | height: 60, 123 | child: Container( 124 | padding: EdgeInsets.all(5), 125 | child: ClipRRect( 126 | borderRadius: 127 | BorderRadius.circular(10), 128 | child: Image.network( 129 | _medias[index].sourceURL!, 130 | loadingBuilder: 131 | (BuildContext context, 132 | Widget child, 133 | ImageChunkEvent? 134 | loadingProgress) { 135 | if (loadingProgress == null) 136 | return child; 137 | return Center( 138 | child: LinearProgressIndicator( 139 | value: loadingProgress 140 | .expectedTotalBytes != 141 | null 142 | ? loadingProgress 143 | .cumulativeBytesLoaded / 144 | loadingProgress 145 | .expectedTotalBytes! 146 | : null, 147 | ), 148 | ); 149 | }, 150 | fit: BoxFit.cover, 151 | ), 152 | ), 153 | ), 154 | ); 155 | }, 156 | ); 157 | } 158 | }, 159 | ), 160 | ), 161 | ), 162 | ], 163 | ), 164 | ), 165 | ], 166 | ), 167 | ), 168 | )); 169 | } 170 | } 171 | 172 | Widget drawerBtn(String text, Function function) { 173 | //String text ; 174 | 175 | return Column( 176 | crossAxisAlignment: CrossAxisAlignment.stretch, 177 | children: [ 178 | ElevatedButton( 179 | onPressed: function as void Function()?, 180 | child: Text(text), 181 | ), 182 | ]); 183 | } 184 | 185 | //btn social 186 | Widget socialBtn(String text, IconData iconData, Color color) { 187 | return Column( 188 | crossAxisAlignment: CrossAxisAlignment.stretch, 189 | children: [ 190 | // Padding( 191 | // padding: EdgeInsets.all(20.0), 192 | //), 193 | ElevatedButton( 194 | onPressed: () {}, 195 | child: Row( 196 | children: [ 197 | Icon( 198 | iconData, 199 | color: Colors.cyan, 200 | ), 201 | Padding(padding: EdgeInsets.all(10.0)), 202 | Text( 203 | text, 204 | style: TextStyle(color: Colors.white), 205 | ) 206 | ], 207 | ), 208 | ), 209 | ]); 210 | } 211 | 212 | Widget drawerBtnPadding() { 213 | return Padding( 214 | padding: EdgeInsets.all(5.0), 215 | ); 216 | } 217 | -------------------------------------------------------------------------------- /lib/src/screens/categories_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import '../providers/categories_provider.dart'; 4 | import '../widgets/loading_placeholders.dart'; 5 | import '../widgets/error_indicator.dart'; 6 | import '../screens/category_posts_screen.dart'; 7 | import '../widgets/shimmer_loading.dart'; 8 | 9 | class CategoriesScreen extends StatelessWidget { 10 | const CategoriesScreen({Key? key}) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | final theme = Theme.of(context); 15 | 16 | return Scaffold( 17 | appBar: AppBar( 18 | elevation: 0, 19 | backgroundColor: theme.colorScheme.background, 20 | title: Text( 21 | 'Categories', 22 | style: theme.textTheme.headlineSmall?.copyWith( 23 | fontWeight: FontWeight.w900, 24 | letterSpacing: -0.5, 25 | color: theme.colorScheme.onBackground, 26 | ), 27 | ), 28 | ), 29 | body: Consumer( 30 | builder: (context, categories, _) { 31 | if (categories.isLoading) { 32 | return ListView.builder( 33 | padding: const EdgeInsets.all(16), 34 | itemCount: 5, 35 | itemBuilder: (context, index) => Padding( 36 | padding: const EdgeInsets.only(bottom: 12), 37 | child: Card( 38 | elevation: 0, 39 | shape: RoundedRectangleBorder( 40 | borderRadius: BorderRadius.circular(12), 41 | side: BorderSide( 42 | color: theme.colorScheme.outline.withOpacity(0.1), 43 | ), 44 | ), 45 | child: Padding( 46 | padding: const EdgeInsets.all(16), 47 | child: Column( 48 | crossAxisAlignment: CrossAxisAlignment.start, 49 | children: [ 50 | Row( 51 | children: [ 52 | Expanded( 53 | child: Container( 54 | height: 24, 55 | decoration: BoxDecoration( 56 | color: Colors.white, 57 | borderRadius: BorderRadius.circular(6), 58 | ), 59 | child: const ShimmerLoading( 60 | isLoading: true, 61 | child: SizedBox(), 62 | ), 63 | ), 64 | ), 65 | const SizedBox(width: 8), 66 | const CategoryLoadingPlaceholder(), 67 | ], 68 | ), 69 | const SizedBox(height: 12), 70 | Container( 71 | height: 16, 72 | width: double.infinity, 73 | decoration: BoxDecoration( 74 | color: Colors.white, 75 | borderRadius: BorderRadius.circular(6), 76 | ), 77 | child: const ShimmerLoading( 78 | isLoading: true, 79 | child: SizedBox(), 80 | ), 81 | ), 82 | const SizedBox(height: 8), 83 | Container( 84 | height: 16, 85 | width: 200, 86 | decoration: BoxDecoration( 87 | color: Colors.white, 88 | borderRadius: BorderRadius.circular(6), 89 | ), 90 | child: const ShimmerLoading( 91 | isLoading: true, 92 | child: SizedBox(), 93 | ), 94 | ), 95 | ], 96 | ), 97 | ), 98 | ), 99 | ), 100 | ); 101 | } 102 | 103 | if (categories.error?.isNotEmpty ?? false) { 104 | return Center( 105 | child: ErrorIndicator( 106 | message: categories.error ?? 'Failed to load categories', 107 | onRetry: () => categories.refreshCategories(), 108 | ), 109 | ); 110 | } 111 | 112 | return RefreshIndicator( 113 | onRefresh: categories.refreshCategories, 114 | child: ListView.builder( 115 | padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), 116 | itemCount: categories.categories.length, 117 | itemBuilder: (context, index) { 118 | final category = categories.categories[index]; 119 | return Card( 120 | elevation: 0, 121 | margin: const EdgeInsets.only(bottom: 12), 122 | shape: RoundedRectangleBorder( 123 | borderRadius: BorderRadius.circular(12), 124 | side: BorderSide( 125 | color: theme.colorScheme.outline.withOpacity(0.1), 126 | ), 127 | ), 128 | child: InkWell( 129 | borderRadius: BorderRadius.circular(12), 130 | onTap: () { 131 | Navigator.push( 132 | context, 133 | MaterialPageRoute( 134 | builder: (context) => CategoryPostsScreen( 135 | category: category, 136 | ), 137 | ), 138 | ); 139 | }, 140 | child: Padding( 141 | padding: const EdgeInsets.all(16), 142 | child: Column( 143 | crossAxisAlignment: CrossAxisAlignment.start, 144 | children: [ 145 | Row( 146 | children: [ 147 | Expanded( 148 | child: Text( 149 | category.name ?? 'Unnamed Category', 150 | style: theme.textTheme.titleLarge?.copyWith( 151 | fontWeight: FontWeight.w800, 152 | letterSpacing: -0.5, 153 | ), 154 | ), 155 | ), 156 | if (category.count != null) 157 | Container( 158 | padding: const EdgeInsets.symmetric( 159 | horizontal: 12, 160 | vertical: 6, 161 | ), 162 | decoration: BoxDecoration( 163 | color: theme.colorScheme.secondaryContainer 164 | .withOpacity(0.7), 165 | borderRadius: BorderRadius.circular(20), 166 | ), 167 | child: Text( 168 | '${category.count} posts', 169 | style: theme.textTheme.labelSmall?.copyWith( 170 | color: theme 171 | .colorScheme.onSecondaryContainer, 172 | fontWeight: FontWeight.w600, 173 | ), 174 | ), 175 | ), 176 | ], 177 | ), 178 | if (category.description?.isNotEmpty ?? false) ...[ 179 | const SizedBox(height: 8), 180 | Text( 181 | category.description!, 182 | style: theme.textTheme.bodyMedium?.copyWith( 183 | color: theme.colorScheme.onSurfaceVariant, 184 | height: 1.5, 185 | ), 186 | maxLines: 2, 187 | overflow: TextOverflow.ellipsis, 188 | ), 189 | ], 190 | const SizedBox(height: 12), 191 | Row( 192 | mainAxisAlignment: MainAxisAlignment.end, 193 | children: [ 194 | Icon( 195 | Icons.arrow_forward, 196 | size: 18, 197 | color: theme.colorScheme.primary, 198 | ), 199 | const SizedBox(width: 4), 200 | Text( 201 | 'View Posts', 202 | style: theme.textTheme.labelMedium?.copyWith( 203 | color: theme.colorScheme.primary, 204 | fontWeight: FontWeight.w600, 205 | ), 206 | ), 207 | ], 208 | ), 209 | ], 210 | ), 211 | ), 212 | ), 213 | ); 214 | }, 215 | ), 216 | ); 217 | }, 218 | ), 219 | ); 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /lib/src/screens/home_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; 4 | import '../models/post.dart'; 5 | import '../providers/posts_provider.dart'; 6 | import '../providers/categories_provider.dart'; 7 | import '../widgets/post_card.dart'; 8 | import '../widgets/loading_indicator.dart'; 9 | import '../widgets/error_indicator.dart'; 10 | import '../theme/app_theme.dart'; 11 | import 'categories_screen.dart'; 12 | import 'package:shared_preferences/shared_preferences.dart'; 13 | import 'category_posts_screen.dart'; 14 | 15 | class HomeScreen extends StatelessWidget { 16 | const HomeScreen({Key? key}) : super(key: key); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | // Add error handling for SharedPreferences 21 | try { 22 | SharedPreferences.getInstance().then((prefs) { 23 | // Initialize preferences here 24 | }).catchError((error) { 25 | ScaffoldMessenger.of(context).showSnackBar( 26 | const SnackBar( 27 | content: Text( 28 | 'Failed to initialize preferences. Some features may be limited.'), 29 | ), 30 | ); 31 | }); 32 | } catch (e) { 33 | // Handle any synchronous errors 34 | } 35 | 36 | final theme = Theme.of(context); 37 | 38 | return Scaffold( 39 | body: CustomScrollView( 40 | slivers: [ 41 | SliverAppBar( 42 | floating: false, 43 | pinned: false, 44 | title: Text( 45 | 'WP Client', 46 | style: theme.textTheme.headlineMedium?.copyWith( 47 | color: theme.colorScheme.onSurface, 48 | fontWeight: FontWeight.bold, 49 | ), 50 | ), 51 | centerTitle: false, 52 | stretch: false, 53 | actions: [ 54 | IconButton( 55 | icon: const Icon(Icons.category_outlined), 56 | tooltip: 'Categories', 57 | onPressed: () async { 58 | final category = await Navigator.push( 59 | context, 60 | MaterialPageRoute( 61 | builder: (context) => const CategoriesScreen(), 62 | ), 63 | ); 64 | if (category != null && context.mounted) { 65 | // TODO: Filter posts by selected category 66 | } 67 | }, 68 | ), 69 | IconButton( 70 | icon: const Icon(Icons.search), 71 | tooltip: 'Search', 72 | onPressed: () { 73 | // TODO: Implement search 74 | }, 75 | ), 76 | IconButton( 77 | icon: const Icon(Icons.dark_mode), 78 | tooltip: 'Toggle theme', 79 | onPressed: () { 80 | // TODO: Toggle theme 81 | }, 82 | ), 83 | ], 84 | ), 85 | Consumer( 86 | builder: (context, postsProvider, _) { 87 | if (postsProvider.isLoading && 88 | postsProvider.pagingController.itemList?.isEmpty == true) { 89 | return const SliverFillRemaining( 90 | child: Center(child: CircularProgressIndicator()), 91 | ); 92 | } 93 | 94 | if (postsProvider.error != null) { 95 | return SliverFillRemaining( 96 | child: Center( 97 | child: Column( 98 | mainAxisAlignment: MainAxisAlignment.center, 99 | children: [ 100 | Icon( 101 | Icons.error_outline, 102 | size: 48, 103 | color: theme.colorScheme.error, 104 | ), 105 | const SizedBox(height: 16), 106 | Text( 107 | 'Failed to load posts', 108 | style: theme.textTheme.titleLarge, 109 | ), 110 | const SizedBox(height: 8), 111 | Text( 112 | postsProvider.error!, 113 | style: theme.textTheme.bodyMedium, 114 | ), 115 | const SizedBox(height: 16), 116 | ElevatedButton( 117 | onPressed: () => postsProvider.refreshPosts(), 118 | child: const Text('Retry'), 119 | ), 120 | ], 121 | ), 122 | ), 123 | ); 124 | } 125 | 126 | return PagedSliverList( 127 | pagingController: postsProvider.pagingController, 128 | builderDelegate: PagedChildBuilderDelegate( 129 | itemBuilder: (context, post, index) => Padding( 130 | padding: const EdgeInsets.symmetric( 131 | horizontal: 16, 132 | vertical: 8, 133 | ), 134 | child: PostCard(post: post), 135 | ), 136 | firstPageErrorIndicatorBuilder: (_) => Center( 137 | child: Column( 138 | mainAxisAlignment: MainAxisAlignment.center, 139 | children: [ 140 | Icon( 141 | Icons.error_outline, 142 | size: 48, 143 | color: theme.colorScheme.error, 144 | ), 145 | const SizedBox(height: 16), 146 | Text( 147 | 'Failed to load posts', 148 | style: theme.textTheme.titleLarge, 149 | ), 150 | const SizedBox(height: 16), 151 | ElevatedButton( 152 | onPressed: () => postsProvider.refreshPosts(), 153 | child: const Text('Retry'), 154 | ), 155 | ], 156 | ), 157 | ), 158 | noItemsFoundIndicatorBuilder: (_) => Center( 159 | child: Text( 160 | 'No posts found', 161 | style: theme.textTheme.titleLarge, 162 | ), 163 | ), 164 | firstPageProgressIndicatorBuilder: (_) => const Center( 165 | child: CircularProgressIndicator(), 166 | ), 167 | newPageProgressIndicatorBuilder: (_) => const Padding( 168 | padding: EdgeInsets.all(16), 169 | child: Center( 170 | child: CircularProgressIndicator(), 171 | ), 172 | ), 173 | newPageErrorIndicatorBuilder: (_) => Padding( 174 | padding: const EdgeInsets.all(16), 175 | child: ElevatedButton( 176 | onPressed: () => postsProvider.pagingController 177 | .retryLastFailedRequest(), 178 | child: const Text('Retry'), 179 | ), 180 | ), 181 | ), 182 | ); 183 | }, 184 | ), 185 | ], 186 | ), 187 | ); 188 | } 189 | } 190 | 191 | class AppDrawer extends StatelessWidget { 192 | const AppDrawer({super.key}); 193 | 194 | @override 195 | Widget build(BuildContext context) { 196 | final categories = context.watch(); 197 | final posts = context.watch(); 198 | final theme = Theme.of(context); 199 | 200 | return Drawer( 201 | child: Column( 202 | children: [ 203 | DrawerHeader( 204 | decoration: BoxDecoration( 205 | color: theme.colorScheme.primary, 206 | ), 207 | child: Center( 208 | child: Column( 209 | mainAxisSize: MainAxisSize.min, 210 | children: [ 211 | Text( 212 | 'Categories', 213 | style: theme.textTheme.headlineSmall?.copyWith( 214 | color: theme.colorScheme.onPrimary, 215 | ), 216 | ), 217 | if (posts.selectedCategoryId != null) ...[ 218 | const SizedBox(height: 8), 219 | TextButton.icon( 220 | icon: const Icon(Icons.clear), 221 | label: const Text('Clear Filter'), 222 | style: TextButton.styleFrom( 223 | foregroundColor: theme.colorScheme.onPrimary, 224 | ), 225 | onPressed: () { 226 | posts.filterByCategory(null); 227 | Navigator.pop(context); 228 | }, 229 | ), 230 | ], 231 | ], 232 | ), 233 | ), 234 | ), 235 | if (categories.isLoading) 236 | const LoadingIndicator() 237 | else if (categories.error?.isNotEmpty ?? false) 238 | ErrorIndicator( 239 | message: categories.error ?? 'Failed to load categories') 240 | else 241 | Expanded( 242 | child: ListView.builder( 243 | itemCount: categories.categories.length, 244 | itemBuilder: (context, index) { 245 | final category = categories.categories[index]; 246 | final isSelected = category.id == posts.selectedCategoryId; 247 | 248 | return ListTile( 249 | title: Text( 250 | category.name ?? 'Unnamed Category', 251 | style: isSelected 252 | ? TextStyle( 253 | color: theme.colorScheme.primary, 254 | fontWeight: FontWeight.bold, 255 | ) 256 | : null, 257 | ), 258 | leading: category.count != null 259 | ? Container( 260 | padding: const EdgeInsets.all(8), 261 | decoration: BoxDecoration( 262 | color: isSelected 263 | ? theme.colorScheme.primaryContainer 264 | : theme.colorScheme.surfaceVariant, 265 | borderRadius: BorderRadius.circular(8), 266 | ), 267 | child: Text( 268 | '${category.count}', 269 | style: theme.textTheme.labelSmall?.copyWith( 270 | color: isSelected 271 | ? theme.colorScheme.onPrimaryContainer 272 | : theme.colorScheme.onSurfaceVariant, 273 | ), 274 | ), 275 | ) 276 | : null, 277 | selected: isSelected, 278 | onTap: () { 279 | Navigator.push( 280 | context, 281 | MaterialPageRoute( 282 | builder: (context) => CategoryPostsScreen( 283 | category: category, 284 | ), 285 | ), 286 | ); 287 | }, 288 | ); 289 | }, 290 | ), 291 | ), 292 | ], 293 | ), 294 | ); 295 | } 296 | } 297 | --------------------------------------------------------------------------------