Merge pull request #3 from Iconica-Development/feature/melos-variant-flutter-introduction

Feature/melos variant flutter introduction
This commit is contained in:
Freek van de Ven 2023-11-29 13:28:01 +01:00 committed by GitHub
commit d4037160ce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
66 changed files with 2807 additions and 767 deletions

10
.github/dependabot.yaml vendored Normal file
View file

@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "pub"
directory: "/"
schedule:
interval: "daily"

View file

@ -0,0 +1,12 @@
name: Iconica Standard Melos CI Workflow
# Workflow Caller version: 1.0.0
on:
pull_request:
workflow_dispatch:
jobs:
call-global-iconica-workflow:
uses: Iconica-Development/.github/.github/workflows/melos-ci.yml@master
secrets: inherit
permissions: write-all

9
.gitignore vendored
View file

@ -31,4 +31,13 @@ build/
.metadata .metadata
pubspec.lock
pubspec_overrides.yaml
example/ios
example/web example/web
example/android
example/linux
example/macos

View file

@ -1,7 +1,7 @@
## 1.0.0 ## 2.0.0
* Update introduction_widget and introduction_service * Initial release of working flutter_introduction mono project.
## 0.0.1 ## 0.0.1
* Initial release. * Initial release of combined flutter_introduction melos project

194
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,194 @@
# Contributing
First off, thanks for taking the time to contribute! ❤️
All types of contributions are encouraged and valued.
See the [Table of Contents](#table-of-contents) for different ways to help and details about how we handle them.
Please make sure to read the relevant section before making your contribution.
It will make it a lot easier for us maintainers and smooth out the experience for all involved.
Iconica looks forward to your contributions. 🎉
## Table of contents
- [Code of conduct](#code-of-conduct)
- [I Have a Question](#i-have-a-question)
- [I Want To Contribute](#i-want-to-contribute)
- [Reporting Bugs](#reporting-bugs)
- [Contributing code](#contributing-code)
## Code of conduct
### Legal notice
When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license.
All accepted pull requests and other additions to this project will be considered intellectual property of Iconica.
All repositories should be kept clean of jokes, easter eggs and other unnecessary additions.
## I have a question
If you want to ask a question, we assume that you have read the available documentation found within the code.
Before you ask a question, it is best to search for existing issues that might help you.
In case you have found a suitable issue but still need clarification, you can ask your question
It is also advisable to search the internet for answers first.
If you then still feel the need to ask a question and need clarification, we recommend the following:
- Open an issue.
- Provide as much context as you can about what you're running into.
We will then take care of the issue as soon as possible.
## I want to contribute
### Reporting bugs
<!-- omit in toc -->
**Before submitting a bug report**
A good bug report shouldn't leave others needing to chase you up for more information.
Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report.
Please complete the following steps in advance to help us fix any potential bug as fast as possible.
- Make sure that you are using the latest version.
- Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (If you are looking for support, you might want to check [this section](#i-have-a-question)).
- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error.
- Also make sure to search the internet (including Stack Overflow) to see if users outside of Iconica have discussed the issue.
- Collect information about the bug:
- Stack trace (Traceback)
- OS, Platform and Version (Windows, Linux, macOS, x86, ARM)
- Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant.
- Time and date of occurance
- Describe the expected result and actual result
- Can you reliably reproduce the issue? And can you also reproduce it with older versions? Describe all steps that lead to the bug.
Once it's filed:
- The project team will label the issue accordingly.
- A team member will try to reproduce the issue with your provided steps.
If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for additional information.
- If the team is able to reproduce the issue, it will be moved into the backlog, as well as marked with a priority, and the issue will be left to be [implemented by someone](#contributing-code).
### Contributing code
When you start working on your contribution, make sure you are aware of the relevant documentation and the functionality of the component you are working on.
When writing code, follow the style guidelines set by Dart: [Effective Dart](https://Dart.dev/guides/language/effective-Dart). This contains most information you will need to write clean Dart code that is well documented.
**Documentation**
As Effective Dart indicates, documenting your public methods with Dart doc comments is recommended.
Aside from Effective Dart, we require specific information in the documentation of a method:
At the very least, your documentation should first name what the code does, then followed below by requirements for calling the method, the result of the method.
Any references to internal variables or other methods should be done through [var] to indicate a reference.
If the method or class is complex enough (determined by the reviewers) an example is required.
If unsure, add an example in the docs using code blocks.
For classes and methods, document the individual parameters with their implications.
> Tip: Remember that the shortest documentation can be written by having good descriptive names in the first place.
An example:
```Dart
library iconica_utilities.bidirectional_sorter;
part 'sorter.Dart';
part 'enum.Dart';
/// Generic sort method, allow sorting of list with primitives or complex types.
/// Uses [SortDirection] to determine the direction, either Ascending or Descending,
/// Gets called on [List] toSort of type [T] which cannot be shorter than 2.
/// Optionally for complex types a [Comparable] [Function] can be given to compare complex types.
/// ```
/// List<TestObject> objects = [];
/// for (int i = 0; i < 10; i++) {
/// objects.add(TestObject(name: "name", id: i));
/// }
///
/// sort<TestObject>(
/// SortDirection.descending, objects, (object) => object.id);
///
/// ```
/// In the above example a list of TestObjects is created, and then sorted in descending order.
/// If the implementation of TestObject is as following:
/// ```
/// class TestObject {
/// final String name;
/// final int id;
///
/// TestObject({required this.name, required this.id});
/// }
/// ```
/// And the list is logged to the console, the following will appear:
/// ```
/// [name9, name8, name7, name6, name5, name4, name3, name2, name1, name0]
/// ```
void sort<T>(
/// Determines the sorting direction, can be either Ascending or Descending
SortDirection sortDirection,
/// Incoming list, which gets sorted
List<T> toSort, [
/// Optional comparable, which is only necessary for complex types
SortFieldGetter<T>? sortValueCallback,
]) {
if (toSort.length < 2) return;
assert(
toSort.whereType<Comparable>().isNotEmpty || sortValueCallback != null);
BidirectionalSorter<T>(
sortInstructions: <SortInstruction<T>>[
SortInstruction(
sortValueCallback ?? (t) => t as Comparable, sortDirection),
],
).sort(toSort);
}
/// same functionality as [sort] but with the added functionality
/// of sorting multiple values
void sortMulti<T>(
/// Incoming list, which gets sorted
List<T> toSort,
/// list of comparables to sort multiple values at once,
/// priority based on index
List<SortInstruction<T>> sortValueCallbacks,
) {
if (toSort.length < 2) return;
assert(sortValueCallbacks.isNotEmpty);
BidirectionalSorter<T>(
sortInstructions: sortValueCallbacks,
).sort(toSort);
}
```
**Tests**
For each public method that was created, excluding widgets, which contains any form of logic (e.g. Calculations, predicates or major side-effects) tests are required.
A set of tests is written for each method, covering at least each path within the method. For example:
```Dart
void foo() {
try {
var bar = doSomething();
if (bar) {
doSomethingElse();
} else {
doSomethingCool();
}
} catch (_) {
displayError();
}
}
```
The method above should result in 3 tests:
1. A test for the path leading to displayError by the cause of an exception
2. A test for if bar is true, resulting in doSomethingElse()
3. A test for if bar is false, resulting in the doSomethingCool() method being called.
This means that we require 100% coverage of each method you test.

View file

@ -1,4 +1,4 @@
Copyright (c) 2022 Iconica, All rights reserved. Copyright (c) 2023 Iconica, All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

View file

@ -1,39 +1,39 @@
<!-- # Flutter Introduction
This README describes the package. If you publish this package to pub.dev,
this README's contents appear on the landing page for your package.
For information about how to write a good package README, see the guide for ![Introduction GIF](flutter_introduction_widget.gif)
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
For general information about developing packages, see the Dart guide for Monorepo for the Flutter introduction package. Including the following packages:
[creating packages](https://dart.dev/guides/libraries/create-library-packages) - Flutter Introduction
and the Flutter guide for Main package for Flutter Introduction including an example.
[developing packages and plugins](https://flutter.dev/developing-packages).
-->
TODO: Put a short description of the package here that helps potential users - Flutter Introduction Firebase
know whether this package might be useful for them. Package to provide content from firebase.
## Features - Flutter Introduction Interface
Interface regarding data for the Introduction widget, like whether to show the introduction or not.
TODO: List what your package can do. Maybe include images, gifs, or videos. - Flutter Introduction Service
Service to handle actions done in the Introduction widget.
## Getting started - Flutter Introduction Shared Preferences
Implementation of the interface with the use of shared preferences.
TODO: List prerequisites and provide or point to information on how to - Flutter Introduction Widget
start using the package. The actual widget showing the Introduction widget.
## Usage ## How to use
The simple way to use this package is by using the flutter_introduction package. An example is included if needed.
TODO: Include short and useful examples for package users. Add longer examples If needed a custom implementation can be made on the interface if the shared preferences doesn't suffice.
to `/example` folder.
```dart ## Issues
const like = 'sample';
```
## Additional information Please file any issues, bugs or feature request as an issue on our [GitHub](https://github.com/Iconica-Development/flutter_introduction/pulls) page. Commercial support is available if you need help with integration with your app or services. You can contact us at [support@iconica.nl](mailto:support@iconica.nl).
TODO: Tell users more about the package: where to find more information, how to ## Want to contribute
contribute to the package, how to file issues, what response they can expect
from the package authors, and more. If you would like to contribute to the plugin (e.g. by improving the documentation, solving a bug or adding a cool new feature), please carefully review our [contribution guide](../CONTRIBUTING.md) and send us your [pull request](https://github.com/Iconica-Development/flutter_introduction/pulls).
## Author
This `flutter_introduction` for Flutter is developed by [Iconica](https://iconica.nl). You can contact us at <support@iconica.nl>

View file

@ -1,4 +0,0 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View file

@ -1,100 +0,0 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
import 'package:flutter_introduction/flutter_introduction.dart';
import 'package:flutter_introduction_shared_preferences/flutter_introduction_shared_preferences.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
IntroductionService service =
IntroductionService(SharedPreferencesIntroductionDataProvider());
@override
Widget build(BuildContext context) {
return Scaffold(
body: Introduction(
options: IntroductionOptions(
pages: [
IntroductionPage(
title: const Text('First page'),
text: const Text('Wow a page'),
graphic: const FlutterLogo(),
),
IntroductionPage(
title: const Text('Second page'),
text: const Text('Another page'),
graphic: const FlutterLogo(),
),
IntroductionPage(
title: const Text('Third page'),
text: const Text('The final page of this app'),
graphic: const FlutterLogo(),
),
],
introductionTranslations: const IntroductionTranslations(
skipButton: 'Skip it!',
nextButton: 'Previous',
previousButton: 'Next',
finishButton: 'To the app!',
),
tapEnabled: true,
displayMode: IntroductionDisplayMode.multiPageHorizontal,
buttonMode: IntroductionScreenButtonMode.text,
indicatorMode: IndicatorMode.dash,
skippable: true,
buttonBuilder: (context, onPressed, child) =>
ElevatedButton(onPressed: onPressed, child: child),
),
service: service,
navigateTo: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) {
return const Home();
},
),
);
},
child: const Home(),
),
);
}
}
class Home extends StatelessWidget {
const Home({
super.key,
});
@override
Widget build(BuildContext context) {
return Container();
}
}

View file

@ -1,542 +0,0 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a
url: "https://pub.dev"
source: hosted
version: "61.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562
url: "https://pub.dev"
source: hosted
version: "5.13.0"
args:
dependency: transitive
description:
name: args
sha256: b003c3098049a51720352d219b0bb5f219b60fbfb68e7a4748139a06a5676515
url: "https://pub.dev"
source: hosted
version: "2.3.1"
async:
dependency: transitive
description:
name: async
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
url: "https://pub.dev"
source: hosted
version: "2.11.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
build:
dependency: transitive
description:
name: build
sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
built_collection:
dependency: transitive
description:
name: built_collection
sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
url: "https://pub.dev"
source: hosted
version: "5.1.1"
built_value:
dependency: transitive
description:
name: built_value
sha256: d7a9cd57c215bdf8d502772447aa6b52a8ab3f956d25d5fdea6ef1df2d2dad60
url: "https://pub.dev"
source: hosted
version: "8.4.1"
characters:
dependency: transitive
description:
name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
clock:
dependency: transitive
description:
name: clock
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
url: "https://pub.dev"
source: hosted
version: "1.1.1"
code_builder:
dependency: transitive
description:
name: code_builder
sha256: "02ce3596b459c666530f045ad6f96209474e8fee6e4855940a3cee65fb872ec5"
url: "https://pub.dev"
source: hosted
version: "4.3.0"
collection:
dependency: transitive
description:
name: collection
sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c"
url: "https://pub.dev"
source: hosted
version: "1.17.1"
convert:
dependency: transitive
description:
name: convert
sha256: "196284f26f69444b7f5c50692b55ec25da86d9e500451dc09333bf2e3ad69259"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
crypto:
dependency: transitive
description:
name: crypto
sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67
url: "https://pub.dev"
source: hosted
version: "3.0.2"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be
url: "https://pub.dev"
source: hosted
version: "1.0.5"
dart_style:
dependency: transitive
description:
name: dart_style
sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4"
url: "https://pub.dev"
source: hosted
version: "2.2.4"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
url: "https://pub.dev"
source: hosted
version: "1.3.1"
ffi:
dependency: transitive
description:
name: ffi
sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99
url: "https://pub.dev"
source: hosted
version: "2.0.2"
file:
dependency: transitive
description:
name: file
sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d"
url: "https://pub.dev"
source: hosted
version: "6.1.4"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: "04be3e934c52e082558cc9ee21f42f5c1cd7a1262f4c63cd0357c08d5bba81ec"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_data_interface:
dependency: transitive
description:
path: "."
ref: "1.0.0"
resolved-ref: "500ed1d08095b33387ae3aa4ed1a2ad4d2fb2ac3"
url: "https://github.com/Iconica-Development/flutter_data_interface.git"
source: git
version: "1.0.0"
flutter_introduction:
dependency: "direct main"
description:
path: ".."
relative: true
source: path
version: "1.0.0"
flutter_introduction_interface:
dependency: transitive
description:
path: "."
ref: "1.0.0"
resolved-ref: "2bb986c60a4ce7370a46c5db4cc3bc82a7f96884"
url: "https://github.com/Iconica-Development/flutter_introduction_interface.git"
source: git
version: "1.0.0"
flutter_introduction_service:
dependency: transitive
description:
path: "."
ref: "1.0.0"
resolved-ref: d8af4b73f1c951dd5fb72d24b07d854ee64a7ee1
url: "https://github.com/Iconica-Development/flutter_introduction_service.git"
source: git
version: "1.0.0"
flutter_introduction_shared_preferences:
dependency: "direct main"
description:
path: "."
ref: "1.0.0"
resolved-ref: fd976b68e0b44bc6fec7d6570f1e410a98ae3d61
url: "https://github.com/Iconica-Development/flutter_introduction_shared_preferences.git"
source: git
version: "1.0.0"
flutter_introduction_widget:
dependency: transitive
description:
path: "."
ref: "3.0.0"
resolved-ref: ae72ec10ea33eea5afbe62913992bf5215b4ad78
url: "https://github.com/Iconica-Development/flutter_introduction_widget.git"
source: git
version: "3.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c
url: "https://pub.dev"
source: hosted
version: "2.0.1"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: c51b4fdfee4d281f49b8c957f1add91b815473597f76bcf07377987f66a55729
url: "https://pub.dev"
source: hosted
version: "2.1.0"
js:
dependency: transitive
description:
name: js
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev"
source: hosted
version: "0.6.7"
lints:
dependency: transitive
description:
name: lints
sha256: "5cfd6509652ff5e7fe149b6df4859e687fca9048437857cb2e65c8d780f396e3"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
logging:
dependency: transitive
description:
name: logging
sha256: c0bbfe94d46aedf9b8b3e695cf3bd48c8e14b35e3b2c639e0aa7755d589ba946
url: "https://pub.dev"
source: hosted
version: "1.1.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb"
url: "https://pub.dev"
source: hosted
version: "0.12.15"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724
url: "https://pub.dev"
source: hosted
version: "0.2.0"
meta:
dependency: transitive
description:
name: meta
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
mockito:
dependency: transitive
description:
name: mockito
sha256: "8b46d7eb40abdda92d62edd01546051f0c27365e65608c284de336dccfef88cc"
url: "https://pub.dev"
source: hosted
version: "5.4.1"
package_config:
dependency: transitive
description:
name: package_config
sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
path:
dependency: transitive
description:
name: path
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
url: "https://pub.dev"
source: hosted
version: "1.8.3"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: ffbb8cc9ed2c9ec0e4b7a541e56fd79b138e8f47d2fb86815f15358a349b3b57
url: "https://pub.dev"
source: hosted
version: "2.1.11"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec"
url: "https://pub.dev"
source: hosted
version: "2.0.6"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: d3f80b32e83ec208ac95253e0cd4d298e104fbc63cb29c5c69edaed43b0c69d6
url: "https://pub.dev"
source: hosted
version: "2.1.6"
platform:
dependency: transitive
description:
name: platform
sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
process:
dependency: transitive
description:
name: process
sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09"
url: "https://pub.dev"
source: hosted
version: "4.2.4"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "816c1a640e952d213ddd223b3e7aafae08cd9f8e1f6864eed304cc13b0272b07"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
shared_preferences:
dependency: transitive
description:
name: shared_preferences
sha256: "16d3fb6b3692ad244a695c0183fca18cf81fd4b821664394a781de42386bf022"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "6478c6bbbecfe9aced34c483171e90d7c078f5883558b30ec3163cf18402c749"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: e014107bb79d6d3297196f4f2d0db54b5d1f85b8ea8ff63b8e8b391a02700feb
url: "https://pub.dev"
source: hosted
version: "2.2.2"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "9d387433ca65717bbf1be88f4d5bb18f10508917a8fa2fb02e0fd0d7479a9afa"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: fb5cf25c0235df2d0640ac1b1174f6466bd311f621574997ac59018a6664548d
url: "https://pub.dev"
source: hosted
version: "2.2.0"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: "74083203a8eae241e0de4a0d597dbedab3b8fef5563f33cf3c12d7e93c655ca5"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "5e588e2efef56916a3b229c3bfe81e6a525665a454519ca51dbcc4236a274173"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_gen:
dependency: transitive
description:
name: source_gen
sha256: "85f8c7d6425dff95475db618404732f034c87fe23efe05478cea50520a2517a3"
url: "https://pub.dev"
source: hosted
version: "1.2.5"
source_span:
dependency: transitive
description:
name: source_span
sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250
url: "https://pub.dev"
source: hosted
version: "1.9.1"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5
url: "https://pub.dev"
source: hosted
version: "1.11.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
url: "https://pub.dev"
source: hosted
version: "1.2.1"
test_api:
dependency: transitive
description:
name: test_api
sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb
url: "https://pub.dev"
source: hosted
version: "0.5.1"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5"
url: "https://pub.dev"
source: hosted
version: "1.3.1"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
watcher:
dependency: transitive
description:
name: watcher
sha256: e42dfcc48f67618344da967b10f62de57e04bae01d9d3af4c2596f3712a88c99
url: "https://pub.dev"
source: hosted
version: "1.0.1"
win32:
dependency: transitive
description:
name: win32
sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c"
url: "https://pub.dev"
source: hosted
version: "4.1.4"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1
url: "https://pub.dev"
source: hosted
version: "1.0.0"
yaml:
dependency: transitive
description:
name: yaml
sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
sdks:
dart: ">=3.0.0-0 <4.0.0"
flutter: ">=3.3.0"

View file

@ -1,34 +0,0 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. 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:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:example/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 MiB

39
melos.yaml Normal file
View file

@ -0,0 +1,39 @@
name: flutter_introduction
packages:
- packages/**
command:
version:
branch: master
scripts:
lint:all:
run: dart run melos run analyze && dart run melos run format-check
description: Run all static analysis checks.
get:
run: |
melos exec -c 1 -- "flutter pub get"
melos exec --scope="*example*" -c 1 -- "flutter pub get"
upgrade:
run: melos exec -c 1 -- "flutter pub upgrade"
create:
# run create in the example folder of flutter_introduction, flutter_introduction_firebase
run: melos exec --scope="*example*" -c 1 -- "flutter create ."
analyze:
run: |
dart run melos exec -c 1 -- \
flutter analyze --fatal-infos
description: Run `flutter analyze` for all packages.
format:
run: dart run melos exec dart format .
description: Run `dart format` for all packages.
format-check:
run: dart run melos exec dart format . --set-exit-if-changed
description: Run `dart format` checks for all packages.

View file

@ -0,0 +1,41 @@
# SPDX-FileCopyrightText: 2022 Iconica
#
# SPDX-License-Identifier: GPL-3.0-or-later
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# 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
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
# /pubspec.lock
**/doc/api/
.dart_tool/
.packages
build/
.flutter-plugins-dependencies
.flutter-plugins
.metadata
pubspec.lock
pubspec_overrides.yaml

View file

@ -0,0 +1,9 @@
include: package:flutter_iconica_analysis/analysis_options.yaml
# Possible to overwrite the rules from the package
analyzer:
exclude:
linter:
rules:

View file

@ -0,0 +1,9 @@
include: package:flutter_iconica_analysis/analysis_options.yaml
# Possible to overwrite the rules from the package
analyzer:
exclude:
linter:
rules:

View file

@ -0,0 +1,92 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
import 'package:flutter_introduction/flutter_introduction.dart';
import 'package:flutter_introduction_shared_preferences/flutter_introduction_shared_preferences.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) => MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
IntroductionService service =
IntroductionService(SharedPreferencesIntroductionDataProvider());
@override
Widget build(BuildContext context) => Scaffold(
body: Introduction(
options: IntroductionOptions(
pages: [
IntroductionPage(
title: const Text('First page'),
text: const Text('Wow a page'),
graphic: const FlutterLogo(),
),
IntroductionPage(
title: const Text('Second page'),
text: const Text('Another page'),
graphic: const FlutterLogo(),
),
IntroductionPage(
title: const Text('Third page'),
text: const Text('The final page of this app'),
graphic: const FlutterLogo(),
),
],
introductionTranslations: const IntroductionTranslations(
skipButton: 'Skip it!',
nextButton: 'Next',
previousButton: 'Previous',
finishButton: 'To the app!',
),
tapEnabled: true,
displayMode: IntroductionDisplayMode.multiPageHorizontal,
buttonMode: IntroductionScreenButtonMode.text,
indicatorMode: IndicatorMode.dash,
skippable: true,
buttonBuilder: (context, onPressed, child, type) =>
ElevatedButton(onPressed: onPressed, child: child),
),
service: service,
navigateTo: () async {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const Home(),
),
);
},
child: const Home(),
),
);
}
class Home extends StatelessWidget {
const Home({
super.key,
});
@override
Widget build(BuildContext context) => Container();
}

View file

@ -17,14 +17,19 @@ dependencies:
flutter_introduction: flutter_introduction:
path: ../ path: ../
flutter_introduction_shared_preferences: flutter_introduction_shared_preferences:
git: git:
url: https://github.com/Iconica-Development/flutter_introduction_shared_preferences.git url: https://github.com/Iconica-Development/flutter_introduction
ref: 1.0.0 ref: 2.0.0
path: packages/flutter_introduction_shared_preferences
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^2.0.0 flutter_lints: ^2.0.0
flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 6.0.0
flutter: flutter:
uses-material-design: true uses-material-design: true

View file

@ -2,14 +2,12 @@
// //
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
library flutter_introduction;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_introduction_service/flutter_introduction_service.dart'; import 'package:flutter_introduction_service/flutter_introduction_service.dart';
import 'package:flutter_introduction_widget/flutter_introduction_widget.dart'; import 'package:flutter_introduction_widget/flutter_introduction_widget.dart';
export 'package:flutter_introduction_widget/flutter_introduction_widget.dart';
export 'package:flutter_introduction_service/flutter_introduction_service.dart'; export 'package:flutter_introduction_service/flutter_introduction_service.dart';
export 'package:flutter_introduction_widget/flutter_introduction_widget.dart';
class Introduction extends StatefulWidget { class Introduction extends StatefulWidget {
const Introduction({ const Introduction({
@ -21,7 +19,7 @@ class Introduction extends StatefulWidget {
super.key, super.key,
}); });
final Function navigateTo; final VoidCallback navigateTo;
final IntroductionService? service; final IntroductionService? service;
final IntroductionOptions options; final IntroductionOptions options;
final ScrollPhysics? physics; final ScrollPhysics? physics;
@ -45,30 +43,29 @@ class _IntroductionState extends State<Introduction> {
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => FutureBuilder(
return FutureBuilder( // ignore: discarded_futures
future: _service.shouldShow(), future: _service.shouldShow(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.data == null || snapshot.data!) { if (snapshot.data == null || snapshot.data!) {
return IntroductionScreen( return IntroductionScreen(
options: widget.options, options: widget.options,
onComplete: () async { onComplete: () async {
_service.onComplete(); await _service.onComplete();
widget.navigateTo();
},
physics: widget.physics,
onSkip: () async {
await _service.onComplete();
widget.navigateTo();
},
);
} else {
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.navigateTo(); widget.navigateTo();
}, });
physics: widget.physics, return widget.child ?? const CircularProgressIndicator();
onSkip: () async { }
_service.onComplete(); },
widget.navigateTo(); );
},
);
} else {
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.navigateTo();
});
return widget.child ?? const CircularProgressIndicator();
}
},
);
}
} }

View file

@ -0,0 +1,33 @@
name: flutter_introduction
description: Combined Package of Flutter Introduction Widget and Flutter Introduction Service
version: 2.0.0
publish_to: none
environment:
sdk: ">=2.18.0 <3.0.0"
flutter: ">=1.17.0"
dependencies:
flutter:
sdk: flutter
flutter_introduction_widget:
git:
url: https://github.com/Iconica-Development/flutter_introduction
ref: 2.0.0
path: packages/flutter_introduction_widget
flutter_introduction_service:
git:
url: https://github.com/Iconica-Development/flutter_introduction
ref: 2.0.0
path: packages/flutter_introduction_service
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 6.0.0
flutter:

View file

@ -0,0 +1,41 @@
# SPDX-FileCopyrightText: 2022 Iconica
#
# SPDX-License-Identifier: GPL-3.0-or-later
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# 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
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
# /pubspec.lock
**/doc/api/
.dart_tool/
.packages
build/
.flutter-plugins-dependencies
.flutter-plugins
.metadata
pubspec.lock
pubspec_overrides.yaml

View file

@ -0,0 +1,9 @@
include: package:flutter_iconica_analysis/analysis_options.yaml
# Possible to overwrite the rules from the package
analyzer:
exclude:
linter:
rules:

View file

@ -0,0 +1,9 @@
// SPDX-FileCopyrightText: 2023 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
///
library flutter_introduction_firebase;
export 'src/firebase_service.dart';
export 'src/introduction_widget.dart';

View file

@ -0,0 +1,75 @@
// SPDX-FileCopyrightText: 2023 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:cached_network_image/cached_network_image.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:flutter_introduction_firebase/src/introduction_page.dart';
const _introductionDocumentRef = 'introduction/introduction';
class FirebaseIntroductionService {
FirebaseIntroductionService({
DocumentReference<Map<String, dynamic>>? documentRef,
}) : _documentRef = documentRef ??
FirebaseFirestore.instance.doc(_introductionDocumentRef);
final DocumentReference<Map<String, dynamic>> _documentRef;
List<IntroductionPageData> _pages = [];
Future<List<IntroductionPageData>> getIntroductionPages() async {
if (_pages.isNotEmpty) return _pages;
var pagesDocuments =
await _documentRef.collection('pages').orderBy('order').get();
return _pages = pagesDocuments.docs.map((document) {
var data = document.data();
// convert Map<String, dynamic> to Map<String, String>
var title = data['title'] != null
? (data['title'] as Map<String, dynamic>).cast<String, String>()
: <String, String>{};
var content = data['content'] != null
? (data['content'] as Map<String, dynamic>).cast<String, String>()
: <String, String>{};
return IntroductionPageData(
title: title,
content: content,
contentImage: data['image'] as String?,
backgroundImage: data['background_image'] as String?,
// the color is stored as a hex string
backgroundColor: data['background_color'] != null
? Color(int.parse(data['background_color'] as String, radix: 16))
: null,
);
}).toList();
}
Future<bool> introductionIsDisabled() async {
var document = await _documentRef.get();
return document.data()!['disabled'] as bool? ?? false;
}
Future<bool> shouldAlwaysShowIntroduction() async {
var document = await _documentRef.get();
return document.data()!['always_show'] as bool? ?? false;
}
Future<void> loadIntroductionPages(
BuildContext context,
) async {
for (var page in _pages) {
if (context.mounted && page.backgroundImage != null) {
await precacheImage(
CachedNetworkImageProvider(page.backgroundImage!),
context,
);
}
if (context.mounted && page.contentImage != null) {
await precacheImage(
CachedNetworkImageProvider(page.contentImage!),
context,
);
}
}
}
}

View file

@ -0,0 +1,32 @@
// SPDX-FileCopyrightText: 2023 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
@immutable
class IntroductionPageData {
const IntroductionPageData({
required this.title,
required this.content,
this.contentImage,
this.backgroundImage,
this.backgroundColor,
});
/// The title of the introduction page in different languages
final Map<String, String> title;
/// The content of the introduction page in different languages
final Map<String, String> content;
/// The imageUrl of the graphic on the introduction page
final String? contentImage;
/// The imageUrl of the background image of the introduction page
final String? backgroundImage;
/// Optional background color of the introduction page
/// (defaults to transparent)
final Color? backgroundColor;
}

View file

@ -0,0 +1,184 @@
// SPDX-FileCopyrightText: 2023 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
// ignore_for_file: discarded_futures
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_introduction_firebase/flutter_introduction_firebase.dart';
import 'package:flutter_introduction_service/flutter_introduction_service.dart';
export 'package:flutter_introduction_firebase/src/introduction_page.dart';
export 'package:flutter_introduction_widget/flutter_introduction_widget.dart';
class IntroductionFirebase extends StatefulWidget {
const IntroductionFirebase({
required this.options,
required this.onComplete,
this.decoration,
this.layoutStyle,
this.titleBuilder,
this.contentBuilder,
this.imageBuilder,
this.onSkip,
this.firebaseService,
this.introductionService,
this.physics,
this.child,
this.languageCodeOverride,
super.key,
});
/// The options used to build the introduction screen
final IntroductionOptions options;
/// The service used to determine if the introduction screen should be shown
final IntroductionService? introductionService;
/// The service used to get the introduction pages
final FirebaseIntroductionService? firebaseService;
/// A function called when the introductionSceen changes
final VoidCallback onComplete;
/// A function called when the introductionScreen is skipped
final VoidCallback? onSkip;
/// How the single child scroll view should respond to scrolling
final ScrollPhysics? physics;
/// The widget to show when the introduction screen is loading
final Widget? child;
/// The decoration of an introduction page if it doesn't have
/// a backgroundImage or backgroundColor
final BoxDecoration? decoration;
/// The layout style of all the introduction pages
final IntroductionLayoutStyle? layoutStyle;
/// The builder used to build the title of the introduction page
final Widget Function(String)? titleBuilder;
/// The builder used to build the content of the introduction page
final Widget Function(String)? contentBuilder;
/// The builder used to build the image of the introduction page
final Widget Function(String)? imageBuilder;
/// Use this to override the language code that is in the context
/// used for showing the introduction in a different language
final String? languageCodeOverride;
@override
State<IntroductionFirebase> createState() => _IntroductionState();
}
class _IntroductionState extends State<IntroductionFirebase> {
late IntroductionService _service;
late FirebaseIntroductionService _firebaseService;
@override
void initState() {
super.initState();
if (widget.introductionService == null) {
_service = IntroductionService();
} else {
_service = widget.introductionService!;
}
if (widget.firebaseService == null) {
_firebaseService = FirebaseIntroductionService();
} else {
_firebaseService = widget.firebaseService!;
}
}
@override
Widget build(BuildContext context) {
Future<bool> shouldShow() async =>
!await _firebaseService.introductionIsDisabled() &&
(await _service.shouldShow() ||
await _firebaseService.shouldAlwaysShowIntroduction());
var languageCode = widget.languageCodeOverride ??
Localizations.localeOf(context).languageCode;
return FutureBuilder(
future: shouldShow(),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data != null && snapshot.data!) {
return FutureBuilder(
future: _firebaseService.getIntroductionPages(),
builder: (context, snapshot) {
if (snapshot.hasData &&
snapshot.data != null &&
snapshot.data is List<IntroductionPageData>) {
return IntroductionScreen(
options: widget.options.copyWith(
pages: snapshot.data?.map(
(e) {
var title = e.title.isEmpty
? ''
: e.title.containsKey(languageCode)
? e.title[languageCode]!
: e.title.values.first;
var content = e.content.isEmpty
? ''
: e.content.containsKey(languageCode)
? e.content[languageCode]!
: e.content.values.first;
return IntroductionPage(
title:
widget.titleBuilder?.call(title) ?? Text(title),
graphic: e.contentImage != null &&
e.contentImage!.isNotEmpty
? widget.imageBuilder?.call(e.contentImage!) ??
CachedNetworkImage(imageUrl: e.contentImage!)
: null,
text: widget.contentBuilder?.call(content) ??
Text(content),
decoration: widget.decoration?.copyWith(
color: e.backgroundColor,
image: e.backgroundImage != null &&
e.backgroundImage!.isNotEmpty
? DecorationImage(
image: CachedNetworkImageProvider(
e.backgroundImage!,
),
fit: BoxFit.cover,
)
: null,
),
layoutStyle: widget.layoutStyle,
);
},
).toList(),
),
onComplete: () async {
await _service.onComplete();
widget.onComplete();
},
physics: widget.physics,
onSkip: () async {
await _service.onSkip();
widget.onComplete();
},
);
} else {
return widget.child ?? const CircularProgressIndicator();
}
},
);
} else {
if (snapshot.hasData && snapshot.data != null && !snapshot.data!) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
await _service.onComplete();
widget.onComplete();
});
}
return widget.child ?? const CircularProgressIndicator();
}
},
);
}
}

View file

@ -0,0 +1,34 @@
name: flutter_introduction_firebase
description: Flutter Introduction Page that uses firebase for the pages and some settings
version: 2.0.0
publish_to: none
environment:
sdk: ">=3.1.5 <4.0.0"
dependencies:
flutter:
sdk: flutter
cloud_firestore: ^4.12.2
cached_network_image: ^3.3.0
flutter_introduction_widget:
git:
url: https://github.com/Iconica-Development/flutter_introduction
ref: 2.0.0
path: packages/flutter_introduction_widget
flutter_introduction_service:
git:
url: https://github.com/Iconica-Development/flutter_introduction
ref: 2.0.0
path: packages/flutter_introduction_service
dev_dependencies:
flutter_test:
sdk: flutter
flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 6.0.0
flutter:

View file

@ -0,0 +1,11 @@
// SPDX-FileCopyrightText: 2023 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter_test/flutter_test.dart';
void main() {
test('test', () {
expect(true, true);
});
}

View file

@ -0,0 +1,32 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# 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
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.packages
build/
.metadata

View file

@ -0,0 +1,9 @@
include: package:flutter_iconica_analysis/analysis_options.yaml
# Possible to overwrite the rules from the package
analyzer:
exclude:
linter:
rules:

View file

@ -0,0 +1,6 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
export './src/introduction_interface.dart';
export './src/local_introduction.dart';

View file

@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter_data_interface/flutter_data_interface.dart';
import 'package:flutter_introduction_interface/src/local_introduction.dart';
abstract class IntroductionInterface extends DataInterface {
IntroductionInterface() : super(token: _token);
static final Object _token = Object();
static IntroductionInterface _instance = LocalIntroductionDataProvider();
static IntroductionInterface get instance => _instance;
static set instance(IntroductionInterface instance) {
DataInterface.verify(instance, _token);
_instance = instance;
}
Future<void> setCompleted({bool value = true});
Future<bool> shouldShow();
}

View file

@ -0,0 +1,19 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter_introduction_interface/src/introduction_interface.dart';
class LocalIntroductionDataProvider extends IntroductionInterface {
LocalIntroductionDataProvider();
bool hasViewed = false;
@override
Future<void> setCompleted({bool value = true}) async {
hasViewed = value;
}
@override
Future<bool> shouldShow() async => hasViewed;
}

View file

@ -0,0 +1,27 @@
name: flutter_introduction_interface
description: A new Flutter package project.
version: 2.0.0
publish_to: none
environment:
sdk: '>=2.18.0 <3.0.0'
flutter: ">=1.17.0"
dependencies:
flutter:
sdk: flutter
flutter_data_interface:
git:
url: https://github.com/Iconica-Development/flutter_data_interface.git
ref: 1.0.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 6.0.0
flutter:

View file

@ -0,0 +1,32 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# 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
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.packages
build/
.metadata

View file

@ -0,0 +1,9 @@
include: package:flutter_iconica_analysis/analysis_options.yaml
# Possible to overwrite the rules from the package
analyzer:
exclude:
linter:
rules:

View file

@ -0,0 +1,7 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
export 'package:flutter_introduction_interface/flutter_introduction_interface.dart';
export './src/introduction_service.dart';

View file

@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter_introduction_interface/flutter_introduction_interface.dart';
class IntroductionService {
IntroductionService([IntroductionInterface? dataProvider])
: _dataProvider = dataProvider ?? LocalIntroductionDataProvider();
late final IntroductionInterface _dataProvider;
Future<void> onSkip() => _dataProvider.setCompleted(value: true);
Future<void> onComplete() => _dataProvider.setCompleted(value: true);
Future<bool> shouldShow() => _dataProvider.shouldShow();
}

View file

@ -0,0 +1,28 @@
name: flutter_introduction_service
description: A new Flutter package project.
version: 2.0.0
publish_to: none
environment:
sdk: '>=2.18.0 <3.0.0'
flutter: ">=1.17.0"
dependencies:
flutter:
sdk: flutter
flutter_introduction_interface:
git:
url: https://github.com/Iconica-Development/flutter_introduction
ref: 2.0.0
path: packages/flutter_introduction_interface
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 6.0.0
flutter:

View file

@ -0,0 +1,34 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# 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
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.packages
build/
.metadata
.flutter-plugins
.flutter-plugins-dependencies

View file

@ -0,0 +1,9 @@
include: package:flutter_iconica_analysis/analysis_options.yaml
# Possible to overwrite the rules from the package
analyzer:
exclude:
linter:
rules:

View file

@ -0,0 +1,33 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter_introduction_interface/flutter_introduction_interface.dart';
import 'package:shared_preferences/shared_preferences.dart';
class SharedPreferencesIntroductionDataProvider extends IntroductionInterface {
SharedPreferencesIntroductionDataProvider();
SharedPreferences? _prefs;
String key = '_completedIntroduction';
Future<void> _writeKeyValue(String key, bool value) async {
await _prefs!.setBool(key, value);
}
Future<void> _init() async {
_prefs ??= await SharedPreferences.getInstance();
}
@override
Future<void> setCompleted({bool value = true}) async {
await _init();
await _writeKeyValue(key, value);
}
@override
Future<bool> shouldShow() async {
await _init();
return !(_prefs!.getBool(key) ?? false);
}
}

View file

@ -0,0 +1,29 @@
name: flutter_introduction_shared_preferences
description: A new Flutter package project.
version: 2.0.0
publish_to: none
environment:
sdk: '>=2.18.0 <3.0.0'
flutter: ">=1.17.0"
dependencies:
flutter:
sdk: flutter
flutter_introduction_interface:
git:
url: https://github.com/Iconica-Development/flutter_introduction
ref: 2.0.0
path: packages/flutter_introduction_interface
shared_preferences: any
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 6.0.0
flutter:

View file

@ -0,0 +1,43 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# 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
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.packages
build/
example/android/
example/ios/
example/macos/
example/web/
example/windows/
example/linux/
example/.metadata
example/pubspec.lock
.flutter-plugins
.flutter-plugins-dependencies
.metadata

View file

@ -0,0 +1,65 @@
[![pub package](https://img.shields.io/pub/v/flutter_introduction_widget.svg)](https://github.com/Iconica-Development) [![Build status](https://img.shields.io/github/workflow/status/Iconica-Development/flutter_introduction_widget/CI)](https://github.com/Iconica-Development/flutter_introduction_widget/actions/new) [![style: effective dart](https://img.shields.io/badge/style-effective_dart-40c4ff.svg)](https://github.com/tenhobi/effective_dart)
# Introduction Widget
Flutter Introduction Widget for showing a list of introduction pages on a single scrollable page or horizontal pageview.
![Introduction GIF](../../flutter_introduction_widget.gif)
## Setup
To use this package, add `flutter_introduction_widget` as a dependency in your pubspec.yaml file.
## How to use
Simple way to use the introduction widget:
```dart
IntroductionScreen(
options: IntroductionOptions(
pages: [
IntroductionPage(
title: const Text('First page'),
text: const Text('Wow a page'),
graphic: const FlutterLogo(),
),
IntroductionPage(
title: const Text('Second page'),
text: const Text('Another page'),
graphic: const FlutterLogo(),
),
IntroductionPage(
title: const Text('Third page'),
text: const Text('The final page of this app'),
graphic: const FlutterLogo(),
backgroundImage: const AssetImage(
'assets/flutter_introduction_background.jpeg'),
),
],
introductionTranslations: const IntroductionTranslations(
skipButton: 'Skip it!',
nextButton: 'Next',
previousButton: 'Previous',
finishButton: 'Finish',
),
buttonMode: IntroductionScreenButtonMode.text,
buttonBuilder: (context, onPressed, child) =>
ElevatedButton(onPressed: onPressed, child: child),
),
onComplete: () {
debugPrint('We completed the cycle');
},
),
```
See the [Example Code](example/lib/main.dart) for an example on how to use this package.
## Issues
Please file any issues, bugs or feature request as an issue on our [GitHub](https://github.com/Iconica-Development/flutter_introduction_widget) page. Commercial support is available if you need help with integration with your app or services. You can contact us at [support@iconica.nl](mailto:support@iconica.nl).
## Want to contribute
If you would like to contribute to the plugin (e.g. by improving the documentation, solving a bug or adding a cool new feature), please carefully review our [contribution guide](./CONTRIBUTING.md) and send us your [pull request](https://github.com/Iconica-Development/flutter_introduction_widget/pulls).
## Author
This `flutter_introduction_widget` for Flutter is developed by [Iconica](https://iconica.nl). You can contact us at <support@iconica.nl>

View file

@ -0,0 +1,9 @@
include: package:flutter_iconica_analysis/analysis_options.yaml
# Possible to overwrite the rules from the package
analyzer:
exclude:
linter:
rules:

View file

@ -0,0 +1,44 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# 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/
# 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View file

@ -0,0 +1,116 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
import 'package:flutter_introduction_widget/flutter_introduction_widget.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
),
home: IntroductionScreen(
options: IntroductionOptions(
pages: [
IntroductionPage(
title: const Text('Basic Page'),
text: const Text(
'A page with some text and a widget in the middle.',
),
graphic: const FlutterLogo(size: 100),
),
IntroductionPage(
title: const Text('Layout Shift'),
text: const Text(
'You can change the layout of a page to mix things up.',
),
graphic: const FlutterLogo(size: 100),
layoutStyle: IntroductionLayoutStyle.imageTop,
),
IntroductionPage(
title: const Text(
'Decoration',
style: TextStyle(
color: Colors.white,
),
),
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topRight,
end: Alignment.bottomLeft,
colors: [
Colors.yellow,
Colors.red,
Colors.indigo,
Colors.teal,
],
),
),
text: const Text(
'Add a Decoration to make a custom background, like a LinearGradient',
style: TextStyle(
color: Colors.white,
),
),
graphic: const FlutterLogo(
size: 100,
),
),
IntroductionPage(
title: const Text(
'Background Image',
),
text: const Text(
'Add a Decoration with a DecorationImage, to add an background image',
),
decoration: const BoxDecoration(
image: DecorationImage(
fit: BoxFit.cover,
image: AssetImage(
'assets/flutter_introduction_background.jpeg',
),
),
),
),
],
introductionTranslations: const IntroductionTranslations(
skipButton: 'Skip it!',
nextButton: 'Next',
previousButton: 'Previous',
finishButton: 'Finish',
),
tapEnabled: true,
displayMode: IntroductionDisplayMode.multiPageHorizontal,
buttonMode: IntroductionScreenButtonMode.text,
indicatorMode: IndicatorMode.dash,
skippable: true,
buttonBuilder: (context, onPressed, child, buttonType) =>
ElevatedButton(onPressed: onPressed, child: child),
),
onComplete: () {
debugPrint('We completed the cycle');
},
),
);
}
}

View file

@ -0,0 +1,60 @@
name: example_widget
description: A new Flutter project.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1
environment:
sdk: '>=2.18.1 <3.0.0'
dependencies:
flutter:
sdk: flutter
flutter_introduction_widget:
path: ../
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
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 application, add an assets section, like this:
assets:
- assets/
# - 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
# 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
# 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 from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages

View file

@ -0,0 +1,6 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
export 'src/config/introduction.dart';
export 'src/introduction.dart';

View file

@ -0,0 +1,246 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
enum IntroductionScreenMode { showNever, showAlways, showOnce }
enum IntroductionScreenButtonMode { text, icon, disabled, singleFinish }
enum IntroductionLayoutStyle {
imageCenter,
imageTop,
imageBottom,
}
enum IndicatorMode { dot, dash, custom }
enum IntroductionDisplayMode {
singleScrollablePageVertical,
multiPageHorizontal
}
enum IntroductionControlMode {
previousNextButton,
singleButton,
}
enum IntroductionButtonType {
next,
previous,
finish,
skip,
}
class IntroductionPage {
/// Creates an introduction page with data used in the introduction screen for
/// each page.
///
/// The values for [title] and [text] are optional in this, but will use a
/// default translation key when built.
///
/// The [background] is fully optional and if not provided will show the
/// [ThemeData.colorScheme.background] as default.
IntroductionPage({
this.title,
this.text,
this.graphic,
this.decoration,
this.layoutStyle,
});
final Widget? title;
final Widget? text;
final Widget? graphic;
final BoxDecoration? decoration;
final IntroductionLayoutStyle? layoutStyle;
}
class IntroductionOptions {
const IntroductionOptions({
this.introductionTranslations = const IntroductionTranslations(),
this.indicatorMode = IndicatorMode.dash,
this.indicatorBuilder,
this.layoutStyle = IntroductionLayoutStyle.imageCenter,
this.pages = const [],
this.buttonMode = IntroductionScreenButtonMode.disabled,
this.tapEnabled = false,
this.mode = IntroductionScreenMode.showNever,
this.textAlign = TextAlign.center,
this.displayMode = IntroductionDisplayMode.multiPageHorizontal,
this.skippable = false,
this.buttonBuilder,
this.controlMode = IntroductionControlMode.previousNextButton,
}) : assert(
!(identical(indicatorMode, IndicatorMode.custom) &&
indicatorBuilder == null),
'When indicator mode is set to custom, '
'make sure to define indicatorBuilder',
);
/// Determine when the introduction screens needs to be shown.
///
/// [IntroductionScreenMode.showNever] To disable introduction screens.
///
/// [IntroductionScreenMode.showAlways] To always show the introduction
/// screens on startup.
///
/// [IntroductionScreenMode.showOnce] To only show the introduction screens
/// once on startup.
final IntroductionScreenMode mode;
/// List of introduction pages to set the text, icons or images for the
/// introduction screens.
final List<IntroductionPage> pages;
/// Determines whether the user can tap the screen to go to the next
/// introduction screen.
final bool tapEnabled;
/// Determines what kind of buttons are used to navigate to the next
/// introduction screen.
/// Introduction screens can always be navigated by swiping (or tapping if
/// [tapEnabled] is enabled).
///
/// [IntroductionScreenButtonMode.text] Use text buttons (text can be set by
/// setting the translation key or using the default appshell translations).
///
/// [IntroductionScreenButtonMode.icon] Use icon buttons (icons can be
/// changed by providing a icon library)
///
/// [IntroductionScreenButtonMode.disabled] Disable buttons.
final IntroductionScreenButtonMode buttonMode;
/// Determines the position of the provided images or icons that are set
/// using [pages].
/// Every introduction page provided with a image or icon will use the same
/// layout setting.
///
/// [IntroductionLayoutStyle.imageCenter] Image/icon will be at the center of the introduction page.
///
/// [IntroductionLayoutStyle.imageTop] Image/icon will be at the top of the introduction page.
///
/// [IntroductionLayoutStyle.imageBottom] Image/icon will be at the bottom of the introduction page.
final IntroductionLayoutStyle layoutStyle;
/// Determines the style of the page indicator shown at the bottom on the
/// introduction pages.
///
/// [IndicatorMode.dot] Shows a dot for each page.
///
/// [IndicatorMode.dash] Shows a dash for each page.
///
/// [IndicatorMode.custom] calls indicatorBuilder for the indicator
final IndicatorMode indicatorMode;
/// Builder that is called when [indicatorMode] is set
/// to [IndicatorMode.custom]
final Widget Function(
BuildContext,
PageController,
int,
int,
)? indicatorBuilder;
/// Determines whether the user can skip the introduction pages using a button
/// in the header
final bool skippable;
/// Determines whether the introduction screens should be shown in a single
final TextAlign textAlign;
/// [IntroductionDisplayMode.multiPageHorizontal] Configured introduction
/// pages will be shown on seperate screens and can be navigated using using
/// buttons (if enabled) or swiping.
///
/// !Unimplemented! [IntroductionDisplayMode.singleScrollablePageVertical]
/// All configured introduction pages will be shown on a single scrollable
/// page.
///
final IntroductionDisplayMode displayMode;
/// When [IntroductionDisplayMode.multiPageHorizontal] is selected multiple
/// controlMode can be selected.
///
/// [IntroductionControlMode.previousNextButton] shows two buttons at the
/// bottom of the screen to return or proceed. The skip button is placed at
/// the top left of the screen.
///
/// [IntroductionControlMode.singleButton] contains one button at the bottom
/// of the screen to proceed. Underneath is clickable text to skip if the
/// current page is the first page. If the current page is any different it
/// return to the previous screen.
///
final IntroductionControlMode controlMode;
/// A builder that can be used to replace the default text buttons when
/// [IntroductionScreenButtonMode.text] is provided to [buttonMode]
final Widget Function(
BuildContext,
VoidCallback,
Widget,
IntroductionButtonType,
)? buttonBuilder;
/// The translations for all buttons on the introductionpages
///
/// See [IntroductionTranslations] for more information
/// The following buttons have a translation:
/// - Skip
/// - Next
/// - Previous
/// - Finish
final IntroductionTranslations introductionTranslations;
IntroductionOptions copyWith({
IntroductionScreenMode? mode,
List<IntroductionPage>? pages,
bool? tapEnabled,
IntroductionScreenButtonMode? buttonMode,
IntroductionLayoutStyle? layoutStyle,
IndicatorMode? indicatorMode,
Widget Function(
BuildContext,
PageController,
int,
int,
)? indicatorBuilder,
bool? skippable,
TextAlign? textAlign,
IntroductionDisplayMode? displayMode,
IntroductionControlMode? controlMode,
Widget Function(BuildContext, VoidCallback, Widget, IntroductionButtonType)?
buttonBuilder,
IntroductionTranslations? introductionTranslations,
}) =>
IntroductionOptions(
mode: mode ?? this.mode,
pages: pages ?? this.pages,
tapEnabled: tapEnabled ?? this.tapEnabled,
buttonMode: buttonMode ?? this.buttonMode,
layoutStyle: layoutStyle ?? this.layoutStyle,
indicatorMode: indicatorMode ?? this.indicatorMode,
indicatorBuilder: indicatorBuilder ?? this.indicatorBuilder,
skippable: skippable ?? this.skippable,
textAlign: textAlign ?? this.textAlign,
displayMode: displayMode ?? this.displayMode,
controlMode: controlMode ?? this.controlMode,
buttonBuilder: buttonBuilder ?? this.buttonBuilder,
introductionTranslations:
introductionTranslations ?? this.introductionTranslations,
);
}
///
class IntroductionTranslations {
const IntroductionTranslations({
this.skipButton = 'skip',
this.nextButton = 'next',
this.previousButton = 'previous',
this.finishButton = 'finish',
});
final String skipButton;
final String nextButton;
final String previousButton;
final String finishButton;
}

View file

@ -0,0 +1,64 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
import 'package:flutter_introduction_widget/src/config/introduction.dart';
import 'package:flutter_introduction_widget/src/types/page_introduction.dart';
import 'package:flutter_introduction_widget/src/types/single_introduction.dart';
const kAnimationDuration = Duration(milliseconds: 300);
class IntroductionScreen extends StatelessWidget {
const IntroductionScreen({
required this.options,
required this.onComplete,
super.key,
this.physics,
this.onNext,
this.onPrevious,
this.onSkip,
});
/// The options used to build the introduction screen
final IntroductionOptions options;
/// A function called when the introductionSceen changes
final VoidCallback onComplete;
/// A function called when the introductionScreen is skipped
final VoidCallback? onSkip;
final ScrollPhysics? physics;
/// A function called when the introductionScreen moved to the next page
/// where the page provided is the page where the user currently moved to
final void Function(IntroductionPage)? onNext;
/// A function called when the introductionScreen moved to the previous page
/// where the page provided is the page where the user currently moved to
final void Function(IntroductionPage)? onPrevious;
@override
Widget build(BuildContext context) => Scaffold(
backgroundColor: Colors.transparent,
body: Builder(
builder: (context) {
switch (options.displayMode) {
case IntroductionDisplayMode.multiPageHorizontal:
return MultiPageIntroductionScreen(
onComplete: onComplete,
physics: physics,
onSkip: onSkip,
onPrevious: onPrevious,
onNext: onNext,
options: options,
);
case IntroductionDisplayMode.singleScrollablePageVertical:
return SingleIntroductionPage(
options: options,
);
}
},
),
);
}

View file

@ -0,0 +1,583 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_introduction_widget/src/config/introduction.dart';
import 'package:flutter_introduction_widget/src/introduction.dart';
import 'package:flutter_introduction_widget/src/widgets/background.dart';
import 'package:flutter_introduction_widget/src/widgets/indicator.dart';
import 'package:flutter_introduction_widget/src/widgets/page_content.dart';
class MultiPageIntroductionScreen extends StatefulWidget {
const MultiPageIntroductionScreen({
required this.options,
required this.onComplete,
this.physics,
this.onNext,
this.onPrevious,
this.onSkip,
super.key,
});
final VoidCallback onComplete;
final VoidCallback? onSkip;
final void Function(IntroductionPage)? onNext;
final void Function(IntroductionPage)? onPrevious;
final ScrollPhysics? physics;
final IntroductionOptions options;
@override
State<MultiPageIntroductionScreen> createState() =>
_MultiPageIntroductionScreenState();
}
class _MultiPageIntroductionScreenState
extends State<MultiPageIntroductionScreen> {
final PageController _controller = PageController();
final ValueNotifier<int> _currentPage = ValueNotifier(0);
@override
void initState() {
super.initState();
_controller.addListener(_handleScroll);
}
void _handleScroll() {
_currentPage.value = _controller.page?.round() ?? 0;
}
@override
void dispose() {
_controller.removeListener(_handleScroll);
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
var pages = widget.options.pages;
var translations = widget.options.introductionTranslations;
return Stack(
children: [
NotificationListener<ScrollNotification>(
onNotification: (notification) {
if (notification is OverscrollNotification) {
if (notification.overscroll > 8) {
widget.onComplete.call();
}
}
// add bouncing scroll physics support
if (notification is ScrollUpdateNotification) {
var offset = notification.metrics.pixels;
if (offset > notification.metrics.maxScrollExtent + 8) {
widget.onComplete.call();
}
}
return false;
},
child: PageView(
controller: _controller,
physics: widget.physics,
children: List.generate(
pages.length,
(index) => ExplainerPage(
onTap: () {
widget.onNext?.call(pages[_currentPage.value]);
},
controller: _controller,
page: pages[index],
options: widget.options,
index: index,
),
),
),
),
SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (widget.options.controlMode ==
IntroductionControlMode.previousNextButton) ...[
Padding(
padding: const EdgeInsets.only(right: 16.0),
child: SizedBox(
height: 64,
child: AnimatedBuilder(
animation: _controller,
builder: (context, _) => Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (widget.options.skippable && !_isLast(pages)) ...[
TextButton(
onPressed: widget.onComplete,
child: Text(translations.skipButton),
),
],
],
),
),
),
),
] else ...[
const SizedBox.shrink(),
],
Align(
alignment: Alignment.bottomCenter,
child: Column(
children: [
AnimatedBuilder(
animation: _currentPage,
builder: (context, _) => Indicator(
indicatorBuilder: widget.options.indicatorBuilder,
mode: widget.options.indicatorMode,
controller: _controller,
count: pages.length,
index: _currentPage.value,
),
),
Padding(
padding: const EdgeInsets.all(32),
child: AnimatedBuilder(
animation: _controller,
builder: (context, _) {
if (widget.options.controlMode ==
IntroductionControlMode.singleButton) {
return IntroductionOneButton(
controller: _controller,
next: _isNext(pages),
previous: _isPrevious,
last: _isLast(pages),
options: widget.options,
onFinish: widget.onComplete,
onNext: () {
widget.onNext?.call(pages[_currentPage.value]);
},
onPrevious: () {
widget.onNext?.call(pages[_currentPage.value]);
},
);
}
return IntroductionTwoButtons(
controller: _controller,
next: _isNext(pages),
previous: _isPrevious,
last: _isLast(pages),
options: widget.options,
onFinish: widget.onComplete,
onNext: () {
widget.onNext?.call(pages[_currentPage.value]);
},
onPrevious: () {
widget.onNext?.call(pages[_currentPage.value]);
},
);
},
),
),
],
),
),
],
),
),
AnimatedBuilder(
animation: _controller,
builder: (context, _) => IntroductionIconButtons(
controller: _controller,
next: _isNext(pages),
previous: _isPrevious,
last: _isLast(pages),
options: widget.options,
onFinish: widget.onComplete,
onNext: () {
widget.onNext?.call(pages[_currentPage.value]);
},
onPrevious: () {
widget.onNext?.call(pages[_currentPage.value]);
},
),
),
],
);
}
bool _isLast(List<IntroductionPage> pages) =>
_currentPage.value == pages.length - 1;
bool get _isPrevious => _currentPage.value > 0;
bool _isNext(List<IntroductionPage> pages) =>
_currentPage.value < pages.length - 1;
}
class ExplainerPage extends StatelessWidget {
const ExplainerPage({
required this.page,
required this.options,
required this.index,
required this.controller,
this.onTap,
super.key,
});
final IntroductionPage page;
final IntroductionOptions options;
final PageController controller;
final int index;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Background(
background: page.decoration,
child: SafeArea(
child: Column(
children: [
const SizedBox(
height: 64,
),
Expanded(
child: IntroductionPageContent(
onTap: () {
if (options.tapEnabled) {
onTap?.call();
}
},
title: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: DefaultTextStyle(
style: theme.textTheme.displayMedium!,
child: page.title ?? Text('introduction.$index.title'),
),
),
text: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: DefaultTextStyle(
style: theme.textTheme.bodyLarge!,
child: page.text ?? Text('introduction.$index.description'),
),
),
graphic: Expanded(
child: Padding(
padding: const EdgeInsets.all(32),
child: page.graphic,
),
),
layoutStyle: page.layoutStyle ?? options.layoutStyle,
),
),
const SizedBox(
height: 144,
),
],
),
),
);
}
}
class IntroductionTwoButtons extends StatelessWidget {
const IntroductionTwoButtons({
required this.options,
required this.controller,
required this.next,
required this.last,
required this.previous,
required this.onFinish,
required this.onNext,
required this.onPrevious,
super.key,
});
final IntroductionOptions options;
final PageController controller;
final VoidCallback? onFinish;
final VoidCallback? onNext;
final VoidCallback? onPrevious;
final bool previous;
final bool next;
final bool last;
Future<void> _previous() async {
await controller.previousPage(
duration: kAnimationDuration,
curve: Curves.easeInOut,
);
onPrevious?.call();
}
@override
Widget build(BuildContext context) {
var translations = options.introductionTranslations;
var showFinishButton =
options.buttonMode == IntroductionScreenButtonMode.singleFinish;
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (options.buttonMode == IntroductionScreenButtonMode.text) ...[
if (previous) ...[
options.buttonBuilder?.call(
context,
_previous,
Text(translations.previousButton),
IntroductionButtonType.previous,
) ??
TextButton(
onPressed: _previous,
child: Text(translations.previousButton),
),
] else
const SizedBox.shrink(),
if (next) ...[
options.buttonBuilder?.call(
context,
_next,
Text(translations.nextButton),
IntroductionButtonType.next,
) ??
TextButton(
onPressed: _next,
child: Text(translations.nextButton),
),
] else if (last) ...[
options.buttonBuilder?.call(
context,
() {
onFinish?.call();
},
Text(translations.finishButton),
IntroductionButtonType.finish,
) ??
TextButton(
onPressed: () {
onFinish?.call();
},
child: Text(translations.finishButton),
),
] else ...[
const SizedBox.shrink(),
],
] else if (showFinishButton) ...[
const SizedBox.shrink(),
Expanded(
child: Visibility(
visible: last,
maintainSize: true,
maintainAnimation: true,
maintainState: true,
maintainInteractivity: false,
child: Align(
child: options.buttonBuilder?.call(
context,
() {
onFinish?.call();
},
Text(translations.finishButton),
IntroductionButtonType.finish,
) ??
ElevatedButton(
onPressed: () {
onFinish?.call();
},
child: Text(translations.finishButton),
),
),
),
),
const SizedBox.shrink(),
],
],
);
}
Future<void> _next() async {
await controller.nextPage(
duration: kAnimationDuration,
curve: Curves.easeInOut,
);
onNext?.call();
}
}
class IntroductionOneButton extends StatelessWidget {
const IntroductionOneButton({
required this.options,
required this.controller,
required this.next,
required this.last,
required this.previous,
required this.onFinish,
required this.onNext,
required this.onPrevious,
super.key,
});
final IntroductionOptions options;
final PageController controller;
final VoidCallback? onFinish;
final VoidCallback? onNext;
final VoidCallback? onPrevious;
final bool previous;
final bool next;
final bool last;
Future<void> _previous() async {
await controller.previousPage(
duration: kAnimationDuration,
curve: Curves.easeInOut,
);
onPrevious?.call();
}
@override
Widget build(BuildContext context) {
var translations = options.introductionTranslations;
return Column(
children: [
if (options.buttonMode == IntroductionScreenButtonMode.text) ...[
if (last) ...[
options.buttonBuilder?.call(
context,
() {
onFinish?.call();
},
Text(translations.finishButton),
IntroductionButtonType.finish,
) ??
TextButton(
onPressed: () {
onFinish?.call();
},
child: Text(translations.finishButton),
),
] else ...[
options.buttonBuilder?.call(
context,
_next,
Text(translations.nextButton),
IntroductionButtonType.next,
) ??
TextButton(
onPressed: _next,
child: Text(translations.nextButton),
),
],
if (previous) ...[
options.buttonBuilder?.call(
context,
_previous,
Text(translations.previousButton),
IntroductionButtonType.previous,
) ??
TextButton(
onPressed: _previous,
child: Text(translations.previousButton),
),
] else ...[
options.buttonBuilder?.call(
context,
() {
onFinish?.call();
},
Text(translations.finishButton),
IntroductionButtonType.skip,
) ??
TextButton(
onPressed: () {
onFinish?.call();
},
child: Text(translations.finishButton),
),
],
],
],
);
}
Future<void> _next() async {
await controller.nextPage(
duration: kAnimationDuration,
curve: Curves.easeInOut,
);
onNext?.call();
}
}
class IntroductionIconButtons extends StatelessWidget {
const IntroductionIconButtons({
required this.options,
required this.controller,
required this.next,
required this.last,
required this.previous,
required this.onFinish,
required this.onNext,
required this.onPrevious,
super.key,
});
final IntroductionOptions options;
final PageController controller;
final VoidCallback? onFinish;
final VoidCallback? onNext;
final VoidCallback? onPrevious;
final bool previous;
final bool next;
final bool last;
Future<void> _previous() async {
await controller.previousPage(
duration: kAnimationDuration,
curve: Curves.easeInOut,
);
onPrevious?.call();
}
@override
Widget build(BuildContext context) => Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (options.buttonMode == IntroductionScreenButtonMode.icon) ...[
if (previous) ...[
IconButton(
iconSize: 70,
onPressed: _previous,
icon: const Icon(Icons.chevron_left),
),
] else
const SizedBox.shrink(),
IconButton(
iconSize: 70,
onPressed: () {
if (next) {
unawaited(_next());
} else {
onFinish?.call();
}
},
icon: const Icon(Icons.chevron_right),
),
],
],
),
);
Future<void> _next() async {
await controller.nextPage(
duration: kAnimationDuration,
curve: Curves.easeInOut,
);
onNext?.call();
}
}

View file

@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/cupertino.dart';
import 'package:flutter_introduction_widget/src/config/introduction.dart';
class SingleIntroductionPage extends StatelessWidget {
const SingleIntroductionPage({
required this.options,
super.key,
});
final IntroductionOptions options;
@override
Widget build(BuildContext context) {
throw UnimplementedError();
}
}

View file

@ -0,0 +1,32 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
class Background extends StatelessWidget {
const Background({
required this.child,
this.background,
super.key,
});
final BoxDecoration? background;
final Widget child;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var background = this.background ??
BoxDecoration(
color: theme.colorScheme.background,
);
var size = MediaQuery.of(context).size;
return Container(
width: size.width,
height: size.height,
decoration: background,
child: child,
);
}
}

View file

@ -0,0 +1,211 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_introduction_widget/src/config/introduction.dart';
import 'package:flutter_introduction_widget/src/introduction.dart';
class Indicator extends StatelessWidget {
const Indicator({
required this.mode,
required this.controller,
required this.count,
required this.index,
required this.indicatorBuilder,
super.key,
}) : assert(
!(mode == IndicatorMode.custom && indicatorBuilder == null),
'When a custom indicator is used the indicatorBuilder '
'must be provided',
);
final IndicatorMode mode;
final PageController controller;
final Widget Function(
BuildContext,
PageController,
int,
int,
)? indicatorBuilder;
final int index;
final int count;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
switch (mode) {
case IndicatorMode.custom:
return indicatorBuilder!.call(context, controller, index, count);
case IndicatorMode.dot:
return DotsIndicator(
controller: controller,
color: theme.colorScheme.primary,
dotcolor: theme.colorScheme.secondary,
itemCount: count,
onPageSelected: (int page) {
unawaited(
controller.animateToPage(
page,
duration: kAnimationDuration,
curve: Curves.easeInOut,
),
);
},
);
case IndicatorMode.dash:
return DashIndicator(
controller: controller,
selectedColor: theme.colorScheme.primary,
itemCount: count,
onPageSelected: (int page) {
unawaited(
controller.animateToPage(
page,
duration: kAnimationDuration,
curve: Curves.easeInOut,
),
);
},
);
}
}
}
class DashIndicator extends AnimatedWidget {
const DashIndicator({
required this.controller,
required this.selectedColor,
required this.itemCount,
required this.onPageSelected,
this.color = Colors.white,
super.key,
}) : super(listenable: controller);
final PageController controller;
final Color color;
final Color selectedColor;
final int itemCount;
final Function(int) onPageSelected;
int _getPage() {
try {
return controller.page?.round() ?? 0;
} on Exception catch (_) {
return 0;
}
}
@override
Widget build(BuildContext context) {
var index = _getPage();
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (int i = 0; i < itemCount; i++) ...[
buildDash(i, selected: index == i),
],
],
);
}
Widget buildDash(int index, {required bool selected}) => SizedBox(
width: 20,
child: Center(
child: Material(
color: selected ? color : color.withAlpha(125),
type: MaterialType.card,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(2.5),
),
width: 16,
height: 5,
child: InkWell(
onTap: () => onPageSelected.call(index),
),
),
),
),
);
}
/// An indicator showing the currently selected page of a PageController
class DotsIndicator extends AnimatedWidget {
const DotsIndicator({
required this.controller,
this.color = Colors.white,
this.dotcolor = Colors.green,
this.itemCount,
this.onPageSelected,
super.key,
}) : super(
listenable: controller,
);
/// The PageController that this DotsIndicator is representing.
final Color? dotcolor;
final PageController controller;
/// The number of items managed by the PageController
final int? itemCount;
/// Called when a dot is tapped
final ValueChanged<int>? onPageSelected;
/// The color of the dots.
///
/// Defaults to `Colors.white`.
final Color color;
// The base size of the dots
static const double _kDotSize = 4.0;
// The increase in the size of the selected dot
static const double _kMaxZoom = 2.0;
// The distance between the center of each dot
static const double _kDotSpacing = 12.0;
Widget _buildDot(int index) {
var selectedness = Curves.easeOut.transform(
max(
0.0,
1.0 -
((controller.page ?? controller.initialPage).round() - index).abs(),
),
);
var zoom = 1.0 + (_kMaxZoom - 1.0) * selectedness;
return SizedBox(
width: _kDotSpacing,
child: Center(
child: Material(
color: (((controller.page ?? controller.initialPage).round()) == index
? color
: color.withAlpha(125)),
type: MaterialType.circle,
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(width: 2, color: dotcolor!),
),
width: _kDotSize * 2 * zoom,
height: _kDotSize * 2 * zoom,
child: InkWell(
onTap: () => onPageSelected!.call(index),
),
),
),
),
);
}
@override
Widget build(BuildContext context) => Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List<Widget>.generate(itemCount!, _buildDot),
);
}

View file

@ -0,0 +1,44 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
import 'package:flutter_introduction_widget/src/config/introduction.dart';
class IntroductionPageContent extends StatelessWidget {
const IntroductionPageContent({
required this.title,
required this.text,
required this.graphic,
required this.layoutStyle,
required this.onTap,
super.key,
});
final Widget? title;
final Widget? text;
final Widget? graphic;
final IntroductionLayoutStyle layoutStyle;
final VoidCallback onTap;
@override
Widget build(BuildContext context) => GestureDetector(
onTap: onTap,
child: Column(
children: [
if (graphic != null &&
layoutStyle == IntroductionLayoutStyle.imageTop)
graphic!,
if (title != null) title!,
if (graphic != null &&
layoutStyle == IntroductionLayoutStyle.imageCenter)
graphic!,
if (text != null) text!,
if (graphic != null &&
layoutStyle == IntroductionLayoutStyle.imageBottom)
graphic!,
],
),
);
}

View file

@ -0,0 +1,23 @@
name: flutter_introduction_widget
description: Flutter Introduction Widget for showing a list of introduction pages on a single scrollable page or horizontal pageview
version: 2.0.0
homepage: https://github.com/Iconica-Development/flutter_introduction_widget
environment:
sdk: ">=2.18.0 <3.0.0"
flutter: ">=1.17.0"
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 6.0.0
flutter:

View file

@ -0,0 +1,11 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter_test/flutter_test.dart';
void main() {
test('test', () {
expect(true, true);
});
}

View file

@ -1,27 +1,7 @@
name: flutter_introduction name: flutter_introduction_workspace
description: Combined Package of Flutter Introduction Widget and Flutter Introduction Service version: 2.0.0
version: 1.0.0
publish_to: none
environment: environment:
sdk: ">=2.18.0 <3.0.0" sdk: '>=3.1.0 <4.0.0'
flutter: ">=1.17.0"
dependencies:
flutter:
sdk: flutter
flutter_introduction_widget:
git:
url: https://github.com/Iconica-Development/flutter_introduction_widget.git
ref: 3.0.0
flutter_introduction_service:
git:
url: https://github.com/Iconica-Development/flutter_introduction_service.git
ref: 1.0.0
dev_dependencies: dev_dependencies:
flutter_test: melos: ^3.0.1
sdk: flutter
flutter_lints: ^2.0.0
flutter: