diff --git a/.gitignore b/.gitignore index 4c50479..a45847f 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ .pub/ /build/ build/ +pubspec.lock # Web related lib/generated_plugin_registrant.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 494ac5b..923b654 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,43 @@ -## [1.0.1] -- Remove debug statements +## [3.1.0] + +Contributed by [@maRci002](https://github.com/maRci002) - see PR [#59](https://github.com/jonataslaw/readmore/pull/59). + +- Added `ReadMoreText.rich` constructor for rich text formatting and interactivity, enhancing text presentation with customizable styles and interactive elements. + +## [3.0.0] + +Contributed by [@maRci002](https://github.com/maRci002) - see PR [#54](https://github.com/jonataslaw/readmore/pull/54). + +**Breaking Changes**: + +- Upgraded to **Dart 3.0.0** +- **`textScaleFactor`** replaced with **`textScaler`** +- Removed **`callback`** in favor of the **`isCollapsed`** controller + +**Features**: + +- Introduced **`Annotation`** functionality for custom styling and interactions with text patterns like hashtags, URLs, and mentions, enhancing text interactivity and usability. + +- This version also has minor contributions from the following users: (@zeeshanhussain), (@sm2017), (@andreaselia), (@amereii), (@govindmaheshwari2),(@olisaemekaejiofor), (@epynic), (@webdastur), (@Siqlain-Hanif), (@unger1984), + +## [2.2.0] + +- Update to Flutter 3. + +## [2.1.0] + +- add RTL support + +## [2.0.0] + +- nullsafety + +## [1.0.1] + +- Remove debug statements - Add delimiter span && style for trimExpandedText & trimCollapsedText - Fix linkSize on ListView -## [1.0.0] +## [1.0.0] + - initial release diff --git a/README.md b/README.md index 6121393..f64d24a 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,100 @@ # readmore -A Flutter plugin than allow expand and collapse text. +A Flutter plugin that allows for expanding and collapsing text with the added capability to style and interact with specific patterns in the text like hashtags, URLs, and mentions using the `Annotation` feature. ![](read-more-text-view-flutter.gif) -## usage: -add to your pubspec +## Usage: +Add the package to your pubspec.yaml: + +```yaml +readmore: ^3.0.0 ``` -readmore: ^1.0.0 -``` -and import: -``` + +Then, import it in your Dart code: + +```dart import 'package:readmore/readmore.dart'; ``` +### Basic Example + ```dart ReadMoreText( 'Flutter is Google’s mobile UI open source framework to build high-quality native (super fast) interfaces for iOS and Android apps with the unified codebase.', + trimMode: TrimMode.Line, trimLines: 2, colorClickableText: Colors.pink, - trimMode: TrimMode.Line, trimCollapsedText: 'Show more', trimExpandedText: 'Show less', moreStyle: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), ); ``` +### Rich Text Example + +With the `ReadMoreText.rich` constructor, you can create text with rich formatting, including different colors, decorations, letter spacing, and interactive elements within a single widget. + +```dart +ReadMoreText.rich( + TextSpan( + style: const TextStyle(color: Colors.black), + children: [ + const TextSpan(text: 'Rich '), + const TextSpan( + text: 'Text', + style: TextStyle( + color: Colors.blue, + decoration: TextDecoration.underline, + letterSpacing: 5, + recognizer: TapGestureRecognizer()..onTap = () { + // Handle tap + }, + ), + ), + ], + ), + trimMode: TrimMode.Line, + trimLines: 2, + colorClickableText: Colors.pink, + trimCollapsedText: '...Read more', + trimExpandedText: ' Less', +); +``` + +### Using Annotations + +The `Annotation` feature enhances the interactivity and functionality of the text content. You can define custom styles and interactions for patterns like hashtags, URLs, and mentions. + +```dart +ReadMoreText( + 'This is a sample text with a #hashtag, a mention <@123>, and a URL: https://example.com.', + trimMode: TrimMode.Line, + trimLines: 2, + colorClickableText: Colors.pink, + annotations: [ + Annotation( + regExp: RegExp(r'#([a-zA-Z0-9_]+)'), + spanBuilder: ({required String text, required TextStyle textStyle}) => TextSpan( + text: text, + style: textStyle.copyWith(color: Colors.blue), + ), + ), + Annotation( + regExp: RegExp(r'<@(\d+)>'), + spanBuilder: ({required String text, required TextStyle textStyle}) => TextSpan( + text: 'User123', + style: textStyle.copyWith(color: Colors.green), + recognizer: TapGestureRecognizer()..onTap = () { + // Handle tap, e.g., navigate to a user profile + }, + ), + ), + // Additional annotations for URLs... + ], + moreStyle: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), +); +``` +_Note: This feature is not available with the `ReadMoreText.rich` constructor, as `TextSpan` gives you total control over styling and adding interactions to parts of the text._ diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000..6d47c6f --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,47 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ +pubspec.lock + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/example/.metadata b/example/.metadata new file mode 100644 index 0000000..56bfc2c --- /dev/null +++ b/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: f4abaa0735eba4dfd8f33f73363911d63931fe03 + channel: stable + +project_type: app diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..a135626 --- /dev/null +++ b/example/README.md @@ -0,0 +1,16 @@ +# example + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/example/android/.gitignore b/example/android/.gitignore new file mode 100644 index 0000000..0a741cb --- /dev/null +++ b/example/android/.gitignore @@ -0,0 +1,11 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle new file mode 100644 index 0000000..3927e33 --- /dev/null +++ b/example/android/app/build.gradle @@ -0,0 +1,59 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 31 + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.example" + minSdkVersion flutter.minSdkVersion + targetSdkVersion 30 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..c208884 --- /dev/null +++ b/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..34dd77e --- /dev/null +++ b/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + diff --git a/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt b/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt new file mode 100644 index 0000000..e793a00 --- /dev/null +++ b/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt @@ -0,0 +1,6 @@ +package com.example.example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/example/android/app/src/main/res/drawable-v21/launch_background.xml b/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/example/android/app/src/main/res/drawable/launch_background.xml b/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/values-night/styles.xml b/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..449a9f9 --- /dev/null +++ b/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..d74aa35 --- /dev/null +++ b/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..c208884 --- /dev/null +++ b/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/build.gradle b/example/android/build.gradle new file mode 100644 index 0000000..f186e83 --- /dev/null +++ b/example/android/build.gradle @@ -0,0 +1,29 @@ +buildscript { + ext.kotlin_version = '1.6.10' + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.2.1' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" + project.evaluationDependsOn(':app') +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/example/android/gradle.properties b/example/android/gradle.properties new file mode 100644 index 0000000..94adc3a --- /dev/null +++ b/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..562c5e4 --- /dev/null +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle new file mode 100644 index 0000000..44e62bc --- /dev/null +++ b/example/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/example/ios/.gitignore b/example/ios/.gitignore new file mode 100644 index 0000000..151026b --- /dev/null +++ b/example/ios/.gitignore @@ -0,0 +1,33 @@ +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..7c56964 --- /dev/null +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 + + diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/example/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..1f6541e --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,474 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..e67b280 --- /dev/null +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..28c6bf0 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..f091b6b Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cde121 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..d0ef06e Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..dcdc230 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..c8f9ed8 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..75b2d16 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..c4df70d Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..6a84f41 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..d0e1f58 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +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. \ No newline at end of file diff --git a/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/Base.lproj/Main.storyboard b/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist new file mode 100644 index 0000000..4f68a2c --- /dev/null +++ b/example/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/example/ios/Runner/Runner-Bridging-Header.h b/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/example/lib/example.dart b/example/lib/example.dart deleted file mode 100644 index 8abe697..0000000 --- a/example/lib/example.dart +++ /dev/null @@ -1,80 +0,0 @@ -import 'package:readmore/readmore.dart'; -import 'package:flutter/material.dart'; - -void main() { - runApp(MaterialApp( - debugShowCheckedModeBanner: false, - theme: ThemeData( - primaryColor: const Color(0xFF02BB9F), - primaryColorDark: const Color(0xFF167F67), - accentColor: const Color(0xFF02BB9F), - ), - title: 'Read More Text', - home: DemoApp(), - )); -} - -class DemoApp extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text( - 'Read More Text', - style: TextStyle(color: Colors.white), - )), - body: DefaultTextStyle.merge( - style: const TextStyle( - fontSize: 16.0, - //fontFamily: 'monospace', - ), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: ReadMoreText( - 'Flutter is Google’s mobile UI open source framework to build high-quality native (super fast) interfaces for iOS and Android apps with the unified codebase.', - trimLines: 2, - colorClickableText: Colors.pink, - trimMode: TrimMode.Line, - trimCollapsedText: '...Show more', - trimExpandedText: ' show less', - ), - ), - Divider( - color: const Color(0xFF167F67), - ), - Padding( - padding: const EdgeInsets.all(16.0), - child: ReadMoreText( - 'Flutter has its own UI components, along with an engine to render them on both the Android and iOS platforms. Most of those UI components, right out of the box, conform to the guidelines of Material Design.', - trimLines: 3, - colorClickableText: Colors.pink, - trimMode: TrimMode.Line, - trimCollapsedText: '...Expand', - trimExpandedText: ' Collapse ', - ), - ), - Divider( - color: const Color(0xFF167F67), - ), - Padding( - padding: const EdgeInsets.all(16.0), - child: ReadMoreText( - 'The Flutter framework builds its layout via the composition of widgets, everything that you construct programmatically is a widget and these are compiled together to create the user interface. ', - trimLines: 2, - colorClickableText: Colors.pink, - trimMode: TrimMode.Line, - trimCollapsedText: '...Read more', - trimExpandedText: ' Less', - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/example/lib/main.dart b/example/lib/main.dart new file mode 100644 index 0000000..e49e4c0 --- /dev/null +++ b/example/lib/main.dart @@ -0,0 +1,430 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:readmore/readmore.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + theme: ThemeData( + primaryColor: const Color(0xFF02BB9F), + primaryColorDark: const Color(0xFF167F67), + ), + title: 'Read More Text', + home: const DemoApp(), + ); + } +} + +class DemoApp extends StatefulWidget { + const DemoApp({super.key}); + + @override + State createState() => _DemoAppState(); +} + +class _DemoAppState extends State { + final isCollapsed = ValueNotifier(false); + + // K: UID, V: Username + final userMap = { + 123: 'Android', + 456: 'iOS', + }; + + var _trimMode = TrimMode.Line; + int _trimLines = 3; + int _trimLength = 190; + + void _incrementTrimLines() => setState(() => _trimLines++); + + void _decrementTrimLines() => + setState(() => _trimLines = _trimLines > 1 ? _trimLines - 1 : 1); + + void _incrementTrimLength() => setState(() => _trimLength++); + + void _decrementTrimLength() => + setState(() => _trimLength = _trimLength > 1 ? _trimLength - 1 : 1); + + void _showMessage(String message) { + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar(SnackBar(content: Text(message))); + } + + @override + void dispose() { + super.dispose(); + + isCollapsed.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Read More Text'), + ), + body: DefaultTextStyle.merge( + style: const TextStyle(fontSize: 14), + child: DraggableDivider( + child: SingleChildScrollView( + child: _buildContent(), + ), + ), + ), + ); + } + + Column _buildContent() { + return Column( + children: [ + _buildSettings(), + Padding( + key: const Key('showMore'), + padding: const EdgeInsets.all(16), + child: ReadMoreText( + 'Flutter is Google’s mobile UI open source framework to build high-quality native (super fast) interfaces for iOS and Android apps with the unified codebase.😊😊😊', + trimMode: _trimMode, + trimLines: _trimLines, + trimLength: _trimLength, + preDataText: 'AMANDA', + preDataTextStyle: const TextStyle(fontWeight: FontWeight.w500), + style: const TextStyle(color: Colors.black), + colorClickableText: Colors.pink, + trimCollapsedText: '...Show more', + trimExpandedText: ' show less', + ), + ), + const Divider( + color: Color(0xFF167F67), + ), + Padding( + padding: const EdgeInsets.all(16), + child: ReadMoreText( + 'Flutter(https://flutter.dev/) has its own UI components, along with an engine to render them on both the <@123> and <@456> platforms <@999> http://google.com #read_more. Most of those UI components, right out of the box, conform to the guidelines of #Material Design.😊😊😊', + trimMode: _trimMode, + trimLines: _trimLines, + trimLength: _trimLength, + style: const TextStyle(color: Colors.black), + colorClickableText: Colors.pink, + trimCollapsedText: '...Expand', + trimExpandedText: ' Collapse ', + annotations: [ + // URL + Annotation( + regExp: RegExp( + r'(?:(?:https?|ftp)://)?[\w/\-?=%.]+\.[\w/\-?=%.]+', + ), + spanBuilder: ({ + required String text, + required TextStyle textStyle, + }) { + return TextSpan( + text: text, + style: textStyle.copyWith( + decoration: TextDecoration.underline, + color: Colors.green, + ), + recognizer: TapGestureRecognizer() + ..onTap = () => _showMessage(text), + ); + }, + ), + // Mention + Annotation( + regExp: RegExp(r'<@(\d+)>'), + spanBuilder: ({ + required String text, + required TextStyle textStyle, + }) { + final user = userMap[int.tryParse( + text.substring(2, text.length - 1), + )]; + + if (user == null) { + return TextSpan( + text: '@unknown user', + style: textStyle.copyWith( + fontWeight: FontWeight.bold, + ), + recognizer: TapGestureRecognizer() + ..onTap = () => _showMessage('User not found'), + ); + } + + return TextSpan( + text: '@$user', + style: textStyle.copyWith( + decoration: TextDecoration.underline, + color: Colors.redAccent, + ), + recognizer: TapGestureRecognizer() + ..onTap = () => _showMessage('@$user'), + children: [ + if (user == 'iOS') const TextSpan(text: 'Extra'), + ], + ); + }, + ), + // Hashtag + Annotation( + // Test: non capturing group should work also + regExp: RegExp('#(?:[a-zA-Z0-9_]+)'), + spanBuilder: ({ + required String text, + required TextStyle textStyle, + }) { + return TextSpan( + text: text, + style: textStyle.copyWith( + color: Colors.blueAccent, + height: 1.5, + letterSpacing: 5, + ), + recognizer: TapGestureRecognizer() + ..onTap = () => _showMessage(text), + ); + }, + ), + ], + ), + ), + const Divider( + color: Color(0xFF167F67), + ), + Padding( + padding: const EdgeInsets.all(16), + child: ReadMoreText( + 'The Flutter framework builds its layout via the composition of widgets, everything that you construct programmatically is a widget and these are compiled together to create the user interface. 😊😊😊', + trimMode: _trimMode, + trimLines: _trimLines, + trimLength: _trimLength, + isCollapsed: isCollapsed, + style: const TextStyle(color: Colors.black), + colorClickableText: Colors.pink, + trimCollapsedText: '...Read more', + trimExpandedText: ' Less', + ), + ), + ValueListenableBuilder( + valueListenable: isCollapsed, + builder: (context, value, child) { + return Center( + child: ElevatedButton( + onPressed: () => isCollapsed.value = !isCollapsed.value, + child: Text('is collapsed: $value'), + ), + ); + }, + ), + const Divider( + color: Color(0xFF167F67), + ), + Padding( + padding: const EdgeInsets.all(16), + // Rich version + child: ReadMoreText.rich( + TextSpan( + style: const TextStyle(color: Colors.black), + children: [ + const TextSpan(text: 'The '), + const TextSpan( + text: 'Flutter framework ', + style: TextStyle( + color: Colors.blue, + decoration: TextDecoration.underline, + letterSpacing: 5, + ), + ), + const TextSpan( + text: 'builds its layout via the composition of ', + ), + TextSpan( + text: 'widgets, ', + style: const TextStyle(color: Colors.green), + recognizer: TapGestureRecognizer() + ..onTap = () => _showMessage('Widgets tapped!'), + ), + const TextSpan( + text: + 'everything that you construct programmatically is a widget and these are compiled together to create the user interface.😊😊😊', + ), + ], + ), + style: TextStyle(inherit: false), + trimMode: _trimMode, + trimLines: _trimLines, + trimLength: _trimLength, + colorClickableText: Colors.blueAccent, + trimCollapsedText: '...Read more', + trimExpandedText: ' Less', + ), + ), + const Divider( + color: Color(0xFF167F67), + ), + Padding( + padding: const EdgeInsets.all(16), + child: ReadMoreText( + '😊' * 200, + trimMode: _trimMode, + trimLines: _trimLines, + trimLength: _trimLength, + colorClickableText: Colors.blueAccent, + trimCollapsedText: '...Read more', + trimExpandedText: ' Less', + ), + ), + ], + ); + } + + Column _buildSettings() { + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Trim Mode'), + Padding( + padding: const EdgeInsets.all(8), + child: SegmentedButton( + segments: const [ + ButtonSegment( + value: TrimMode.Length, + label: Text('Length'), + ), + ButtonSegment( + value: TrimMode.Line, + label: Text('Line'), + ), + ], + selected: {_trimMode}, + onSelectionChanged: (Set newSelection) { + setState(() { + _trimMode = newSelection.first; + }); + }, + ), + ), + if (_trimMode == TrimMode.Length) + Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.remove), + onPressed: _decrementTrimLength, + ), + Text('$_trimLength'), + IconButton( + icon: const Icon(Icons.add), + onPressed: _incrementTrimLength, + ), + ], + ), + if (_trimMode == TrimMode.Line) + Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.remove), + onPressed: _decrementTrimLines, + ), + Text('$_trimLines'), + IconButton( + icon: const Icon(Icons.add), + onPressed: _incrementTrimLines, + ), + ], + ), + ], + ); + } +} + +class DraggableDivider extends StatefulWidget { + const DraggableDivider({super.key, required this.child}); + + final Widget child; + + @override + State createState() => _DraggableDividerState(); +} + +class _DraggableDividerState extends State { + final double dividerWidth = 10; + late double _leftWidth; + final double _minWidth = 20; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + final screenWidth = MediaQuery.sizeOf(context).width; + setState(() { + _leftWidth = screenWidth - _minWidth - dividerWidth; + }); + } + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: _leftWidth, + child: widget.child, + ), + GestureDetector( + onHorizontalDragUpdate: (details) { + setState(() { + final newWidth = _leftWidth + details.delta.dx; + final screenWidth = MediaQuery.of(context).size.width; + + if (newWidth >= _minWidth && + (screenWidth - newWidth - dividerWidth) >= _minWidth) { + _leftWidth = newWidth; + } + }); + }, + child: Container( + color: Colors.grey, + width: 10, + ), + ), + const Expanded( + child: Center(child: VerticalText('Drag Test')), + ), + ], + ); + } +} + +class VerticalText extends StatelessWidget { + const VerticalText(this.text, {super.key}); + + final String text; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: text + .split('') + .map( + (letter) => Text( + letter, + textAlign: TextAlign.center, + ), + ) + .toList(), + ); + } +} diff --git a/example/pubspec.yaml b/example/pubspec.yaml index f2ad384..e48b669 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,11 +1,24 @@ -name: readmore_demo +name: example description: A new Flutter project. -version: 0.0.1 -author: -homepage: + +# The following line prevents the package from being accidentally published to +# pub.dev using `pub publish`. This is preferred for private packages. +publish_to: "none" # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +version: 1.0.0+1 environment: - sdk: ">=2.1.0 <3.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: flutter: @@ -13,6 +26,10 @@ dependencies: readmore: path: ../ + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.5 + dev_dependencies: flutter_test: sdk: flutter @@ -22,19 +39,23 @@ dev_dependencies: # The following section is specific to Flutter. flutter: + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true - # To add assets to your package, add an assets section, like this: + # To add assets to your application, add an assets section, like this: # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # - # For details regarding assets in packages, see - # https://flutter.dev/assets-and-images/#from-packages - # + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. - # To add custom fonts to your package, add a fonts section here, + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, # in this "flutter" section. Each entry in this list should have a # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For @@ -51,5 +72,5 @@ flutter: # - asset: fonts/TrajanPro_Bold.ttf # weight: 700 # - # For details regarding fonts in packages, see - # https://flutter.dev/custom-fonts/#from-packages + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart new file mode 100644 index 0000000..7654fb6 --- /dev/null +++ b/example/test/widget_test.dart @@ -0,0 +1,48 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility that Flutter provides. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:example/main.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + final finderShowMore = find.byWidgetPredicate( + (widget) => widget is RichText && tapTextSpan(widget, '...Show more'), + ); + + final finderShowLess = find.byWidgetPredicate( + (widget) => widget is RichText && tapTextSpan(widget, ' show less'), + ); + + // Verify that our counter starts at 0. + expect(finderShowMore, findsOneWidget); + expect(finderShowLess, findsNothing); + }); +} + +bool findTextAndTap(InlineSpan visitor, String text) { + if (visitor is TextSpan && visitor.text == text) { + (visitor.recognizer! as TapGestureRecognizer).onTap?.call(); + + return false; + } + + return true; +} + +bool tapTextSpan(RichText richText, String text) { + final isTapped = !richText.text.visitChildren( + (visitor) => findTextAndTap(visitor, text), + ); + + return isTapped; +} diff --git a/lib/readmore.dart b/lib/readmore.dart index db52220..d237641 100644 --- a/lib/readmore.dart +++ b/lib/readmore.dart @@ -1,55 +1,166 @@ -library readmore; +import 'dart:ui' as ui show TextHeightBehavior; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -enum TrimMode { - Length, - Line, +enum TrimMode { Length, Line } + +/// Defines a customizable pattern within text, such as hashtags, URLs, or mentions. +/// +/// Enables applying custom styles and interactions to matched patterns, +/// enhancing text interactivity. Utilize this class to highlight specific text +/// segments or to add clickable functionality, facilitating navigation or other actions. +@immutable +class Annotation { + const Annotation({ + required this.regExp, + required this.spanBuilder, + }); + + final RegExp regExp; + final TextSpan Function({required String text, required TextStyle textStyle}) + spanBuilder; } class ReadMoreText extends StatefulWidget { const ReadMoreText( - this.data, { - Key key, + String this.data, { + super.key, + this.isCollapsed, + this.preDataText, + this.postDataText, + this.preDataTextStyle, + this.postDataTextStyle, this.trimExpandedText = 'show less', this.trimCollapsedText = 'read more', this.colorClickableText, this.trimLength = 240, this.trimLines = 2, this.trimMode = TrimMode.Length, + this.moreStyle, + this.lessStyle, + this.delimiter = '$_kEllipsis ', + this.delimiterStyle, + this.annotations, + this.isExpandable = true, this.style, + this.strutStyle, this.textAlign, this.textDirection, this.locale, - this.textScaleFactor, + this.softWrap, + this.overflow, + this.textScaler, this.semanticsLabel, + this.textWidthBasis, + this.textHeightBehavior, + this.selectionColor, + }) : richData = null, + richPreData = null, + richPostData = null; + + const ReadMoreText.rich( + TextSpan this.richData, { + super.key, + this.richPreData, + this.richPostData, + this.isCollapsed, + this.trimExpandedText = 'show less', + this.trimCollapsedText = 'read more', + this.colorClickableText, + this.trimLength = 240, + this.trimLines = 2, + this.trimMode = TrimMode.Length, this.moreStyle, this.lessStyle, - this.delimiter = '... ', + this.delimiter = '$_kEllipsis ', this.delimiterStyle, - this.callback, - }) : assert(data != null), - super(key: key); + this.isExpandable = true, + this.style, + this.strutStyle, + this.textAlign, + this.textDirection, + this.locale, + this.softWrap, + this.overflow, + this.textScaler, + this.semanticsLabel, + this.textWidthBasis, + this.textHeightBehavior, + this.selectionColor, + }) : data = null, + annotations = null, + preDataText = null, + postDataText = null, + preDataTextStyle = null, + postDataTextStyle = null; - final String delimiter; - final String data; - final String trimExpandedText; - final String trimCollapsedText; - final Color colorClickableText; + final ValueNotifier? isCollapsed; + + /// Used on TrimMode.Length final int trimLength; + + /// Used on TrimMode.Lines final int trimLines; + + /// Determines the type of trim. TrimMode.Length takes into account + /// the number of letters, while TrimMode.Lines takes into account + /// the number of lines final TrimMode trimMode; - final TextStyle style; - final TextAlign textAlign; - final TextDirection textDirection; - final Locale locale; - final double textScaleFactor; - final String semanticsLabel; - final TextStyle moreStyle; - final TextStyle lessStyle; - final TextStyle delimiterStyle; - final Function(bool val) callback; + + /// TextStyle for expanded text + final TextStyle? moreStyle; + + /// TextStyle for compressed text + final TextStyle? lessStyle; + + /// Textspan used before the data any heading or somthing + final String? preDataText; + + /// Textspan used after the data end or before the more/less + final String? postDataText; + + /// Textspan used before the data any heading or somthing + final TextStyle? preDataTextStyle; + + /// Textspan used after the data end or before the more/less + final TextStyle? postDataTextStyle; + + /// Rich version of [preDataText] + final TextSpan? richPreData; + + /// Rich version of [postDataText] + final TextSpan? richPostData; + + final List? annotations; + + /// Expand text on readMore press + final bool isExpandable; + + final String delimiter; + final String? data; + final TextSpan? richData; + final String trimExpandedText; + final String trimCollapsedText; + final Color? colorClickableText; + final TextStyle? delimiterStyle; + + // DefaultTextStyle start + + final TextStyle? style; + final StrutStyle? strutStyle; + final TextAlign? textAlign; + final TextDirection? textDirection; + final Locale? locale; + final bool? softWrap; + final TextOverflow? overflow; + final TextScaler? textScaler; + final String? semanticsLabel; + final TextWidthBasis? textWidthBasis; + final ui.TextHeightBehavior? textHeightBehavior; + final Color? selectionColor; + + // DefaultTextStyle end @override ReadMoreTextState createState() => ReadMoreTextState(); @@ -60,156 +171,329 @@ const String _kEllipsis = '\u2026'; const String _kLineSeparator = '\u2028'; class ReadMoreTextState extends State { - bool _readMore = true; + static final _nonCapturingGroupPattern = RegExp(r'\((?!\?:)'); + + final TapGestureRecognizer _recognizer = TapGestureRecognizer(); + + ValueNotifier? _isCollapsed; + ValueNotifier get _effectiveIsCollapsed => + widget.isCollapsed ?? (_isCollapsed ??= ValueNotifier(true)); + + void _onTap() { + if (widget.isExpandable) { + _effectiveIsCollapsed.value = !_effectiveIsCollapsed.value; + } + } + + RegExp? _mergeRegexPatterns(List? annotations) { + if (annotations == null || annotations.isEmpty) { + return null; + } else if (annotations.length == 1) { + return annotations[0].regExp; + } + + // replacing groups '(' => to non capturing groups '(?:' + return RegExp( + annotations + .map( + (a) => + '(${a.regExp.pattern.replaceAll(_nonCapturingGroupPattern, '(?:')})', + ) + .join('|'), + ); + } + + @override + void initState() { + super.initState(); + + _recognizer.onTap = _onTap; + } - void _onTapLink() { - setState((){ - _readMore = !_readMore; - widget.callback?.call(_readMore); - }); + @override + void didUpdateWidget(ReadMoreText oldWidget) { + super.didUpdateWidget(oldWidget); + + if (widget.isCollapsed == null && oldWidget.isCollapsed != null) { + final oldValue = oldWidget.isCollapsed!.value; + (_isCollapsed ??= ValueNotifier(oldValue)).value = oldValue; + } + } + + @override + void dispose() { + _recognizer.dispose(); + _isCollapsed?.dispose(); + + super.dispose(); } @override Widget build(BuildContext context) { - final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context); - TextStyle effectiveTextStyle = widget.style; - if (widget.style == null || widget.style.inherit) { + return ValueListenableBuilder( + valueListenable: _effectiveIsCollapsed, + builder: _builder, + ); + } + + Widget _builder(BuildContext context, bool isCollapsed, Widget? child) { + final defaultTextStyle = DefaultTextStyle.of(context); + TextStyle effectiveTextStyle; + if (widget.style == null || widget.style!.inherit) { effectiveTextStyle = defaultTextStyle.style.merge(widget.style); + } else { + effectiveTextStyle = widget.style!; + } + if (MediaQuery.boldTextOf(context)) { + effectiveTextStyle = effectiveTextStyle + .merge(const TextStyle(fontWeight: FontWeight.bold)); } + final registrar = SelectionContainer.maybeOf(context); + final textScaler = widget.textScaler ?? MediaQuery.textScalerOf(context); final textAlign = widget.textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start; final textDirection = widget.textDirection ?? Directionality.of(context); - final textScaleFactor = - widget.textScaleFactor ?? MediaQuery.textScaleFactorOf(context); - final overflow = defaultTextStyle.overflow; - final locale = - widget.locale ?? Localizations.localeOf(context, nullOk: true); + final locale = widget.locale ?? Localizations.maybeLocaleOf(context); + final softWrap = widget.softWrap ?? defaultTextStyle.softWrap; + final overflow = widget.overflow ?? defaultTextStyle.overflow; + final textWidthBasis = + widget.textWidthBasis ?? defaultTextStyle.textWidthBasis; + final textHeightBehavior = widget.textHeightBehavior ?? + defaultTextStyle.textHeightBehavior ?? + DefaultTextHeightBehavior.maybeOf(context); + final selectionColor = widget.selectionColor ?? + DefaultSelectionStyle.of(context).selectionColor ?? + DefaultSelectionStyle.defaultColor; + final colorClickableText = - widget.colorClickableText ?? Theme.of(context).accentColor; - final _defaultLessStyle = widget.lessStyle ?? + widget.colorClickableText ?? Theme.of(context).colorScheme.secondary; + final defaultLessStyle = widget.lessStyle ?? effectiveTextStyle.copyWith(color: colorClickableText); - final _defaultMoreStyle = widget.moreStyle ?? + final defaultMoreStyle = widget.moreStyle ?? effectiveTextStyle.copyWith(color: colorClickableText); - final _defaultDelimiterStyle = widget.delimiterStyle ?? effectiveTextStyle; + final defaultDelimiterStyle = widget.delimiterStyle ?? effectiveTextStyle; - TextSpan link = TextSpan( - text: _readMore ? widget.trimCollapsedText : widget.trimExpandedText, - style: _readMore ? _defaultMoreStyle : _defaultLessStyle, - recognizer: TapGestureRecognizer()..onTap = _onTapLink, + final link = TextSpan( + text: isCollapsed ? widget.trimCollapsedText : widget.trimExpandedText, + style: isCollapsed ? defaultMoreStyle : defaultLessStyle, + recognizer: _recognizer, ); - TextSpan _delimiter = TextSpan( - text: _readMore + final delimiter = TextSpan( + text: isCollapsed ? widget.trimCollapsedText.isNotEmpty ? widget.delimiter : '' - : widget.trimExpandedText.isNotEmpty - ? widget.delimiter - : '', - style: _defaultDelimiterStyle, - recognizer: TapGestureRecognizer()..onTap = _onTapLink, + : '', + style: defaultDelimiterStyle, + recognizer: _recognizer, ); Widget result = LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { assert(constraints.hasBoundedWidth); - final double maxWidth = constraints.maxWidth; + final maxWidth = constraints.maxWidth; + + TextSpan? preTextSpan; + TextSpan? postTextSpan; + + if (widget.richPreData != null) { + preTextSpan = widget.richPreData; + } else if (widget.preDataText != null) { + preTextSpan = TextSpan( + text: '${widget.preDataText!} ', + style: widget.preDataTextStyle ?? effectiveTextStyle, + ); + } + + if (widget.richPostData != null) { + postTextSpan = widget.richPostData; + } else if (widget.postDataText != null) { + postTextSpan = TextSpan( + text: ' ${widget.postDataText!}', + style: widget.postDataTextStyle ?? effectiveTextStyle, + ); + } + + final TextSpan dataTextSpan; + // Constructed by ReadMoreText.rich(...) + if (widget.richData != null) { + assert(_isTextSpan(widget.richData!)); + dataTextSpan = TextSpan( + style: effectiveTextStyle, + children: [widget.richData!], + ); + // Constructed by ReadMoreText(...) + } else { + dataTextSpan = _buildAnnotatedTextSpan( + data: widget.data!, + textStyle: effectiveTextStyle, + regExp: _mergeRegexPatterns(widget.annotations), + annotations: widget.annotations, + ); + } // Create a TextSpan with data final text = TextSpan( - style: effectiveTextStyle, - text: widget.data, + children: [ + if (preTextSpan != null) preTextSpan, + dataTextSpan, + if (postTextSpan != null) postTextSpan, + ], ); // Layout and measure link - TextPainter textPainter = TextPainter( + final textPainter = TextPainter( text: link, textAlign: textAlign, textDirection: textDirection, - textScaleFactor: textScaleFactor, - maxLines: widget.trimLines, - ellipsis: overflow == TextOverflow.ellipsis ? _kEllipsis : null, locale: locale, + textScaler: textScaler, + maxLines: widget.trimLines, + strutStyle: widget.strutStyle, + textWidthBasis: textWidthBasis, + textHeightBehavior: textHeightBehavior, + ellipsis: overflow == TextOverflow.ellipsis ? widget.delimiter : null, ); - textPainter.layout(minWidth: 0, maxWidth: maxWidth); + textPainter.layout(maxWidth: maxWidth); final linkSize = textPainter.size; + // Layout and measure delimiter + textPainter.text = delimiter; + textPainter.layout(maxWidth: maxWidth); + final delimiterSize = textPainter.size; + // Layout and measure text textPainter.text = text; textPainter.layout(minWidth: constraints.minWidth, maxWidth: maxWidth); final textSize = textPainter.size; // Get the endIndex of data - bool linkLongerThanLine = false; + var linkLongerThanLine = false; int endIndex; if (linkSize.width < maxWidth) { - final pos = textPainter.getPositionForOffset(Offset( - textSize.width - linkSize.width, - textSize.height, - )); - endIndex = textPainter.getOffsetBefore(pos.offset); + final readMoreSize = linkSize.width + delimiterSize.width; + final pos = textPainter.getPositionForOffset( + Offset( + textDirection == TextDirection.rtl + ? readMoreSize + : textSize.width - readMoreSize, + textSize.height, + ), + ); + endIndex = textPainter.getOffsetBefore(pos.offset) ?? 0; } else { - var pos = textPainter.getPositionForOffset( + final pos = textPainter.getPositionForOffset( textSize.bottomLeft(Offset.zero), ); endIndex = pos.offset; linkLongerThanLine = true; } - var textSpan; + late final TextSpan textSpan; switch (widget.trimMode) { case TrimMode.Length: - if (widget.trimLength < widget.data.length) { - textSpan = TextSpan( - style: effectiveTextStyle, - text: _readMore - ? widget.data.substring(0, widget.trimLength) - : widget.data, - children: [_delimiter, link], - ); - } else { - textSpan = TextSpan( - style: effectiveTextStyle, - text: widget.data, + // Constructed by ReadMoreText.rich(...) + if (widget.richData != null) { + final trimResult = _trimTextSpan( + textSpan: dataTextSpan, + spanStartIndex: 0, + endIndex: widget.trimLength, + splitByRunes: true, ); + + if (trimResult.didTrim) { + textSpan = TextSpan( + children: [ + if (isCollapsed) trimResult.textSpan else dataTextSpan, + delimiter, + link, + ], + ); + } else { + textSpan = dataTextSpan; + } + } + // Constructed by ReadMoreText(...) + else { + if (widget.trimLength < widget.data!.runes.length) { + final effectiveDataTextSpan = isCollapsed + ? _trimTextSpan( + textSpan: dataTextSpan, + spanStartIndex: 0, + endIndex: widget.trimLength, + splitByRunes: true, + ).textSpan + : dataTextSpan; + + textSpan = TextSpan( + children: [ + effectiveDataTextSpan, + delimiter, + link, + ], + ); + } else { + textSpan = dataTextSpan; + } } break; case TrimMode.Line: if (textPainter.didExceedMaxLines) { + final effectiveDataTextSpan = isCollapsed + ? _trimTextSpan( + textSpan: dataTextSpan, + spanStartIndex: 0, + endIndex: endIndex, + splitByRunes: false, + ).textSpan + : dataTextSpan; + textSpan = TextSpan( - style: effectiveTextStyle, - text: _readMore - ? widget.data.substring(0, endIndex) + - (linkLongerThanLine ? _kLineSeparator : '') - : widget.data, - children: [_delimiter, link], + children: [ + effectiveDataTextSpan, + if (linkLongerThanLine) const TextSpan(text: _kLineSeparator), + delimiter, + link, + ], ); } else { - textSpan = TextSpan( - style: effectiveTextStyle, - text: widget.data, - ); + textSpan = dataTextSpan; } break; - default: - throw Exception( - 'TrimMode type: ${widget.trimMode} is not supported'); } return RichText( + text: TextSpan( + children: [ + if (preTextSpan != null) preTextSpan, + textSpan, + if (postTextSpan != null) postTextSpan, + ], + ), textAlign: textAlign, textDirection: textDirection, - softWrap: true, - //softWrap, - overflow: TextOverflow.clip, - //overflow, - textScaleFactor: textScaleFactor, - text: textSpan, + locale: locale, + softWrap: softWrap, + overflow: overflow, + textScaler: textScaler, + strutStyle: widget.strutStyle, + textWidthBasis: textWidthBasis, + textHeightBehavior: textHeightBehavior, + selectionRegistrar: registrar, + selectionColor: selectionColor, ); }, ); + if (registrar != null) { + result = MouseRegion( + cursor: DefaultSelectionStyle.of(context).mouseCursor ?? + SystemMouseCursors.text, + child: result, + ); + } if (widget.semanticsLabel != null) { result = Semantics( textDirection: widget.textDirection, @@ -221,4 +505,167 @@ class ReadMoreTextState extends State { } return result; } + + TextSpan _buildAnnotatedTextSpan({ + required String data, + required TextStyle textStyle, + required RegExp? regExp, + required List? annotations, + }) { + if (regExp == null || data.isEmpty) { + return TextSpan(text: data, style: textStyle); + } + + final contents = []; + + data.splitMapJoin( + regExp, + onMatch: (Match regexMatch) { + final matchedText = regexMatch.group(0)!; + late final Annotation matchedAnnotation; + + if (annotations!.length == 1) { + matchedAnnotation = annotations[0]; + } else { + for (var i = 0; i < regexMatch.groupCount; i++) { + if (matchedText == regexMatch.group(i + 1)) { + matchedAnnotation = annotations[i]; + break; + } + } + } + + final content = matchedAnnotation.spanBuilder( + text: matchedText, + textStyle: textStyle, + ); + + assert(_isTextSpan(content)); + contents.add(content); + + return ''; + }, + onNonMatch: (String unmatchedText) { + contents.add(TextSpan(text: unmatchedText)); + return ''; + }, + ); + + return TextSpan(style: textStyle, children: contents); + } + + _TextSpanTrimResult _trimTextSpan({ + required TextSpan textSpan, + required int spanStartIndex, + required int endIndex, + required bool splitByRunes, + }) { + var spanEndIndex = spanStartIndex; + + final text = textSpan.text; + if (text != null) { + final textLen = splitByRunes ? text.runes.length : text.length; + spanEndIndex += textLen; + + if (spanEndIndex >= endIndex) { + final newText = splitByRunes + ? String.fromCharCodes(text.runes, 0, endIndex - spanStartIndex) + : text.substring(0, endIndex - spanStartIndex); + + final nextSpan = TextSpan( + text: newText, + children: null, // remove potential children + style: textSpan.style, + recognizer: textSpan.recognizer, + mouseCursor: textSpan.mouseCursor, + onEnter: textSpan.onEnter, + onExit: textSpan.onExit, + semanticsLabel: textSpan.semanticsLabel, + locale: textSpan.locale, + spellOut: textSpan.spellOut, + ); + + return _TextSpanTrimResult( + textSpan: nextSpan, + spanEndIndex: spanEndIndex, + didTrim: true, + ); + } + } + + var didTrim = false; + final newChildren = []; + + final children = textSpan.children; + if (children != null) { + for (final child in children) { + if (child is TextSpan) { + final result = _trimTextSpan( + textSpan: child, + spanStartIndex: spanEndIndex, + endIndex: endIndex, + splitByRunes: splitByRunes, + ); + + spanEndIndex = result.spanEndIndex; + newChildren.add(result.textSpan); + + if (result.didTrim) { + didTrim = true; + break; + } + } else { + // WidgetSpan shouldn't occur + newChildren.add(child); + } + } + } + + final resultTextSpan = didTrim + ? TextSpan( + text: textSpan.text, + children: newChildren, // update children + style: textSpan.style, + recognizer: textSpan.recognizer, + mouseCursor: textSpan.mouseCursor, + onEnter: textSpan.onEnter, + onExit: textSpan.onExit, + semanticsLabel: textSpan.semanticsLabel, + locale: textSpan.locale, + spellOut: textSpan.spellOut, + ) + : textSpan; + + return _TextSpanTrimResult( + textSpan: resultTextSpan, + spanEndIndex: spanEndIndex, + didTrim: didTrim, + ); + } + + bool _isTextSpan(InlineSpan span) { + if (span is! TextSpan) { + return false; + } + + final children = span.children; + if (children == null || children.isEmpty) { + return true; + } + + return children.every(_isTextSpan); + } +} + +@immutable +class _TextSpanTrimResult { + const _TextSpanTrimResult({ + required this.textSpan, + required this.spanEndIndex, + required this.didTrim, + }); + + final TextSpan textSpan; + final int spanEndIndex; + final bool didTrim; } diff --git a/pubspec.lock b/pubspec.lock deleted file mode 100644 index b308ac3..0000000 --- a/pubspec.lock +++ /dev/null @@ -1,146 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - async: - dependency: transitive - description: - name: async - url: "https://pub.dartlang.org" - source: hosted - version: "2.5.0-nullsafety.1" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0-nullsafety.1" - characters: - dependency: transitive - description: - name: characters - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0-nullsafety.3" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0-nullsafety.1" - clock: - dependency: transitive - description: - name: clock - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0-nullsafety.1" - collection: - dependency: transitive - description: - name: collection - url: "https://pub.dartlang.org" - source: hosted - version: "1.15.0-nullsafety.3" - fake_async: - dependency: transitive - description: - name: fake_async - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0-nullsafety.1" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - matcher: - dependency: transitive - description: - name: matcher - url: "https://pub.dartlang.org" - source: hosted - version: "0.12.10-nullsafety.1" - meta: - dependency: transitive - description: - name: meta - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0-nullsafety.3" - path: - dependency: transitive - description: - name: path - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.0-nullsafety.1" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.99" - source_span: - dependency: transitive - description: - name: source_span - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.0-nullsafety.2" - stack_trace: - dependency: transitive - description: - name: stack_trace - url: "https://pub.dartlang.org" - source: hosted - version: "1.10.0-nullsafety.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0-nullsafety.1" - string_scanner: - dependency: transitive - description: - name: string_scanner - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0-nullsafety.1" - term_glyph: - dependency: transitive - description: - name: term_glyph - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0-nullsafety.1" - test_api: - dependency: transitive - description: - name: test_api - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.19-nullsafety.2" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0-nullsafety.3" - vector_math: - dependency: transitive - description: - name: vector_math - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0-nullsafety.3" -sdks: - dart: ">=2.10.0-110 <2.11.0" diff --git a/pubspec.yaml b/pubspec.yaml index 1d46b28..ef1b673 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,52 +1,51 @@ name: readmore -description: A Flutter package than allow expand and collapse text dynamically -version: 1.0.1 +description: A Flutter package that allows for dynamic expansion and collapse of text, as well as interactions with text patterns such as hashtags, URLs, and mentions. +version: 3.1.0 homepage: https://github.com/jonataslaw/loadmore environment: - sdk: ">=2.1.0 <3.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: flutter: sdk: flutter - -dev_dependencies: - flutter_test: - sdk: flutter - # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter. -flutter: +# flutter: + +# To add assets to your package, add an assets section, like this: +# assets: +# - images/a_dot_burr.jpeg +# - images/a_dot_ham.jpeg +# +# For details regarding assets in packages, see +# https://flutter.dev/assets-and-images/#from-packages +# +# An image asset can refer to one or more resolution-specific "variants", see +# https://flutter.dev/assets-and-images/#resolution-aware. - # To add assets to your package, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # - # For details regarding assets in packages, see - # https://flutter.dev/assets-and-images/#from-packages - # - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. +# To add custom fonts to your package, add a fonts section here, +# in this "flutter" section. Each entry in this list should have a +# "family" key with the font family name, and a "fonts" key with a +# list giving the asset and other descriptors for the font. For +# example: +# fonts: +# - family: Schyler +# fonts: +# - asset: fonts/Schyler-Regular.ttf +# - asset: fonts/Schyler-Italic.ttf +# style: italic +# - family: Trajan Pro +# fonts: +# - asset: fonts/TrajanPro.ttf +# - asset: fonts/TrajanPro_Bold.ttf +# weight: 700 +# +# For details regarding fonts in packages, see +# https://flutter.dev/custom-fonts/#from-packages - # To add custom fonts to your package, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts in packages, see - # https://flutter.dev/custom-fonts/#from-packages +screenshots: + - description: 'This screenshot provides a preview of the readmore package.' + path: read-more-text-view-flutter.gif \ No newline at end of file diff --git a/read-more-text-view-flutter.gif b/read-more-text-view-flutter.gif index 28c3170..0b74ccf 100644 Binary files a/read-more-text-view-flutter.gif and b/read-more-text-view-flutter.gif differ diff --git a/readmore.iml b/readmore.iml deleted file mode 100644 index 675c1e7..0000000 --- a/readmore.iml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file