├── images
├── 1.png
├── 2.png
├── 3.png
├── 4.png
├── 5.png
├── 6.png
├── full-1.png
└── full-2.png
├── assets
├── icon.png
├── branding.png
├── icon-bg.png
├── splash.png
├── dialpad
│ ├── 0.mp3
│ ├── 1.mp3
│ ├── 2.mp3
│ ├── 3.mp3
│ ├── 4.mp3
│ ├── 5.mp3
│ ├── 6.mp3
│ ├── 7.mp3
│ ├── 8.mp3
│ ├── 9.mp3
│ ├── hash.mp3
│ └── star.mp3
└── static-bg.jpg
├── android
├── app
│ ├── src
│ │ ├── main
│ │ │ ├── res
│ │ │ │ ├── drawable-hdpi
│ │ │ │ │ ├── splash.png
│ │ │ │ │ └── android12splash.png
│ │ │ │ ├── drawable-mdpi
│ │ │ │ │ ├── splash.png
│ │ │ │ │ └── android12splash.png
│ │ │ │ ├── drawable
│ │ │ │ │ ├── background.png
│ │ │ │ │ └── launch_background.xml
│ │ │ │ ├── drawable-xhdpi
│ │ │ │ │ ├── splash.png
│ │ │ │ │ └── android12splash.png
│ │ │ │ ├── drawable-xxhdpi
│ │ │ │ │ ├── splash.png
│ │ │ │ │ └── android12splash.png
│ │ │ │ ├── drawable-night
│ │ │ │ │ ├── background.png
│ │ │ │ │ └── launch_background.xml
│ │ │ │ ├── drawable-v21
│ │ │ │ │ ├── background.png
│ │ │ │ │ └── launch_background.xml
│ │ │ │ ├── drawable-xxxhdpi
│ │ │ │ │ ├── splash.png
│ │ │ │ │ └── android12splash.png
│ │ │ │ ├── mipmap-hdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-mdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-xhdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-xxhdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── drawable-night-hdpi
│ │ │ │ │ ├── splash.png
│ │ │ │ │ └── android12splash.png
│ │ │ │ ├── drawable-night-mdpi
│ │ │ │ │ ├── splash.png
│ │ │ │ │ └── android12splash.png
│ │ │ │ ├── drawable-night-xhdpi
│ │ │ │ │ ├── splash.png
│ │ │ │ │ └── android12splash.png
│ │ │ │ ├── mipmap-xxxhdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── drawable-night-v21
│ │ │ │ │ ├── background.png
│ │ │ │ │ └── launch_background.xml
│ │ │ │ ├── drawable-night-xxhdpi
│ │ │ │ │ ├── splash.png
│ │ │ │ │ └── android12splash.png
│ │ │ │ ├── drawable-night-xxxhdpi
│ │ │ │ │ ├── splash.png
│ │ │ │ │ └── android12splash.png
│ │ │ │ ├── values-v31
│ │ │ │ │ └── styles.xml
│ │ │ │ ├── values-night-v31
│ │ │ │ │ └── styles.xml
│ │ │ │ ├── values
│ │ │ │ │ └── styles.xml
│ │ │ │ └── values-night
│ │ │ │ │ └── styles.xml
│ │ │ ├── kotlin
│ │ │ │ └── com
│ │ │ │ │ └── example
│ │ │ │ │ └── revo
│ │ │ │ │ └── MainActivity.kt
│ │ │ └── AndroidManifest.xml
│ │ ├── debug
│ │ │ └── AndroidManifest.xml
│ │ └── profile
│ │ │ └── AndroidManifest.xml
│ ├── proguard-rules.pro
│ └── build.gradle
├── gradle.properties
├── gradle
│ └── wrapper
│ │ └── gradle-wrapper.properties
├── .gitignore
├── build.gradle
└── settings.gradle
├── lib
├── ui
│ ├── views
│ │ ├── common
│ │ │ ├── constants.dart
│ │ │ ├── matched_view.dart
│ │ │ └── contact_tile.dart
│ │ ├── dialpad_view
│ │ │ ├── action_btn.dart
│ │ │ └── dial_btn.dart
│ │ ├── qr_scanner_view.dart
│ │ ├── settings_view
│ │ │ ├── call.dart
│ │ │ ├── sound.dart
│ │ │ ├── about.dart
│ │ │ └── user_interface.dart
│ │ ├── home_view
│ │ │ ├── fav_view.dart
│ │ │ ├── navigation_view.dart
│ │ │ ├── appbar_view.dart
│ │ │ ├── contacts_view.dart
│ │ │ └── recents_view.dart
│ │ ├── search_view.dart
│ │ ├── home_view.dart
│ │ ├── history_view.dart
│ │ ├── settings_view.dart
│ │ ├── dialpad_view.dart
│ │ ├── call_screen.dart
│ │ └── contactinfo_view.dart
│ ├── popups
│ │ ├── qr_popup.dart
│ │ ├── welcome_changelog.dart
│ │ ├── number_choose_popup.dart
│ │ └── sim_choose_popup.dart
│ └── theme
│ │ └── handler.dart
├── extensions
│ ├── theme.dart
│ ├── title.dart
│ └── datetime.dart
├── utils
│ ├── share.dart
│ ├── center_text.dart
│ ├── utils.dart
│ ├── rounded_icon_btn.dart
│ ├── circle_profile.dart
│ ├── switch_tile.dart
│ └── menu_tile.dart
├── constants
│ ├── routes.dart
│ └── pref.dart
├── services
│ ├── cubit
│ │ ├── mobile_service.dart
│ │ ├── call_log_service.dart
│ │ └── contact_service.dart
│ ├── prefservice.dart
│ ├── activity_service.dart
│ └── backgroundservice.dart2
├── model
│ ├── sim_card.dart
│ ├── call_type.dart
│ ├── call_log.dart
│ └── contact.dart
└── main.dart
├── devtools_options.yaml
├── .cph
└── .A_Minimal_Coprime.cpp_54e86288cedbc49628b71caf69c90717.prob
├── .gitignore
├── analysis_options.yaml
├── .metadata
├── privacy-policy.md
├── README.md
├── pubspec.yaml
└── pubspec.lock
/images/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/images/1.png
--------------------------------------------------------------------------------
/images/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/images/2.png
--------------------------------------------------------------------------------
/images/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/images/3.png
--------------------------------------------------------------------------------
/images/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/images/4.png
--------------------------------------------------------------------------------
/images/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/images/5.png
--------------------------------------------------------------------------------
/images/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/images/6.png
--------------------------------------------------------------------------------
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/assets/icon.png
--------------------------------------------------------------------------------
/assets/branding.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/assets/branding.png
--------------------------------------------------------------------------------
/assets/icon-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/assets/icon-bg.png
--------------------------------------------------------------------------------
/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/assets/splash.png
--------------------------------------------------------------------------------
/images/full-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/images/full-1.png
--------------------------------------------------------------------------------
/images/full-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/images/full-2.png
--------------------------------------------------------------------------------
/assets/dialpad/0.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/assets/dialpad/0.mp3
--------------------------------------------------------------------------------
/assets/dialpad/1.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/assets/dialpad/1.mp3
--------------------------------------------------------------------------------
/assets/dialpad/2.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/assets/dialpad/2.mp3
--------------------------------------------------------------------------------
/assets/dialpad/3.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/assets/dialpad/3.mp3
--------------------------------------------------------------------------------
/assets/dialpad/4.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/assets/dialpad/4.mp3
--------------------------------------------------------------------------------
/assets/dialpad/5.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/assets/dialpad/5.mp3
--------------------------------------------------------------------------------
/assets/dialpad/6.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/assets/dialpad/6.mp3
--------------------------------------------------------------------------------
/assets/dialpad/7.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/assets/dialpad/7.mp3
--------------------------------------------------------------------------------
/assets/dialpad/8.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/assets/dialpad/8.mp3
--------------------------------------------------------------------------------
/assets/dialpad/9.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/assets/dialpad/9.mp3
--------------------------------------------------------------------------------
/assets/static-bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/assets/static-bg.jpg
--------------------------------------------------------------------------------
/assets/dialpad/hash.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/assets/dialpad/hash.mp3
--------------------------------------------------------------------------------
/assets/dialpad/star.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/assets/dialpad/star.mp3
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-hdpi/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/android/app/src/main/res/drawable-hdpi/splash.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-mdpi/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/android/app/src/main/res/drawable-mdpi/splash.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/android/app/src/main/res/drawable/background.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xhdpi/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/android/app/src/main/res/drawable-xhdpi/splash.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxhdpi/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/android/app/src/main/res/drawable-xxhdpi/splash.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-night/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/android/app/src/main/res/drawable-night/background.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-v21/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/android/app/src/main/res/drawable-v21/background.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxxhdpi/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/android/app/src/main/res/drawable-xxxhdpi/splash.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-night-hdpi/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/android/app/src/main/res/drawable-night-hdpi/splash.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-night-mdpi/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/android/app/src/main/res/drawable-night-mdpi/splash.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-night-xhdpi/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/android/app/src/main/res/drawable-night-xhdpi/splash.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
2 | android.useAndroidX=true
3 | android.enableJetifier=true
4 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-hdpi/android12splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/android/app/src/main/res/drawable-hdpi/android12splash.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-mdpi/android12splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/android/app/src/main/res/drawable-mdpi/android12splash.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-night-v21/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/android/app/src/main/res/drawable-night-v21/background.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-night-xxhdpi/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/android/app/src/main/res/drawable-night-xxhdpi/splash.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-night-xxxhdpi/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/android/app/src/main/res/drawable-night-xxxhdpi/splash.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xhdpi/android12splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/android/app/src/main/res/drawable-xhdpi/android12splash.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxhdpi/android12splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/android/app/src/main/res/drawable-xxhdpi/android12splash.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxxhdpi/android12splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/android/app/src/main/res/drawable-xxxhdpi/android12splash.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-night-hdpi/android12splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/android/app/src/main/res/drawable-night-hdpi/android12splash.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-night-mdpi/android12splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/android/app/src/main/res/drawable-night-mdpi/android12splash.png
--------------------------------------------------------------------------------
/lib/ui/views/common/constants.dart:
--------------------------------------------------------------------------------
1 | const String version = "1.1";
2 | const String changelog =
3 | "1. Tweaked the interface\n2. Added settings page\n3. Fixed data fetch issues";
4 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-night-xhdpi/android12splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/android/app/src/main/res/drawable-night-xhdpi/android12splash.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/user-grinch/RivoPhoneApp/HEAD/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/com/example/revo/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.grinch.rivo
2 |
3 | import io.flutter.embedding.android.FlutterActivity
4 |
5 | class MainActivity: FlutterActivity()
6 |
7 |
--------------------------------------------------------------------------------
/devtools_options.yaml:
--------------------------------------------------------------------------------
1 | description: This file stores settings for Dart & Flutter DevTools.
2 | documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
3 | extensions:
4 |
--------------------------------------------------------------------------------
/android/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | -dontwarn java.beans.ConstructorProperties
2 | -dontwarn java.beans.Transient
3 | -dontwarn org.conscrypt.Conscrypt
4 | -dontwarn org.conscrypt.OpenSSLProvider
5 | -dontwarn org.w3c.dom.bootstrap.DOMImplementationRegistry
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | zipStoreBase=GRADLE_USER_HOME
4 | zipStorePath=wrapper/dists
5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip
6 |
--------------------------------------------------------------------------------
/lib/extensions/theme.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | extension ColorSchemeExtension on BuildContext {
4 | ColorScheme get colorScheme => Theme.of(this).colorScheme;
5 | }
6 |
7 | extension ThemeExtension on BuildContext {
8 | TextTheme get textTheme => Theme.of(this).textTheme;
9 | }
10 |
--------------------------------------------------------------------------------
/android/.gitignore:
--------------------------------------------------------------------------------
1 | gradle-wrapper.jar
2 | /.gradle
3 | /captures/
4 | /gradlew
5 | /gradlew.bat
6 | /local.properties
7 | GeneratedPluginRegistrant.java
8 |
9 | # Remember to never publicly share your keystore.
10 | # See https://flutter.dev/to/reference-keystore
11 | key.properties
12 | **/*.keystore
13 | **/*.jks
14 |
--------------------------------------------------------------------------------
/lib/extensions/title.dart:
--------------------------------------------------------------------------------
1 | extension StringExtensions on String {
2 | String toTitleCase() {
3 | if (isEmpty) return this;
4 |
5 | return split(' ').map((word) {
6 | if (word.isEmpty) return word;
7 | return word[0].toUpperCase() + word.substring(1).toLowerCase();
8 | }).join(' ');
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/lib/utils/share.dart:
--------------------------------------------------------------------------------
1 | import 'package:revo/model/contact.dart';
2 |
3 | String generateVCardString(Contact contact) {
4 | String str = '''
5 | BEGIN:VCARD
6 | VERSION:3.0
7 | FN:${contact.fullName}''';
8 |
9 | for (var phone in contact.phones) {
10 | str += 'TEL:$phone\n';
11 | }
12 | str += 'END:VCARD';
13 | return str;
14 | }
15 |
--------------------------------------------------------------------------------
/lib/constants/routes.dart:
--------------------------------------------------------------------------------
1 | const String settingsRoute = '/settings';
2 | const String searchRoute = '/search';
3 | const String homeRoute = '/';
4 | const String dialpadRoute = '/dialpad';
5 | const String contactInfoRoute = '/contact-info';
6 | const String qrScanRoute = '/qr-scan';
7 | const String callHistoryRoute = '/call-history';
8 | const String callScreenRoute = '/call-screen';
9 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
6 | -
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-night/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
6 | -
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-v21/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
6 | -
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-night-v21/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
6 | -
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | allprojects {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | }
6 | }
7 |
8 | rootProject.buildDir = "../build"
9 | subprojects {
10 | project.buildDir = "${rootProject.buildDir}/${project.name}"
11 | }
12 | subprojects {
13 | project.evaluationDependsOn(":app")
14 | }
15 |
16 | tasks.register("clean", Delete) {
17 | delete rootProject.buildDir
18 | }
19 |
--------------------------------------------------------------------------------
/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/android/app/src/profile/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.cph/.A_Minimal_Coprime.cpp_54e86288cedbc49628b71caf69c90717.prob:
--------------------------------------------------------------------------------
1 | {"name":"A. Minimal Coprime","group":"Codeforces - Codeforces Round 1000 (Div. 2)","url":"https://codeforces.com/contest/2063/problem/A","interactive":false,"memoryLimit":256,"timeLimit":1000,"tests":[{"id":1737555498318,"input":"6\n1 2\n1 10\n49 49\n69 420\n1 1\n9982 44353\n","output":"1\n9\n0\n351\n1\n34371\n"}],"testType":"single","input":{"type":"stdin"},"output":{"type":"stdout"},"languages":{"java":{"mainClass":"Main","taskClass":"AMinimalCoprime"}},"batch":{"id":"2fbebdc0-7915-41eb-82e5-c6d7a6cd7e4b","size":1},"srcPath":"d:\\GitHub Projects\\Rivo\\A_Minimal_Coprime.cpp"}
--------------------------------------------------------------------------------
/lib/utils/center_text.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:google_fonts/google_fonts.dart';
3 | import 'package:revo/extensions/theme.dart';
4 |
5 | class CenterText extends StatelessWidget {
6 | final String text;
7 | final double size;
8 | const CenterText({super.key, required this.text, this.size = 16.0});
9 |
10 | @override
11 | Widget build(BuildContext context) {
12 | return Center(
13 | child: Text(
14 | text,
15 | style: GoogleFonts.raleway(
16 | fontSize: size,
17 | color: context.colorScheme.onSurface,
18 | ),
19 | ),
20 | );
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/lib/constants/pref.dart:
--------------------------------------------------------------------------------
1 | const String PREF_DTMF_TONE = "PREF_DTMF_TONE";
2 | const String PREF_DIALPAD_VIBRATION = "PREF_DIALPAD_VIBRATION";
3 | const String PREF_DIALPAD_LETTERS = "PREF_DIALPAD_LETTERS";
4 |
5 | const String PREF_MATERIAL_THEMING = "PREF_MATERIAL_THEMING";
6 | const String PREF_AMOLED_DARK_MODE = "PREF_AMOLED_DARK_MODE";
7 | const String PREF_SHOW_FIRST_LETTER = "PREF_SHOW_FIRST_LETTER";
8 | const String PREF_SHOW_PICTURE_IN_AVATAR = "PREF_SHOW_PICTURE_IN_AVATAR";
9 | const String PREF_ICON_ONLY_BOTTOMSHEET = "PREF_ICON_ONLY_BOTTOMSHEET";
10 | const String PREF_ALWAYS_SHOW_SELECTED_IN_BOTTOMSHEET =
11 | "PREF_ALWAYS_SHOW_SELECTED_IN_BOTTOMSHEET";
12 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | def flutterSdkPath = {
3 | def properties = new Properties()
4 | file("local.properties").withInputStream { properties.load(it) }
5 | def flutterSdkPath = properties.getProperty("flutter.sdk")
6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
7 | return flutterSdkPath
8 | }()
9 |
10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
11 |
12 | repositories {
13 | google()
14 | mavenCentral()
15 | gradlePluginPortal()
16 | }
17 | }
18 |
19 | plugins {
20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0"
21 | id "com.android.application" version "8.1.0" apply false
22 | id "org.jetbrains.kotlin.android" version "1.8.22" apply false
23 | }
24 |
25 | include ":app"
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | .DS_Store
7 | .atom/
8 | .build/
9 | .buildlog/
10 | .history
11 | .svn/
12 | .swiftpm/
13 | migrate_working_dir/
14 |
15 | # IntelliJ related
16 | *.iml
17 | *.ipr
18 | *.iws
19 | .idea/
20 |
21 | # The .vscode folder contains launch configuration and tasks you configure in
22 | # VS Code which you may wish to be included in version control, so this line
23 | # is commented out by default.
24 | #.vscode/
25 |
26 | # Flutter/Dart/Pub related
27 | **/doc/api/
28 | **/ios/Flutter/.last_build_id
29 | .dart_tool/
30 | .flutter-plugins
31 | .flutter-plugins-dependencies
32 | .pub-cache/
33 | .pub/
34 | /build/
35 |
36 | # Symbolication related
37 | app.*.symbols
38 |
39 | # Obfuscation related
40 | app.*.map.json
41 |
42 | # Android Studio will place build artifacts here
43 | /android/app/debug
44 | /android/app/profile
45 | /android/app/release
46 |
--------------------------------------------------------------------------------
/lib/services/cubit/mobile_service.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_bloc/flutter_bloc.dart';
2 | import 'package:flutter_sim_data/sim_data.dart';
3 | import 'package:permission_handler/permission_handler.dart';
4 | import 'package:revo/model/sim_card.dart';
5 | import 'package:revo/services/activity_service.dart';
6 |
7 | class MobileService extends Cubit> {
8 | List? _simCards;
9 | MobileService() : super([]) {
10 | _initialize();
11 | }
12 |
13 | Future _initialize() async {
14 | await ActivityService().requestPermissions();
15 | if (await Permission.phone.status.isGranted) {
16 | try {
17 | var data = await SimData().getSimData();
18 | _simCards = data.map((e) => SimCard.fromInternal(e)).toList();
19 | } catch (e) {
20 | // TODO: Show error dialog
21 | }
22 | }
23 | }
24 |
25 | List get getSimInfo {
26 | return _simCards ?? [];
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/lib/ui/popups/qr_popup.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:google_fonts/google_fonts.dart';
3 | import 'package:qr_flutter/qr_flutter.dart';
4 | import 'package:revo/extensions/theme.dart';
5 |
6 | Widget qrCodePopup(
7 | BuildContext context,
8 | String data,
9 | ) {
10 | return Dialog(
11 | backgroundColor: context.colorScheme.surfaceContainer,
12 | child: Padding(
13 | padding: const EdgeInsets.all(16.0),
14 | child: Column(
15 | mainAxisSize: MainAxisSize.min,
16 | children: [
17 | Text(
18 | 'Scan to add contact',
19 | style: GoogleFonts.raleway(
20 | color: context.colorScheme.onSurface,
21 | fontSize: 20,
22 | ),
23 | ),
24 | SizedBox(height: 20),
25 | QrImageView(
26 | data: data,
27 | size: 280,
28 | backgroundColor: Colors.white,
29 | ),
30 | SizedBox(height: 20),
31 | ],
32 | ),
33 | ),
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/lib/model/sim_card.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_sim_data/sim_data_model.dart' as lib;
2 |
3 | class SimCard {
4 | final String carrierName;
5 | final bool isESIM;
6 | final int subscriptionId;
7 | final int simSlotIndex;
8 | final int cardId;
9 | final String phoneNumber;
10 | final String displayName;
11 | final String countryCode;
12 |
13 | SimCard({
14 | required this.carrierName,
15 | required this.isESIM,
16 | required this.subscriptionId,
17 | required this.simSlotIndex,
18 | required this.cardId,
19 | required this.phoneNumber,
20 | required this.displayName,
21 | required this.countryCode,
22 | });
23 |
24 | factory SimCard.fromInternal(lib.SimDataModel data) {
25 | return SimCard(
26 | carrierName: data.carrierName,
27 | isESIM: data.isESIM,
28 | subscriptionId: data.subscriptionId,
29 | simSlotIndex: data.simSlotIndex,
30 | cardId: data.cardId,
31 | phoneNumber: data.phoneNumber,
32 | displayName: data.displayName,
33 | countryCode: data.countryCode,
34 | );
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/lib/ui/views/dialpad_view/action_btn.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:revo/extensions/theme.dart';
3 |
4 | class DialActionButton extends StatelessWidget {
5 | final IconData icon;
6 | final String label;
7 | final Function()? func;
8 |
9 | const DialActionButton(
10 | {required this.icon, required this.label, this.func, super.key});
11 |
12 | @override
13 | Widget build(BuildContext context) {
14 | return TextButton(
15 | style: TextButton.styleFrom(
16 | shape: RoundedRectangleBorder(
17 | borderRadius: BorderRadius.circular(50),
18 | ),
19 | backgroundColor: context.colorScheme.secondaryContainer.withAlpha(150),
20 | elevation: 0,
21 | padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 10),
22 | ),
23 | onPressed: func,
24 | child: Row(
25 | children: [
26 | Icon(
27 | icon,
28 | color: context.colorScheme.onSurface,
29 | ),
30 | SizedBox(
31 | width: 2,
32 | ),
33 | Text(
34 | label,
35 | style:
36 | TextStyle(fontSize: 18, color: context.colorScheme.onSurface),
37 | ),
38 | ],
39 | ),
40 | );
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values-v31/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
18 |
21 |
22 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values-night-v31/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
18 |
21 |
22 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
19 |
22 |
23 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values-night/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
19 |
22 |
23 |
--------------------------------------------------------------------------------
/lib/extensions/datetime.dart:
--------------------------------------------------------------------------------
1 | import 'package:intl/intl.dart';
2 |
3 | extension ContextAware on DateTime {
4 | String getContextAwareDate() {
5 | final now = DateTime.now();
6 | final today = DateTime(now.year, now.month, now.day);
7 | final yesterday = today.subtract(Duration(days: 1));
8 | final aWeekAgo = today.subtract(Duration(days: 7));
9 |
10 | if (isAfter(today)) {
11 | return 'Today';
12 | } else if (isAfter(yesterday)) {
13 | return 'Yesterday';
14 | } else if (isAfter(aWeekAgo)) {
15 | return DateFormat.EEEE().format(this);
16 | } else {
17 | return DateFormat('d MMM, y').format(this);
18 | }
19 | }
20 |
21 | String getContextAwareDateTime() {
22 | final now = DateTime.now();
23 | final today = DateTime(now.year, now.month, now.day);
24 | final yesterday = today.subtract(Duration(days: 1));
25 | final aWeekAgo = today.subtract(Duration(days: 7));
26 |
27 | if (isAfter(today)) {
28 | return '${DateFormat.jm().format(this)}, Today';
29 | } else if (isAfter(yesterday)) {
30 | return '${DateFormat.jm().format(this)}, Yesterday';
31 | } else if (isAfter(aWeekAgo)) {
32 | return '${DateFormat.EEEE().format(this)}, ${DateFormat.jm().format(this)}';
33 | } else {
34 | return '${DateFormat('d MMM, y').format(this)}, ${DateFormat.jm().format(this)}';
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/lib/ui/views/common/matched_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_bloc/flutter_bloc.dart';
3 | import 'package:revo/model/contact.dart';
4 | import 'package:revo/services/cubit/contact_service.dart';
5 | import 'package:revo/ui/views/common/contact_tile.dart';
6 |
7 | class MatchedView extends StatelessWidget {
8 | final ScrollController scrollController;
9 | final String number;
10 |
11 | const MatchedView({
12 | super.key,
13 | required this.scrollController,
14 | required this.number,
15 | });
16 |
17 | @override
18 | Widget build(BuildContext context) {
19 | return BlocBuilder>(
20 | builder: (context, state) {
21 | List contacts;
22 | if (number.isEmpty) {
23 | contacts = context.read().state;
24 | } else {
25 | contacts = context
26 | .read()
27 | .findAllByNameOrNumber(number, number);
28 | }
29 | return Scrollbar(
30 | controller: scrollController,
31 | child: ListView.builder(
32 | padding: const EdgeInsets.only(top: 20.0),
33 | shrinkWrap: true,
34 | itemCount: contacts.length,
35 | controller: scrollController,
36 | itemBuilder: (context, i) {
37 | return ContactTile(contact: contacts[i]);
38 | },
39 | ),
40 | );
41 | },
42 | );
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/lib/utils/utils.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter/services.dart';
3 | import 'package:revo/constants/pref.dart';
4 | import 'package:revo/services/prefservice.dart';
5 | import 'package:url_launcher/url_launcher.dart';
6 |
7 | String normalizePhoneNumber(String phoneNumber) {
8 | return phoneNumber.replaceAll(RegExp(r'[^0-9+]'), '');
9 | }
10 |
11 | String convertSecondsToHMS(int totalSeconds) {
12 | int hours = totalSeconds ~/ 3600;
13 | int minutes = (totalSeconds % 3600) ~/ 60;
14 | int seconds = totalSeconds % 60;
15 |
16 | String result = '';
17 | if (hours > 0) {
18 | result += '$hours hour${hours > 1 ? 's' : ''} ';
19 | }
20 | if (minutes > 0) {
21 | result += '$minutes min${minutes > 1 ? 's' : ''} ';
22 | }
23 | if (seconds > 0 || result.isEmpty) {
24 | result += '$seconds sec${seconds > 1 ? 's' : ''}';
25 | }
26 |
27 | return result.trim();
28 | }
29 |
30 | Future launchURL(String url) async {
31 | try {
32 | final Uri uri = Uri.parse(url);
33 | if (await canLaunchUrl(uri)) {
34 | await launchUrl(uri, mode: LaunchMode.externalApplication);
35 | } else {
36 | debugPrint('Cannot launch URL: $url');
37 | throw Exception('Could not launch $url');
38 | }
39 | } catch (e) {
40 | debugPrint('Error occurred: $e');
41 | }
42 | }
43 |
44 | void hapticVibration() {
45 | if (SharedPrefService().getBool(PREF_DIALPAD_VIBRATION, def: true)) {
46 | HapticFeedback.lightImpact();
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id "com.android.application"
3 | id "kotlin-android"
4 | // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
5 | id "dev.flutter.flutter-gradle-plugin"
6 | }
7 |
8 | android {
9 | namespace = "com.grinch.rivo"
10 | compileSdk = flutter.compileSdkVersion
11 | ndkVersion = flutter.ndkVersion
12 |
13 | compileOptions {
14 | sourceCompatibility = JavaVersion.VERSION_17
15 | targetCompatibility = JavaVersion.VERSION_17
16 | }
17 |
18 | kotlinOptions {
19 | jvmTarget = JavaVersion.VERSION_17
20 | }
21 |
22 | defaultConfig {
23 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
24 | applicationId = "com.grinch.rivo"
25 | // You can update the following values to match your application needs.
26 | // For more information, see: https://flutter.dev/to/review-gradle-config.
27 | minSdk = 24
28 | targetSdk = flutter.targetSdkVersion
29 | versionCode = flutter.versionCode
30 | versionName = flutter.versionName
31 | }
32 |
33 | buildTypes {
34 | release {
35 | // TODO: Add your own signing config for the release build.
36 | // Signing with the debug keys for now, so `flutter run --release` works.
37 | signingConfig = signingConfigs.debug
38 | }
39 | }
40 | }
41 |
42 | flutter {
43 | source = "../.."
44 | }
45 |
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | # This file configures the analyzer, which statically analyzes Dart code to
2 | # check for errors, warnings, and lints.
3 | #
4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled
5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
6 | # invoked from the command line by running `flutter analyze`.
7 |
8 | # The following line activates a set of recommended lints for Flutter apps,
9 | # packages, and plugins designed to encourage good coding practices.
10 | include: package:flutter_lints/flutter.yaml
11 |
12 | linter:
13 | # The lint rules applied to this project can be customized in the
14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml`
15 | # included above or to enable additional rules. A list of all available lints
16 | # and their documentation is published at https://dart.dev/lints.
17 | #
18 | # Instead of disabling a lint rule for the entire project in the
19 | # section below, it can also be suppressed for a single line of code
20 | # or a specific dart file by using the `// ignore: name_of_lint` and
21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file
22 | # producing the lint.
23 | rules:
24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule
25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
26 |
27 | # Additional information about this file can be found at
28 | # https://dart.dev/guides/language/analysis-options
29 |
--------------------------------------------------------------------------------
/lib/utils/rounded_icon_btn.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:google_fonts/google_fonts.dart';
3 | import 'package:revo/extensions/theme.dart';
4 |
5 | class RoundedIconButton extends StatelessWidget {
6 | final VoidCallback? onTap;
7 | final VoidCallback? onLongPress;
8 | final IconData icon;
9 | final double size;
10 | final String text;
11 |
12 | const RoundedIconButton(
13 | BuildContext context, {
14 | super.key,
15 | required this.icon,
16 | required this.size,
17 | this.text = '',
18 | this.onTap,
19 | this.onLongPress,
20 | });
21 |
22 | @override
23 | Widget build(BuildContext context) {
24 | return Column(
25 | children: [
26 | GestureDetector(
27 | onTap: onTap,
28 | onLongPress: onLongPress,
29 | child: Container(
30 | decoration: BoxDecoration(
31 | color: context.colorScheme.secondaryContainer,
32 | shape: BoxShape.circle,
33 | ),
34 | width: size,
35 | height: size,
36 | child: Icon(
37 | icon,
38 | color: context.colorScheme.onSecondaryContainer,
39 | size: size / 1.75,
40 | ),
41 | ),
42 | ),
43 | const SizedBox(height: 8),
44 | if (text.isNotEmpty)
45 | Text(
46 | text,
47 | style: GoogleFonts.raleway(
48 | textStyle: context.textTheme.bodyLarge,
49 | color: context.colorScheme.onSurface,
50 | fontSize: 12,
51 | ),
52 | ),
53 | ],
54 | );
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/lib/model/call_type.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:hugeicons/hugeicons.dart';
3 |
4 | enum CallType {
5 | incoming,
6 | outgoing,
7 | missed,
8 | rejected,
9 | blocked,
10 | unknown,
11 | }
12 |
13 | extension CallTypeHelper on CallType {
14 | IconData getIcon() {
15 | switch (this) {
16 | case CallType.incoming:
17 | return HugeIcons.strokeRoundedCallReceived;
18 | case CallType.outgoing:
19 | return HugeIcons.strokeRoundedCallOutgoing01;
20 | case CallType.rejected:
21 | return HugeIcons.strokeRoundedCallDisabled02;
22 | case CallType.blocked:
23 | return HugeIcons.strokeRoundedCallBlocked;
24 | default:
25 | return HugeIcons.strokeRoundedCallMissed01;
26 | }
27 | }
28 |
29 | String getText() {
30 | switch (this) {
31 | case CallType.incoming:
32 | return 'Incoming';
33 | case CallType.outgoing:
34 | return 'Outgoing';
35 | case CallType.rejected:
36 | return 'Rejected';
37 | case CallType.missed:
38 | return 'Missed';
39 | case CallType.blocked:
40 | return 'Blocked';
41 | default:
42 | return '';
43 | }
44 | }
45 |
46 | Color getColor() {
47 | switch (this) {
48 | case CallType.incoming:
49 | return Colors.blue.withAlpha(200);
50 | case CallType.outgoing:
51 | return Colors.green.withAlpha(200);
52 | case CallType.rejected:
53 | return Colors.red.withAlpha(200);
54 | case CallType.missed:
55 | return Colors.red.withAlpha(200);
56 | case CallType.blocked:
57 | return Colors.grey.withAlpha(200);
58 | default:
59 | return Colors.white.withAlpha(200);
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/lib/ui/views/qr_scanner_view.dart:
--------------------------------------------------------------------------------
1 | // class SimpleBarcodeScannerView extends StatelessWidget {
2 | // @override
3 | // Widget build(BuildContext context) {
4 | // bool isProcessingScan = false;
5 |
6 | // return Scaffold(
7 | // appBar: AppBar(
8 | // leading: IconButton(
9 | // icon: Icon(HugeIcons.strokeRoundedArrowLeft01),
10 | // onPressed: () => Navigator.of(context).pop(), // Close scanner screen
11 | // ),
12 | // title: const Text('Scan QR to add contact'),
13 | // ),
14 | // body: SimpleBarcodeScanner.streamBarcode(
15 | // context,
16 | // barcodeAppBar: const BarcodeAppBar(
17 | // appBarTitle: 'Scan QR to add contact',
18 | // centerTitle: false,
19 | // enableBackButton: true,
20 | // backButtonIcon: Icon(HugeIcons.strokeRoundedArrowLeft01),
21 | // ),
22 | // scanType: ScanType.qr,
23 | // isShowFlashIcon: true,
24 | // delayMillis: 500,
25 | // ).listen((event) async {
26 | // if (isProcessingScan) return;
27 | // isProcessingScan = true;
28 |
29 | // if (event.startsWith("BEGIN:VCARD") && event.endsWith("END:VCARD")) {
30 | // await context.read().insertContactFromVCard(event);
31 | // ScaffoldMessenger.of(context).showSnackBar(
32 | // const SnackBar(content: Text('Contact added successfully!')),
33 | // );
34 | // Navigator.of(context).pop(); // Close scanner window after success
35 | // } else {
36 | // ScaffoldMessenger.of(context).showSnackBar(
37 | // const SnackBar(content: Text('Invalid vCard format!')),
38 | // );
39 | // }
40 | // isProcessingScan = false;
41 | // }),
42 | // );
43 | // }
44 | // }
45 |
--------------------------------------------------------------------------------
/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled and should not be manually edited.
5 |
6 | version:
7 | revision: "17025dd88227cd9532c33fa78f5250d548d87e9a"
8 | channel: "stable"
9 |
10 | project_type: app
11 |
12 | # Tracks metadata for the flutter migrate command
13 | migration:
14 | platforms:
15 | - platform: root
16 | create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a
17 | base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a
18 | - platform: android
19 | create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a
20 | base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a
21 | - platform: ios
22 | create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a
23 | base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a
24 | - platform: linux
25 | create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a
26 | base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a
27 | - platform: macos
28 | create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a
29 | base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a
30 | - platform: web
31 | create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a
32 | base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a
33 | - platform: windows
34 | create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a
35 | base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a
36 |
37 | # User provided section
38 |
39 | # List of Local paths (relative to this file) that should be
40 | # ignored by the migrate tool.
41 | #
42 | # Files that are not part of the templates will be ignored by default.
43 | unmanaged_files:
44 | - 'lib/main.dart'
45 | - 'ios/Runner.xcodeproj/project.pbxproj'
46 |
--------------------------------------------------------------------------------
/lib/model/call_log.dart:
--------------------------------------------------------------------------------
1 | import 'dart:typed_data';
2 | import 'package:call_e_log/call_log.dart' as lib;
3 | import 'package:revo/model/call_type.dart';
4 |
5 | class CallLog {
6 | final Uint8List? profile;
7 | final String name;
8 | final String number;
9 | final String simDisplayName;
10 | final DateTime date;
11 | final String duration;
12 | final CallType type;
13 | final String accountId;
14 |
15 | CallLog(
16 | this.profile, {
17 | required this.name,
18 | required this.number,
19 | required this.simDisplayName,
20 | required this.date,
21 | required this.duration,
22 | required this.type,
23 | required this.accountId,
24 | });
25 |
26 | factory CallLog.fromEntry({
27 | required lib.CallLogEntry entry,
28 | Uint8List? profile,
29 | }) {
30 | return CallLog(
31 | profile,
32 | name: entry.name ?? '',
33 | number: entry.number ?? '',
34 | simDisplayName: entry.simDisplayName ?? 'Unknown',
35 | date: DateTime.fromMillisecondsSinceEpoch(entry.timestamp ?? 0),
36 | duration: entry.duration.toString(),
37 | type: _convertFromInternalType(entry.callType ?? lib.CallType.unknown),
38 | accountId: entry.phoneAccountId ?? '',
39 | );
40 | }
41 |
42 | static CallType _convertFromInternalType(lib.CallType type) {
43 | return type == lib.CallType.incoming
44 | ? CallType.incoming
45 | : type == lib.CallType.outgoing
46 | ? CallType.outgoing
47 | : type == lib.CallType.rejected
48 | ? CallType.rejected
49 | : type == lib.CallType.blocked
50 | ? CallType.blocked
51 | : type == lib.CallType.missed
52 | ? CallType.missed
53 | : CallType.unknown;
54 | }
55 |
56 | String get displayName {
57 | if (name.isNotEmpty) {
58 | return name;
59 | } else if (number.isNotEmpty) {
60 | return number;
61 | } else {
62 | return 'Unknown';
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/lib/ui/views/common/contact_tile.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:google_fonts/google_fonts.dart';
3 | import 'package:hugeicons/hugeicons.dart';
4 | import 'package:revo/constants/routes.dart';
5 | import 'package:revo/extensions/theme.dart';
6 | import 'package:revo/model/contact.dart';
7 | import 'package:revo/ui/popups/sim_choose_popup.dart';
8 | import 'package:revo/utils/circle_profile.dart';
9 | import 'package:revo/utils/rounded_icon_btn.dart';
10 |
11 | class ContactTile extends StatelessWidget {
12 | final Contact contact;
13 |
14 | const ContactTile({
15 | super.key,
16 | required this.contact,
17 | });
18 |
19 | @override
20 | Widget build(BuildContext context) {
21 | return ListTile(
22 | onTap: () async {
23 | if (contact.phones.isNotEmpty) {
24 | simChooserDialog(context, contact.phones[0]);
25 | }
26 | },
27 | shape: RoundedRectangleBorder(
28 | borderRadius: BorderRadius.circular(20),
29 | ),
30 | leading: CircleProfile(
31 | name: contact.fullName,
32 | profile: contact.photo,
33 | size: 30,
34 | ),
35 | title: Text(
36 | contact.displayName,
37 | style: GoogleFonts.raleway(
38 | fontSize: 16,
39 | color: context.colorScheme.onSurface.withAlpha(200),
40 | ),
41 | ),
42 | subtitle: Text(
43 | contact.phones
44 | .toString()
45 | .substring(1, contact.phones.toString().length - 1),
46 | style: GoogleFonts.raleway(
47 | fontSize: 12,
48 | color: context.colorScheme.onSurface.withAlpha(200),
49 | ),
50 | ),
51 | trailing: RoundedIconButton(
52 | context,
53 | icon: HugeIcons.strokeRoundedArrowRight01,
54 | size: 30,
55 | onTap: () async {
56 | await Navigator.of(context).pushNamed(
57 | contactInfoRoute,
58 | arguments: contact,
59 | );
60 | },
61 | ),
62 | );
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/privacy-policy.md:
--------------------------------------------------------------------------------
1 | Privacy Policy for Rivo Dialer App
2 |
3 | We value your privacy and are committed to protecting the personal information you share with us. This Privacy Policy explains how we handle and protect any data you provide while using our app.
4 |
5 | 1. Information We Collect
6 |
7 | Rivo does not collect any personal information from users. The app does not track or store data such as your phone number, contacts, or call logs externally.
8 |
9 | 2. Data Storage
10 |
11 | All data, including call logs and user preferences, is stored locally on the user’s device. We do not store any user data on remote servers, and everything is kept on your device for your privacy and security.
12 |
13 | 3. Data Sharing
14 |
15 | Rivo does not share any data with third parties. All information stored within the app remains private and is not transmitted or shared externally in any form.
16 |
17 | 4. Open Source
18 |
19 | Rivo is an open-source application. This means that the app's source code is publicly available for review. Users are encouraged to inspect the code, and anyone can contribute to the development of the app.
20 |
21 | 5. Privacy Policy Changes
22 |
23 | We may update this Privacy Policy from time to time. If any changes are made, users will be notified through the app. Please check for notifications regularly to stay informed about our privacy practices.
24 |
25 | 6. User Rights
26 |
27 | Since no personal data is collected, users are not required to manage or delete any stored data from our end. All data remains solely on your device, and you have full control over your data at all times.
28 |
29 | 7. Data Security
30 |
31 | As we do not store data remotely, security concerns regarding your personal information are minimized. However, we encourage users to protect their devices with proper security measures to safeguard their privacy.
32 |
33 | 8. Contact Information
34 |
35 | If you have any questions about this Privacy Policy or the app's privacy practices, please feel free to contact us at:
36 | Email: user.grinch@gmail.com
37 |
38 | By using Rivo, you agree to this Privacy Policy. If you do not agree with the terms outlined here, we recommend not using the app.
39 |
--------------------------------------------------------------------------------
/lib/utils/circle_profile.dart:
--------------------------------------------------------------------------------
1 | import 'dart:typed_data';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:google_fonts/google_fonts.dart';
5 | import 'package:hugeicons/hugeicons.dart';
6 | import 'package:revo/constants/pref.dart';
7 | import 'package:revo/extensions/theme.dart';
8 | import 'package:revo/services/prefservice.dart';
9 |
10 | class CircleProfile extends StatefulWidget {
11 | final Uint8List? profile;
12 | final String name;
13 | final double size;
14 | const CircleProfile(
15 | {super.key, required this.name, this.profile, required this.size});
16 |
17 | @override
18 | State createState() => _CircleProfileState();
19 | }
20 |
21 | class _CircleProfileState extends State {
22 | @override
23 | void initState() {
24 | SharedPrefService().onPreferenceChanged.listen((key) {
25 | if (key == PREF_SHOW_FIRST_LETTER || key == PREF_SHOW_PICTURE_IN_AVATAR) {
26 | setState(() {});
27 | }
28 | });
29 | super.initState();
30 | }
31 |
32 | @override
33 | Widget build(BuildContext context) {
34 | bool showPic = widget.profile != null &&
35 | SharedPrefService().getBool(PREF_SHOW_PICTURE_IN_AVATAR, def: true);
36 |
37 | bool showFirstLetter = widget.name.isNotEmpty &&
38 | SharedPrefService().getBool(PREF_SHOW_FIRST_LETTER, def: true);
39 | return CircleAvatar(
40 | radius: widget.size,
41 | backgroundColor: context.colorScheme.secondaryContainer,
42 | backgroundImage: showPic ? MemoryImage(widget.profile!) : null,
43 | child: !showPic
44 | ? showFirstLetter
45 | ? Text(
46 | widget.name[0].toUpperCase(),
47 | style: GoogleFonts.raleway(
48 | fontSize: widget.size,
49 | fontWeight: FontWeight.w300,
50 | color: context.colorScheme.onSurface,
51 | ),
52 | )
53 | : Icon(
54 | HugeIcons.strokeRoundedUser,
55 | size: widget.size,
56 | color: context.colorScheme.onSurface,
57 | )
58 | : null,
59 | );
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/lib/utils/switch_tile.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | class SwitchTileWidget extends StatelessWidget {
4 | final String title;
5 | final String subtitle;
6 | final bool value;
7 | final ValueChanged onChanged;
8 | final bool isFirst;
9 | final bool isLast;
10 |
11 | const SwitchTileWidget({
12 | super.key,
13 | required this.title,
14 | required this.subtitle,
15 | required this.value,
16 | required this.onChanged,
17 | this.isFirst = false,
18 | this.isLast = false,
19 | });
20 |
21 | @override
22 | Widget build(BuildContext context) {
23 | return Padding(
24 | padding: const EdgeInsets.all(2.0),
25 | child: Container(
26 | decoration: BoxDecoration(
27 | color:
28 | Theme.of(context).colorScheme.secondaryContainer.withAlpha(110),
29 | borderRadius: BorderRadius.vertical(
30 | top: isFirst ? const Radius.circular(15) : Radius.zero,
31 | bottom: isLast ? const Radius.circular(15) : Radius.zero,
32 | ),
33 | ),
34 | child: SwitchListTile(
35 | shape: RoundedRectangleBorder(
36 | borderRadius: BorderRadius.only(
37 | topLeft: isFirst ? const Radius.circular(15) : Radius.zero,
38 | topRight: isFirst ? const Radius.circular(15) : Radius.zero,
39 | bottomLeft: isLast ? const Radius.circular(15) : Radius.zero,
40 | bottomRight: isLast ? const Radius.circular(15) : Radius.zero,
41 | ),
42 | ),
43 | contentPadding:
44 | const EdgeInsets.symmetric(horizontal: 20.0, vertical: 2),
45 | title: Text(
46 | title,
47 | style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
48 | ),
49 | subtitle: Text(
50 | subtitle,
51 | style: TextStyle(
52 | fontSize: 12,
53 | fontWeight: FontWeight.w500,
54 | color: Theme.of(context)
55 | .colorScheme
56 | .onSecondaryContainer
57 | .withAlpha(180),
58 | ),
59 | ),
60 | value: value,
61 | onChanged: onChanged,
62 | ),
63 | ),
64 | );
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/lib/services/prefservice.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'package:shared_preferences/shared_preferences.dart';
3 |
4 | class SharedPrefService {
5 | static final SharedPrefService _instance = SharedPrefService._internal();
6 |
7 | factory SharedPrefService() {
8 | return _instance;
9 | }
10 |
11 | SharedPrefService._internal();
12 |
13 | late SharedPreferences _prefs;
14 | final StreamController _prefChangesController =
15 | StreamController.broadcast();
16 |
17 | Stream get onPreferenceChanged => _prefChangesController.stream;
18 |
19 | Future init() async {
20 | _prefs = await SharedPreferences.getInstance();
21 | }
22 |
23 | Future saveString(String key, String value) async {
24 | await _prefs.setString(key, value);
25 | _prefChangesController.add(key);
26 | }
27 |
28 | String? getString(String key) {
29 | return _prefs.getString(key);
30 | }
31 |
32 | Future saveBool(String key, bool value) async {
33 | await _prefs.setBool(key, value);
34 | _prefChangesController.add(key);
35 | }
36 |
37 | bool getBool(String key, {bool def = false}) {
38 | return _prefs.getBool(key) ?? def;
39 | }
40 |
41 | Future saveInt(String key, int value) async {
42 | await _prefs.setInt(key, value);
43 | _prefChangesController.add(key);
44 | }
45 |
46 | int? getInt(String key) {
47 | return _prefs.getInt(key);
48 | }
49 |
50 | Future saveDouble(String key, double value) async {
51 | await _prefs.setDouble(key, value);
52 | _prefChangesController.add(key);
53 | }
54 |
55 | double? getDouble(String key) {
56 | return _prefs.getDouble(key);
57 | }
58 |
59 | Future saveStringList(String key, List value) async {
60 | await _prefs.setStringList(key, value);
61 | _prefChangesController.add(key);
62 | }
63 |
64 | List? getStringList(String key) {
65 | return _prefs.getStringList(key);
66 | }
67 |
68 | Future remove(String key) async {
69 | await _prefs.remove(key);
70 | _prefChangesController.add(key);
71 | }
72 |
73 | Future clear() async {
74 | await _prefs.clear();
75 | _prefChangesController.add("all");
76 | }
77 |
78 | /// Dispose the stream when not needed
79 | void dispose() {
80 | _prefChangesController.close();
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/lib/ui/popups/welcome_changelog.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:google_fonts/google_fonts.dart';
3 | import 'package:revo/extensions/theme.dart';
4 |
5 | Widget welcomePopup(
6 | BuildContext context,
7 | String version,
8 | String changelog,
9 | ) {
10 | return Dialog(
11 | backgroundColor: context.colorScheme.surfaceContainer,
12 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
13 | child: Padding(
14 | padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 24.0),
15 | child: Padding(
16 | padding: const EdgeInsets.symmetric(vertical: 12.0),
17 | child: Column(
18 | mainAxisSize: MainAxisSize.min,
19 | crossAxisAlignment: CrossAxisAlignment.center,
20 | children: [
21 | Image.asset(
22 | 'assets/icon.png',
23 | width: 70,
24 | height: 70,
25 | fit: BoxFit.cover,
26 | ),
27 | SizedBox(height: 16),
28 | Text(
29 | 'Rivo',
30 | style: GoogleFonts.raleway(
31 | color: context.colorScheme.onSurface,
32 | fontSize: 30,
33 | fontWeight: FontWeight.bold,
34 | ),
35 | ),
36 | Text(
37 | 'Version: $version',
38 | style: GoogleFonts.raleway(
39 | color: context.colorScheme.onSurfaceVariant,
40 | fontSize: 16,
41 | ),
42 | ),
43 | SizedBox(height: 50),
44 | Align(
45 | alignment: Alignment.centerLeft,
46 | child: Text(
47 | 'Changelog:',
48 | style: GoogleFonts.raleway(
49 | color: context.colorScheme.onSurface,
50 | fontSize: 16,
51 | fontWeight: FontWeight.w600,
52 | ),
53 | ),
54 | ),
55 | SizedBox(height: 8),
56 | Text(
57 | changelog,
58 | style: GoogleFonts.raleway(
59 | color: context.colorScheme.onSurfaceVariant,
60 | fontSize: 16,
61 | height: 1.3,
62 | ),
63 | ),
64 | ],
65 | ),
66 | ),
67 | ),
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/lib/ui/popups/number_choose_popup.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:hugeicons/hugeicons.dart';
3 | import 'package:revo/extensions/theme.dart';
4 | import 'package:revo/utils/center_text.dart';
5 |
6 | Widget numberChooserDialog(
7 | BuildContext context,
8 | List numbers,
9 | void Function(String)? onTap,
10 | ) {
11 | return Dialog(
12 | backgroundColor: context.colorScheme.surfaceContainer,
13 | shape: RoundedRectangleBorder(
14 | borderRadius: BorderRadius.circular(24),
15 | ),
16 | alignment: Alignment.bottomCenter,
17 | child: Padding(
18 | padding: const EdgeInsets.all(24.0),
19 | child: Column(
20 | mainAxisSize: MainAxisSize.min,
21 | crossAxisAlignment: CrossAxisAlignment.start,
22 | children: [
23 | CenterText(
24 | text: "Choose a number",
25 | size: 24,
26 | ),
27 | const SizedBox(height: 8),
28 | Column(
29 | children: numbers.map((number) {
30 | return _buildNumberOption(context, number, onTap);
31 | }).toList(),
32 | ),
33 | ],
34 | ),
35 | ),
36 | );
37 | }
38 |
39 | Widget _buildNumberOption(
40 | BuildContext context,
41 | String number,
42 | void Function(String)? onTap,
43 | ) {
44 | return Card(
45 | elevation: 0,
46 | margin: const EdgeInsets.symmetric(vertical: 4),
47 | shape: RoundedRectangleBorder(
48 | borderRadius: BorderRadius.circular(24),
49 | ),
50 | color: context.colorScheme.secondaryContainer,
51 | child: InkWell(
52 | onTap: () {
53 | if (onTap != null) {
54 | onTap(number);
55 | }
56 | },
57 | borderRadius: BorderRadius.circular(20),
58 | child: Padding(
59 | padding: const EdgeInsets.all(6.0),
60 | child: Row(
61 | children: [
62 | CircleAvatar(
63 | radius: 18,
64 | backgroundColor: Theme.of(context).colorScheme.primary,
65 | child: Icon(
66 | HugeIcons.strokeRoundedSmartPhone01,
67 | color: context.colorScheme.onPrimary,
68 | size: 18,
69 | ),
70 | ),
71 | const SizedBox(width: 16),
72 | CenterText(
73 | text: number,
74 | size: 18,
75 | ),
76 | ],
77 | ),
78 | ),
79 | ),
80 | );
81 | }
82 |
--------------------------------------------------------------------------------
/lib/ui/views/dialpad_view/dial_btn.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:google_fonts/google_fonts.dart';
3 | import 'package:revo/constants/pref.dart';
4 | import 'package:revo/extensions/theme.dart';
5 | import 'package:flutter_dtmf/dtmf.dart';
6 | import 'package:revo/services/prefservice.dart';
7 | import 'package:revo/utils/utils.dart';
8 |
9 | class DialPadButton extends StatefulWidget {
10 | final String mainText;
11 | final String? subText;
12 | final Function(String) onUpdate;
13 |
14 | const DialPadButton({
15 | required this.mainText,
16 | this.subText,
17 | required this.onUpdate,
18 | super.key,
19 | });
20 |
21 | @override
22 | State createState() => _DialPadButtonState();
23 | }
24 |
25 | class _DialPadButtonState extends State {
26 | @override
27 | Widget build(BuildContext context) {
28 | bool letters = SharedPrefService().getBool(PREF_DIALPAD_LETTERS, def: true);
29 | double textSz = widget.mainText == "*" ? 45 : 20;
30 |
31 | if (!letters) {
32 | textSz += 10;
33 | }
34 | return TextButton(
35 | style: TextButton.styleFrom(
36 | elevation: 0,
37 | shape: const RoundedRectangleBorder(
38 | borderRadius: BorderRadius.all(Radius.circular(50)),
39 | ),
40 | backgroundColor: context.colorScheme.secondaryContainer.withAlpha(150),
41 | overlayColor: context.colorScheme.onSurface,
42 | ),
43 | onPressed: () async {
44 | if (SharedPrefService().getBool(PREF_DTMF_TONE, def: true)) {
45 | await Dtmf.playTone(digits: widget.mainText, volume: 3);
46 | }
47 | hapticVibration();
48 | widget.onUpdate(widget.mainText);
49 | },
50 | child: Column(
51 | mainAxisAlignment: MainAxisAlignment.center,
52 | children: [
53 | Expanded(
54 | child: Text(
55 | widget.mainText,
56 | style: GoogleFonts.raleway(
57 | fontSize: textSz,
58 | fontWeight: FontWeight.normal,
59 | color: context.colorScheme.onSurface,
60 | ),
61 | ),
62 | ),
63 | if (widget.subText != null)
64 | Text(
65 | widget.subText!,
66 | style: GoogleFonts.raleway(
67 | fontSize: 12,
68 | color: context.colorScheme.onSurface,
69 | ),
70 | ),
71 | ],
72 | ),
73 | );
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/lib/ui/theme/handler.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:google_fonts/google_fonts.dart';
3 | import 'package:revo/constants/pref.dart';
4 | import 'package:revo/services/prefservice.dart';
5 |
6 | ThemeData getTheme(
7 | ColorScheme? dynamicCol,
8 | ThemeProvider provider,
9 | bool isDark,
10 | ) {
11 | ColorScheme defScheme = ColorScheme.fromSeed(
12 | seedColor: Colors.blueAccent.shade100,
13 | brightness: isDark ? Brightness.dark : Brightness.light,
14 | );
15 | if (provider.isAmoled) {
16 | return _getAmoledTheme();
17 | }
18 |
19 | return ThemeData(
20 | colorScheme: provider.isDynamic ? dynamicCol ?? defScheme : defScheme,
21 | useMaterial3: true,
22 | textTheme: GoogleFonts.cabinTextTheme(),
23 | );
24 | }
25 |
26 | ThemeData _getAmoledTheme() {
27 | return ThemeData(
28 | brightness: Brightness.dark,
29 | scaffoldBackgroundColor: Colors.black,
30 | primaryColor: Colors.black,
31 | cardColor: Colors.black,
32 | dialogBackgroundColor: Colors.black,
33 | appBarTheme: AppBarTheme(
34 | backgroundColor: Colors.black,
35 | foregroundColor: Colors.white,
36 | ),
37 | colorScheme: ColorScheme.dark(
38 | primary: Color(0xFF000000),
39 | onPrimary: Color(0xFFFFFFFF),
40 | primaryContainer: Color(0xFF121212),
41 | secondary: Color(0xFF1C1C1C),
42 | onSecondary: Color(0xFFD3D3D3),
43 | secondaryContainer: Color(0xFF2B2B2B),
44 | onSecondaryContainer: Color(0xFFFFFFFF),
45 | surface: Color(0xFF121212),
46 | onSurface: Color(0xFFE0E0E0),
47 | surfaceContainer: Color(0xFF1A1A1A),
48 | ),
49 | textTheme: GoogleFonts.cabinTextTheme(),
50 | );
51 | }
52 |
53 | class ThemeProvider extends ChangeNotifier {
54 | bool _isAmoled = false;
55 | bool _isDynamic = false;
56 |
57 | bool get isAmoled => _isAmoled;
58 | bool get isDynamic => _isDynamic;
59 |
60 | Future initTheme() async {
61 | await SharedPrefService().init();
62 | _isAmoled = SharedPrefService().getBool(PREF_AMOLED_DARK_MODE, def: false);
63 | _isDynamic = SharedPrefService().getBool(PREF_MATERIAL_THEMING, def: false);
64 | notifyListeners();
65 | }
66 |
67 | void toggleDynamicColors() {
68 | _isDynamic = !_isDynamic;
69 | SharedPrefService().saveBool(PREF_MATERIAL_THEMING, _isDynamic);
70 | notifyListeners();
71 | }
72 |
73 | void toggleAmoledColors() {
74 | _isAmoled = !_isAmoled;
75 | SharedPrefService().saveBool(PREF_AMOLED_DARK_MODE, _isAmoled);
76 | notifyListeners();
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/lib/ui/views/settings_view/call.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:google_fonts/google_fonts.dart';
3 | import 'package:hugeicons/hugeicons.dart';
4 | import 'package:revo/extensions/theme.dart';
5 | import 'package:revo/utils/menu_tile.dart';
6 | import 'package:revo/utils/switch_tile.dart';
7 |
8 | class CallView extends StatefulWidget {
9 | const CallView({super.key});
10 |
11 | @override
12 | State createState() => _CallViewState();
13 | }
14 |
15 | class _CallViewState extends State {
16 | bool disableMaterialYou = false;
17 | bool hideAvatarInitials = false;
18 | bool showAvatarPictures = true;
19 | bool iconOnlyBottomNav = false;
20 | bool enableCustomCallScreen = false;
21 |
22 | @override
23 | Widget build(BuildContext context) {
24 | return Scaffold(
25 | appBar: AppBar(
26 | leading: IconButton(
27 | icon: Icon(HugeIcons.strokeRoundedArrowLeft01),
28 | onPressed: () => Navigator.of(context).pop(),
29 | ),
30 | title: Text(
31 | 'Call Settings',
32 | style: GoogleFonts.raleway(
33 | fontSize: 20,
34 | fontWeight: FontWeight.w600,
35 | color: context.colorScheme.onSurface,
36 | ),
37 | ),
38 | ),
39 | body: ListView(
40 | padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
41 | children: [
42 | SwitchTileWidget(
43 | title: "Speed dial",
44 | subtitle: "Directly call someone by holding a dialpad key",
45 | value: disableMaterialYou,
46 | onChanged: (value) {
47 | setState(() {
48 | disableMaterialYou = value;
49 | });
50 | },
51 | isFirst: true),
52 | MenuTile(
53 | title: 'Speed dial Settings',
54 | subtitle: '',
55 | icon: HugeIcons.strokeRoundedDialpadSquare02,
56 | onTap: () {},
57 | isLast: true,
58 | ),
59 | const SizedBox(
60 | height: 10,
61 | ),
62 | SwitchTileWidget(
63 | title: "T9 Dialing",
64 | subtitle: "Predicts words from numeric keypad inputs",
65 | value: enableCustomCallScreen,
66 | onChanged: (value) {
67 | setState(() {
68 | enableCustomCallScreen = value;
69 | });
70 | },
71 | isFirst: true,
72 | isLast: true),
73 | ],
74 | ),
75 | );
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Rivo
2 |
3 | **Rivo** is a modern, feature-rich dialer app built using Flutter. Designed for seamless communication, Rivo provides a sleek and user-friendly interface for managing calls and contacts.
4 |
5 | ## Features
6 | - **Fast Dialing:** Quickly dial numbers with an intuitive interface.
7 | - **Contact Management:** Easily manage and organize your contacts.
8 | - **Call History:** Access and manage your call logs effortlessly.
9 | - **Customizable UI:** Enjoy a visually appealing and customizable design.
10 |
11 | ## Tech Stack
12 | - **Framework:** Flutter
13 | - **Language:** Dart
14 |
15 | ## Preview
16 |
17 | ### Screenshots
18 |
19 | Show Images
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | ## Getting Started
29 | 1. Clone the repository:
30 | ```bash
31 | git clone https://github.com/user-grinch/Rivo.git
32 | ```
33 | 2. Navigate to the project directory:
34 | ```bash
35 | cd Rivo
36 | ```
37 | 3. Install dependencies:
38 | ```bash
39 | flutter pub get
40 | ```
41 | 4. Run the app in debug mode:
42 | ```bash
43 | flutter run
44 | ```
45 | 5. Build the app for release:
46 | ```bash
47 | flutter build apk
48 | ```
49 |
50 | ## Contribution Guidelines
51 | We welcome contributions to improve Rivo! Follow these steps to contribute:
52 | 1. Fork the repository.
53 | 2. Create a new branch for your feature or bug fix:
54 | ```bash
55 | git checkout -b feature-name
56 | ```
57 | 3. Make your changes and commit them with a descriptive message:
58 | ```bash
59 | git commit -m "Add a brief description of your changes"
60 | ```
61 | 4. Push your branch to your forked repository:
62 | ```bash
63 | git push origin feature-name
64 | ```
65 | 5. Create a pull request on the main repository, describing your changes.
66 |
67 | ## License
68 | This project is licensed under the GNU General Public License v3.0. See the [LICENSE](LICENSE) file for details.
69 |
70 | ---
71 |
72 | Made with ❤️ using Flutter.
73 |
--------------------------------------------------------------------------------
/lib/utils/menu_tile.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | class MenuTile extends StatelessWidget {
4 | final String title;
5 | final String subtitle;
6 | final IconData icon;
7 | final VoidCallback? onTap;
8 | final bool isFirst;
9 | final bool isLast;
10 |
11 | const MenuTile({
12 | super.key,
13 | required this.title,
14 | required this.subtitle,
15 | required this.icon,
16 | required this.onTap,
17 | this.isFirst = false,
18 | this.isLast = false,
19 | });
20 |
21 | @override
22 | Widget build(BuildContext context) {
23 | return Padding(
24 | padding: const EdgeInsets.all(2.0),
25 | child: Container(
26 | decoration: BoxDecoration(
27 | color: onTap == null
28 | ? Theme.of(context).disabledColor
29 | : Theme.of(context).colorScheme.secondaryContainer.withAlpha(110),
30 | borderRadius: BorderRadius.vertical(
31 | top: isFirst ? const Radius.circular(15) : Radius.zero,
32 | bottom: isLast ? const Radius.circular(15) : Radius.zero,
33 | ),
34 | ),
35 | child: ListTile(
36 | shape: RoundedRectangleBorder(
37 | borderRadius: BorderRadius.only(
38 | topLeft: isFirst ? const Radius.circular(15) : Radius.zero,
39 | topRight: isFirst ? const Radius.circular(15) : Radius.zero,
40 | bottomLeft: isLast ? const Radius.circular(15) : Radius.zero,
41 | bottomRight: isLast ? const Radius.circular(15) : Radius.zero,
42 | ),
43 | ),
44 | contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
45 | leading: Icon(
46 | icon,
47 | color: onTap == null
48 | ? Theme.of(context).disabledColor
49 | : Theme.of(context).colorScheme.onSecondaryContainer,
50 | size: 30,
51 | ),
52 | title: Text(
53 | title,
54 | style: TextStyle(
55 | fontSize: 16,
56 | fontWeight: FontWeight.w500,
57 | color: onTap == null ? Theme.of(context).disabledColor : null),
58 | ),
59 | subtitle: Text(
60 | subtitle,
61 | style: TextStyle(
62 | fontSize: 12,
63 | color: onTap == null
64 | ? Theme.of(context).disabledColor
65 | : Theme.of(context)
66 | .colorScheme
67 | .onSecondaryContainer
68 | .withAlpha(128),
69 | ),
70 | ),
71 | onTap: onTap,
72 | ),
73 | ),
74 | );
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/lib/ui/views/home_view/fav_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_bloc/flutter_bloc.dart';
3 | import 'package:google_fonts/google_fonts.dart';
4 | import 'package:revo/extensions/theme.dart';
5 | import 'package:revo/model/contact.dart';
6 | import 'package:revo/services/cubit/contact_service.dart';
7 | import 'package:revo/ui/views/contactinfo_view.dart';
8 | import 'package:revo/utils/center_text.dart';
9 | import 'package:revo/utils/circle_profile.dart';
10 |
11 | class FavView extends StatelessWidget {
12 | const FavView({super.key});
13 |
14 | @override
15 | Widget build(BuildContext context) {
16 | return BlocBuilder>(
17 | builder: (context, state) {
18 | var stars = context.read().filterByStars();
19 | if (stars.isEmpty) {
20 | return CenterText(text: 'No favorite contacts found');
21 | }
22 | return Padding(
23 | padding: const EdgeInsets.only(top: 30.0),
24 | child: GridView.builder(
25 | padding: const EdgeInsets.all(8.0),
26 | shrinkWrap: true,
27 | gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
28 | crossAxisCount: 3,
29 | crossAxisSpacing: 16.0,
30 | mainAxisSpacing: 16.0,
31 | childAspectRatio: 0.8,
32 | ),
33 | itemCount: stars.length,
34 | itemBuilder: (context, i) {
35 | return _buildFavs(context, stars[i]);
36 | },
37 | ),
38 | );
39 | },
40 | );
41 | }
42 |
43 | Widget _buildFavs(BuildContext context, Contact contact) {
44 | return InkWell(
45 | borderRadius: BorderRadius.all(Radius.circular(15)),
46 | onTap: () {
47 | Navigator.of(context)
48 | .push(MaterialPageRoute(builder: (_) => ContactInfoView(contact)));
49 | },
50 | child: Column(
51 | children: [
52 | CircleProfile(
53 | name: contact.displayName,
54 | profile: contact.photo,
55 | size: 45,
56 | ),
57 | const SizedBox(height: 10),
58 | Flexible(
59 | child: Center(
60 | child: Text(
61 | contact.displayName,
62 | textAlign: TextAlign.center,
63 | style: GoogleFonts.raleway(
64 | color: context.colorScheme.onSurface,
65 | fontSize: 14,
66 | ),
67 | overflow: TextOverflow.ellipsis,
68 | maxLines: 2,
69 | ),
70 | ),
71 | ),
72 | ],
73 | ),
74 | );
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/lib/ui/views/search_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:google_fonts/google_fonts.dart';
3 | import 'package:hugeicons/hugeicons.dart';
4 | import 'package:revo/extensions/theme.dart';
5 | import 'package:revo/ui/views/common/matched_view.dart';
6 | import 'package:revo/utils/center_text.dart';
7 |
8 | class SearchView extends StatefulWidget {
9 | const SearchView({super.key});
10 |
11 | @override
12 | State createState() => _SearchViewState();
13 | }
14 |
15 | class _SearchViewState extends State {
16 | late final TextEditingController _controller;
17 | String _searchQuery = '';
18 |
19 | late final ScrollController _scrollController;
20 | late final FocusNode _focusNode;
21 |
22 | @override
23 | void initState() {
24 | _controller = TextEditingController();
25 | _scrollController = ScrollController();
26 | _focusNode = FocusNode();
27 | WidgetsBinding.instance.addPostFrameCallback((_) {
28 | _focusNode.requestFocus();
29 | });
30 | super.initState();
31 | }
32 |
33 | @override
34 | void dispose() {
35 | _scrollController.dispose();
36 | _controller.dispose();
37 | _focusNode.dispose();
38 | super.dispose();
39 | }
40 |
41 | @override
42 | Widget build(BuildContext context) {
43 | return Scaffold(
44 | appBar: AppBar(
45 | leading: IconButton(
46 | icon: Icon(HugeIcons.strokeRoundedArrowLeft01),
47 | onPressed: () => Navigator.of(context).pop(),
48 | ),
49 | title: _buildSearchBox(),
50 | backgroundColor: context.colorScheme.surfaceTint.withAlpha(25),
51 | elevation: 0,
52 | ),
53 | body: SafeArea(
54 | child: Padding(
55 | padding: EdgeInsets.only(left: 16),
56 | child: MatchedView(
57 | scrollController: _scrollController,
58 | number: _searchQuery,
59 | ),
60 | ),
61 | ),
62 | );
63 | }
64 |
65 | Widget _buildSearchBox() {
66 | return Container(
67 | decoration: BoxDecoration(
68 | borderRadius: BorderRadius.circular(50),
69 | ),
70 | child: TextField(
71 | focusNode: _focusNode,
72 | controller: _controller,
73 | style: GoogleFonts.raleway(
74 | color: context.colorScheme.onSurface,
75 | ),
76 | decoration: InputDecoration(
77 | hintText: 'Search name/ number...',
78 | hintStyle: GoogleFonts.raleway(
79 | color: Colors.grey,
80 | ),
81 | border: InputBorder.none,
82 | ),
83 | onChanged: (query) {
84 | setState(() {
85 | _searchQuery = query;
86 | });
87 | },
88 | ),
89 | );
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/lib/services/activity_service.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:android_intent_plus/android_intent.dart';
4 | import 'package:permission_handler/permission_handler.dart';
5 | import 'package:url_launcher/url_launcher.dart';
6 |
7 | class ActivityService {
8 | ActivityService._internal();
9 | static final ActivityService _instance = ActivityService._internal();
10 | factory ActivityService() {
11 | return _instance;
12 | }
13 |
14 | Completer? _permissionsCompleter;
15 |
16 | Future requestPermissions() async {
17 | if (_permissionsCompleter == null) {
18 | _permissionsCompleter = Completer();
19 | try {
20 | await [
21 | Permission.sms,
22 | Permission.phone,
23 | Permission.contacts,
24 | Permission.camera,
25 | ].request();
26 | _permissionsCompleter!.complete();
27 | } catch (e) {
28 | _permissionsCompleter!.completeError(e);
29 | } finally {
30 | _permissionsCompleter = null;
31 | }
32 | } else {
33 | await _permissionsCompleter!.future;
34 | }
35 | }
36 |
37 | Future makePhoneCall(String phoneNumber, int simSlot) async {
38 | if (await Permission.phone.status.isGranted) {
39 | final caller = AndroidIntent(
40 | action: 'android.intent.action.CALL',
41 | data: 'tel:$phoneNumber',
42 | arguments: {
43 | 'com.android.phone.force.slot': true,
44 | 'com.android.phone.extra.slot': simSlot,
45 | });
46 | caller.launch();
47 | }
48 | }
49 |
50 | Future sendSMS(String phoneNumber) async {
51 | if (await Permission.sms.status.isGranted) {
52 | final Uri smsUri = Uri(scheme: 'sms', path: phoneNumber);
53 | if (await canLaunchUrl(smsUri)) {
54 | await launchUrl(smsUri);
55 | } else {
56 | throw 'Could not send SMS.';
57 | }
58 | }
59 | }
60 |
61 | Future makeVideoCall(String phoneNumber) async {
62 | if (await Permission.phone.status.isGranted) {
63 | final Uri videoCallUri = Uri(scheme: 'tel', path: phoneNumber);
64 | if (await canLaunchUrl(videoCallUri)) {
65 | await launchUrl(videoCallUri);
66 | } else {
67 | throw 'Could not start the video call.';
68 | }
69 | }
70 | }
71 |
72 | void openWhatsApp(String phoneNumber) async {
73 | final url = 'https://wa.me/$phoneNumber';
74 | if (await canLaunchUrl(Uri.parse(url))) {
75 | await launchUrl(Uri.parse(url));
76 | } else {
77 | throw 'Could not launch $url';
78 | }
79 | }
80 |
81 | void openTelegram(String phoneNumber) async {
82 | final url = 'https://t.me/$phoneNumber';
83 | if (await canLaunchUrl(Uri.parse(url))) {
84 | await launchUrl(Uri.parse(url));
85 | } else {
86 | throw 'Could not launch $url';
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/lib/model/contact.dart:
--------------------------------------------------------------------------------
1 | import 'dart:typed_data';
2 | import 'package:flutter_contacts/flutter_contacts.dart' as lib;
3 | import 'package:sticky_az_list/sticky_az_list.dart';
4 | import 'package:revo/utils/utils.dart';
5 |
6 | class Contact extends TaggedItem {
7 | String id;
8 | String displayName;
9 | Uint8List? thumbnail;
10 | Uint8List? photo;
11 | bool isStarred;
12 | String fullName;
13 | List phones;
14 | List emails;
15 | List addresses;
16 | List organizations;
17 | List websites;
18 | List socialMedias;
19 | List events;
20 | List notes;
21 | List accounts;
22 | List groups;
23 |
24 | Contact({
25 | required this.id,
26 | required this.displayName,
27 | this.thumbnail,
28 | this.photo,
29 | this.isStarred = false,
30 | required this.fullName,
31 | this.phones = const [],
32 | this.emails = const [],
33 | this.addresses = const [],
34 | this.organizations = const [],
35 | this.websites = const [],
36 | this.socialMedias = const [],
37 | this.events = const [],
38 | this.notes = const [],
39 | this.accounts = const [],
40 | this.groups = const [],
41 | });
42 |
43 | // TODO: Needs more work
44 | factory Contact.fromInternal(lib.Contact contact) {
45 | return Contact(
46 | id: contact.id,
47 | displayName: contact.displayName,
48 | thumbnail: contact.thumbnail,
49 | photo: contact.photo,
50 | isStarred: contact.isStarred,
51 | fullName:
52 | '${contact.name.first} ${contact.name.middle} ${contact.name.last}',
53 | phones: contact.phones.map((phone) => (phone.number)).toList(),
54 | emails: contact.emails,
55 | addresses: contact.addresses,
56 | organizations: contact.organizations,
57 | websites: contact.websites,
58 | socialMedias: contact.socialMedias,
59 | events: contact.events,
60 | notes: contact.notes,
61 | accounts: contact.accounts,
62 | groups: contact.groups,
63 | );
64 | }
65 |
66 | lib.Contact toInternal() {
67 | return lib.Contact(
68 | id: id,
69 | displayName: displayName,
70 | thumbnail: thumbnail,
71 | photo: photo,
72 | name: lib.Name(
73 | first: fullName.split(' ')[0],
74 | middle: fullName.split(' ').length > 2 ? fullName.split(' ')[1] : '',
75 | last: fullName.split(' ').last,
76 | ),
77 | phones: phones.map((p) => lib.Phone(p)).toList(),
78 | emails: emails,
79 | addresses: addresses,
80 | organizations: organizations,
81 | websites: websites,
82 | socialMedias: socialMedias,
83 | events: events,
84 | notes: notes,
85 | accounts: accounts,
86 | groups: groups,
87 | isStarred: isStarred,
88 | );
89 | }
90 |
91 | @override
92 | String sortName() => fullName;
93 | }
94 |
--------------------------------------------------------------------------------
/lib/ui/views/home_view/navigation_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:hugeicons/hugeicons.dart';
3 | import 'package:revo/constants/pref.dart';
4 | import 'package:revo/extensions/theme.dart';
5 | import 'package:revo/services/prefservice.dart';
6 |
7 | class NavigationView extends StatefulWidget {
8 | final PageController pageController;
9 |
10 | const NavigationView({super.key, required this.pageController});
11 |
12 | @override
13 | State createState() => _NavigationViewState();
14 | }
15 |
16 | class _NavigationViewState extends State {
17 | int _selectedIndex = 0;
18 | bool _prevFlag = false;
19 |
20 | @override
21 | void initState() {
22 | widget.pageController.addListener(() {
23 | setState(() {
24 | _selectedIndex = widget.pageController.page?.round() ?? 0;
25 | });
26 | });
27 | SharedPrefService().onPreferenceChanged.listen((key) {
28 | if (key == PREF_ICON_ONLY_BOTTOMSHEET ||
29 | key == PREF_ALWAYS_SHOW_SELECTED_IN_BOTTOMSHEET) {
30 | setState(() {});
31 | }
32 | });
33 | super.initState();
34 | }
35 |
36 | @override
37 | Widget build(BuildContext context) {
38 | bool onlyIcons = SharedPrefService().getBool(PREF_ICON_ONLY_BOTTOMSHEET);
39 | bool alwaysSelectedIcon =
40 | SharedPrefService().getBool(PREF_ALWAYS_SHOW_SELECTED_IN_BOTTOMSHEET);
41 | return NavigationBar(
42 | backgroundColor: context.colorScheme.surface,
43 | elevation: 3,
44 | indicatorColor: context.colorScheme.secondaryContainer,
45 | surfaceTintColor: context.colorScheme.surfaceTint,
46 | labelBehavior: onlyIcons
47 | ? alwaysSelectedIcon
48 | ? NavigationDestinationLabelBehavior.onlyShowSelected
49 | : NavigationDestinationLabelBehavior.alwaysHide
50 | : NavigationDestinationLabelBehavior.alwaysShow,
51 | destinations: [
52 | NavigationDestination(
53 | icon: Icon(HugeIcons.strokeRoundedClock01),
54 | label: 'Recents',
55 | selectedIcon: Icon(HugeIcons.strokeRoundedClock01),
56 | ),
57 | NavigationDestination(
58 | icon: Icon(HugeIcons.strokeRoundedUser),
59 | label: 'Contacts',
60 | selectedIcon: Icon(HugeIcons.strokeRoundedUser),
61 | ),
62 | NavigationDestination(
63 | icon: Icon(HugeIcons.strokeRoundedFavourite),
64 | label: 'Favorites',
65 | selectedIcon: Icon(HugeIcons.strokeRoundedFavourite),
66 | ),
67 | ],
68 | onDestinationSelected: (index) {
69 | setState(() {
70 | _selectedIndex = index;
71 | });
72 | widget.pageController.animateToPage(
73 | index,
74 | duration: Duration(milliseconds: 250),
75 | curve: Curves.easeInOut,
76 | );
77 | },
78 | selectedIndex: _selectedIndex,
79 | );
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/lib/ui/views/settings_view/sound.dart:
--------------------------------------------------------------------------------
1 | import 'package:android_intent_plus/android_intent.dart';
2 | import 'package:android_intent_plus/flag.dart';
3 | import 'package:flutter/material.dart';
4 | import 'package:google_fonts/google_fonts.dart';
5 | import 'package:hugeicons/hugeicons.dart';
6 | import 'package:revo/constants/pref.dart';
7 | import 'package:revo/extensions/theme.dart';
8 | import 'package:revo/services/prefservice.dart';
9 | import 'package:revo/utils/menu_tile.dart';
10 | import 'package:revo/utils/switch_tile.dart';
11 |
12 | class SoundView extends StatefulWidget {
13 | const SoundView({super.key});
14 |
15 | @override
16 | State createState() => _SoundViewState();
17 | }
18 |
19 | class _SoundViewState extends State {
20 | @override
21 | Widget build(BuildContext context) {
22 | return Scaffold(
23 | appBar: AppBar(
24 | leading: IconButton(
25 | icon: Icon(HugeIcons.strokeRoundedArrowLeft01),
26 | onPressed: () => Navigator.of(context).pop(),
27 | ),
28 | title: Text(
29 | 'Sound & Vibration',
30 | style: GoogleFonts.raleway(
31 | fontSize: 20,
32 | fontWeight: FontWeight.w600,
33 | color: context.colorScheme.onSurface,
34 | ),
35 | ),
36 | ),
37 | body: ListView(
38 | padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
39 | children: [
40 | SwitchTileWidget(
41 | title: "DTMF tone",
42 | subtitle: "Dialpad tone that plays during keypress",
43 | value: SharedPrefService().getBool(PREF_DTMF_TONE, def: true),
44 | onChanged: (value) {
45 | SharedPrefService().saveBool(PREF_DTMF_TONE, value);
46 | setState(() {});
47 | },
48 | isFirst: true),
49 | SwitchTileWidget(
50 | title: "Dialpad vibration",
51 | subtitle: "Dialpad vibration that plays during keypress",
52 | value:
53 | SharedPrefService().getBool(PREF_DIALPAD_VIBRATION, def: true),
54 | onChanged: (value) {
55 | SharedPrefService().saveBool(PREF_DIALPAD_VIBRATION, value);
56 | setState(() {});
57 | },
58 | isLast: true,
59 | ),
60 | const SizedBox(
61 | height: 10,
62 | ),
63 | MenuTile(
64 | title: 'Ringtone Settings',
65 | subtitle: '',
66 | icon: HugeIcons.strokeRoundedMusicNote02,
67 | onTap: () {
68 | final intent = AndroidIntent(
69 | action: 'android.settings.SOUND_SETTINGS',
70 | flags: [Flag.FLAG_ACTIVITY_NEW_TASK],
71 | );
72 | intent.launch();
73 | },
74 | isFirst: true,
75 | isLast: true,
76 | ),
77 | ],
78 | ),
79 | );
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'package:dynamic_color/dynamic_color.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:flutter/services.dart';
4 | import 'package:flutter_bloc/flutter_bloc.dart';
5 | import 'package:provider/provider.dart';
6 | import 'package:revo/constants/routes.dart';
7 | import 'package:revo/model/contact.dart';
8 | import 'package:revo/services/cubit/call_log_service.dart';
9 | import 'package:revo/services/cubit/contact_service.dart';
10 | import 'package:revo/services/cubit/mobile_service.dart';
11 | import 'package:revo/ui/theme/handler.dart';
12 | import 'package:revo/ui/views/call_screen.dart';
13 | import 'package:revo/ui/views/contactinfo_view.dart';
14 | import 'package:revo/ui/views/dialpad_view.dart';
15 | import 'package:revo/ui/views/history_view.dart';
16 | import 'package:revo/ui/views/home_view.dart';
17 | import 'package:revo/ui/views/search_view.dart';
18 | import 'package:revo/ui/views/settings_view.dart';
19 |
20 | void main() {
21 | WidgetsFlutterBinding.ensureInitialized();
22 | SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
23 | SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
24 | statusBarColor: Colors.transparent,
25 | systemNavigationBarColor: Colors.transparent));
26 |
27 | runApp(ChangeNotifierProvider(
28 | create: (context) => ThemeProvider(),
29 | child: DynamicColorBuilder(builder: (
30 | ColorScheme? lightDynamic,
31 | ColorScheme? darkDynamic,
32 | ) {
33 | return MultiProvider(
34 | providers: [
35 | BlocProvider(create: (context) => CallLogService(), lazy: false),
36 | BlocProvider(create: (context) => ContactService(), lazy: false),
37 | BlocProvider(create: (context) => MobileService(), lazy: false),
38 | ],
39 | child: Consumer(
40 | builder: (context, themeProvider, child) {
41 | return MaterialApp(
42 | debugShowCheckedModeBanner: false,
43 | theme: getTheme(lightDynamic, themeProvider, false),
44 | darkTheme: getTheme(darkDynamic, themeProvider, true),
45 | themeMode: ThemeMode.system,
46 | initialRoute: homeRoute,
47 | routes: {
48 | homeRoute: (context) => HomeView(),
49 | settingsRoute: (context) => SettingsView(),
50 | searchRoute: (context) => SearchView(),
51 | dialpadRoute: (context) => DialPadView(),
52 | callScreenRoute: (context) => CallScreenView(),
53 | contactInfoRoute: (context) => ContactInfoView(
54 | ModalRoute.of(context)!.settings.arguments as Contact),
55 | callHistoryRoute: (context) => HistoryView(
56 | numbers: ModalRoute.of(context)!.settings.arguments
57 | as List),
58 | },
59 | );
60 | },
61 | ),
62 | );
63 | }),
64 | ));
65 | }
66 |
--------------------------------------------------------------------------------
/lib/services/cubit/call_log_service.dart:
--------------------------------------------------------------------------------
1 | import 'dart:typed_data';
2 | import 'package:call_e_log/call_log.dart' as lib;
3 | import 'package:flutter/material.dart';
4 | import 'package:flutter_bloc/flutter_bloc.dart';
5 | import 'package:permission_handler/permission_handler.dart';
6 | import 'package:revo/model/call_log.dart';
7 | import 'package:revo/services/activity_service.dart';
8 | import 'package:revo/services/cubit/contact_service.dart';
9 | import 'package:revo/utils/utils.dart';
10 | import 'package:flutter_contacts/flutter_contacts.dart' as fc;
11 |
12 | class CallLogService extends Cubit> {
13 | CallLogService() : super([]) {
14 | _initialize();
15 | }
16 |
17 | Future _initialize() async {
18 | await ActivityService().requestPermissions();
19 | if (await Permission.phone.status.isGranted) {
20 | var fContact = (await fc.FlutterContacts.getContacts(
21 | withProperties: true,
22 | withAccounts: true,
23 | withThumbnail: true,
24 | ))
25 | .toList();
26 |
27 | List logs = (await lib.CallLog.get()).toList();
28 |
29 | var list = logs.map((e) {
30 | Uint8List? photo;
31 |
32 | try {
33 | String logNumber = normalizePhoneNumber(e.number!);
34 | fc.Contact contact = fContact.firstWhere((f) {
35 | return f.phones.any((g) {
36 | String contactNumber = normalizePhoneNumber(g.normalizedNumber);
37 | return logNumber.endsWith(contactNumber) ||
38 | logNumber == contactNumber;
39 | });
40 | });
41 |
42 | if ((e.name ?? '').isEmpty) {
43 | e.name =
44 | '${contact.name.first} ${contact.name.middle} ${contact.name.last}'
45 | .trim();
46 | }
47 | photo = contact.thumbnail;
48 | } catch (_) {
49 | photo = null;
50 | }
51 |
52 | return CallLog.fromEntry(entry: e, profile: photo);
53 | }).toList();
54 |
55 | emit(list);
56 | }
57 | }
58 |
59 | Future fetchData(BuildContext context) async {
60 | if (state.isNotEmpty && await Permission.phone.status.isGranted) {
61 | List list = state.map((e) {
62 | var contactList = context.read();
63 | var contact = contactList.findByNumber(e.number);
64 | e = CallLog(
65 | contact.photo,
66 | name: e.name,
67 | number: e.number,
68 | simDisplayName: e.simDisplayName,
69 | date: e.date,
70 | duration: e.duration,
71 | type: e.type,
72 | accountId: e.accountId,
73 | );
74 | }).toList();
75 | emit(state);
76 | }
77 | }
78 |
79 | List filterByNumber(List numbers) {
80 | final filteredLogs = state
81 | .where(
82 | (element) => numbers.any(
83 | (e) {
84 | String p1 = normalizePhoneNumber(e);
85 | String p2 = normalizePhoneNumber(element.number);
86 | return p1 == p2 || p1.endsWith(p2) || p2.endsWith(p1);
87 | },
88 | ),
89 | )
90 | .toList();
91 | return filteredLogs;
92 | }
93 |
94 | CallLogService refresh() {
95 | _initialize();
96 | return this;
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/lib/ui/views/home_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_bloc/flutter_bloc.dart';
3 | import 'package:hugeicons/hugeicons.dart';
4 | import 'package:revo/constants/routes.dart';
5 | import 'package:revo/extensions/theme.dart';
6 | import 'package:revo/services/cubit/contact_service.dart';
7 | import 'package:revo/services/prefservice.dart';
8 | import 'package:revo/ui/popups/welcome_changelog.dart';
9 | import 'package:revo/ui/theme/handler.dart';
10 | import 'package:revo/ui/views/common/constants.dart';
11 | import 'package:revo/ui/views/home_view/appbar_view.dart';
12 | import 'package:revo/ui/views/home_view/contacts_view.dart';
13 | import 'package:revo/ui/views/home_view/fav_view.dart';
14 | import 'package:revo/ui/views/home_view/navigation_view.dart';
15 | import 'package:revo/ui/views/home_view/recents_view.dart';
16 |
17 | class HomeView extends StatefulWidget {
18 | const HomeView({super.key});
19 |
20 | @override
21 | State createState() => _HomeViewState();
22 | }
23 |
24 | class _HomeViewState extends State {
25 | late final PageController _pageController;
26 | int _currentPage = 0;
27 |
28 | @override
29 | void initState() {
30 | _pageController = PageController();
31 | _pageController.addListener(() {
32 | final pageIndex = _pageController.page?.round() ?? 0;
33 | if (pageIndex != _currentPage) {
34 | setState(() {
35 | _currentPage = pageIndex;
36 | });
37 | }
38 | });
39 | super.initState();
40 |
41 | WidgetsBinding.instance.addPostFrameCallback((_) {
42 | Future.delayed(Duration(milliseconds: 100), () async {
43 | if (mounted) {
44 | await context.read().initTheme();
45 | await SharedPrefService().init();
46 | bool flag = SharedPrefService().getBool("WelcomeShown$version");
47 | if (!flag && mounted) {
48 | showDialog(
49 | context: context,
50 | builder: (context) => welcomePopup(context, version, changelog),
51 | );
52 | SharedPrefService().saveBool("WelcomeShown$version", true);
53 | }
54 | }
55 | });
56 | });
57 | }
58 |
59 | @override
60 | void dispose() {
61 | _pageController.dispose();
62 | super.dispose();
63 | }
64 |
65 | @override
66 | Widget build(BuildContext context) {
67 | return Scaffold(
68 | appBar: AppBarView(),
69 | body: Padding(
70 | padding: const EdgeInsets.symmetric(horizontal: 8.0),
71 | child: PageView(
72 | controller: _pageController,
73 | children: const [
74 | RecentsView(),
75 | ContactsView(),
76 | FavView(),
77 | ],
78 | ),
79 | ),
80 | bottomNavigationBar: NavigationView(pageController: _pageController),
81 | floatingActionButton: FloatingActionButton(
82 | backgroundColor: context.colorScheme.secondaryContainer,
83 | onPressed: () {
84 | if (_pageController.page == 1.0) {
85 | context.read().createNewContact();
86 | } else {
87 | Navigator.of(context).pushNamed(dialpadRoute);
88 | }
89 | },
90 | elevation: 1,
91 | child: Icon(_currentPage == 1.0
92 | ? HugeIcons.strokeRoundedUserAdd01
93 | : HugeIcons.strokeRoundedDialpadCircle02),
94 | ),
95 | );
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/lib/ui/views/home_view/appbar_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_bloc/flutter_bloc.dart';
3 | import 'package:google_fonts/google_fonts.dart';
4 | import 'package:hugeicons/hugeicons.dart';
5 | import 'package:revo/constants/routes.dart';
6 | import 'package:revo/extensions/theme.dart';
7 | import 'package:revo/services/cubit/contact_service.dart';
8 | import 'package:simple_barcode_scanner/simple_barcode_scanner.dart';
9 |
10 | class AppBarView extends StatelessWidget implements PreferredSizeWidget {
11 | @override
12 | final Size preferredSize;
13 |
14 | const AppBarView({super.key})
15 | : preferredSize = const Size.fromHeight(kToolbarHeight);
16 |
17 | @override
18 | Widget build(BuildContext context) {
19 | return AppBar(
20 | title: InkWell(
21 | onTap: () {
22 | Navigator.pushNamed(context, searchRoute);
23 | },
24 | borderRadius: BorderRadius.circular(50),
25 | splashColor: context.colorScheme.secondaryContainer,
26 | child: Container(
27 | decoration: BoxDecoration(
28 | color: context.colorScheme.secondaryContainer.withAlpha(200),
29 | borderRadius: BorderRadius.circular(50),
30 | ),
31 | child: Row(
32 | mainAxisAlignment: MainAxisAlignment.spaceBetween,
33 | children: [
34 | IconButton(
35 | onPressed: () async {
36 | // Navigator.pushNamed(context, qrScanRoute);
37 | String? res = await SimpleBarcodeScanner.scanBarcode(
38 | context,
39 | barcodeAppBar: const BarcodeAppBar(
40 | appBarTitle: 'Scan QR to add contact',
41 | centerTitle: false,
42 | enableBackButton: true,
43 | backButtonIcon:
44 | Icon(HugeIcons.strokeRoundedArrowLeft01),
45 | ),
46 | scanType: ScanType.qr,
47 | isShowFlashIcon: true,
48 | delayMillis: 1000,
49 | ) ??
50 | "";
51 |
52 | if (res.startsWith("BEGIN:VCARD") &&
53 | res.endsWith("END:VCARD")) {
54 | await context
55 | .read()
56 | .insertContactFromVCard(res);
57 | ScaffoldMessenger.of(context).showSnackBar(
58 | const SnackBar(
59 | content: Text('Contact added successfully!')),
60 | );
61 | } else {
62 | ScaffoldMessenger.of(context).showSnackBar(
63 | const SnackBar(content: Text('Invalid vCard format!')),
64 | );
65 | }
66 | },
67 | icon: Icon(
68 | HugeIcons.strokeRoundedQrCode,
69 | ),
70 | ),
71 | Text(
72 | 'Search in Rivo',
73 | style: GoogleFonts.raleway(
74 | fontSize: 20,
75 | color: context.colorScheme.onSurface,
76 | ),
77 | ),
78 | IconButton(
79 | onPressed: () {
80 | Navigator.pushNamed(context, settingsRoute);
81 | },
82 | icon: Icon(
83 | HugeIcons.strokeRoundedSettings03,
84 | ),
85 | ),
86 | ],
87 | ),
88 | ),
89 | ),
90 | centerTitle: true,
91 | );
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/lib/ui/popups/sim_choose_popup.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_bloc/flutter_bloc.dart';
3 | import 'package:google_fonts/google_fonts.dart';
4 | import 'package:revo/extensions/theme.dart';
5 | import 'package:revo/model/sim_card.dart';
6 | import 'package:revo/services/activity_service.dart';
7 | import 'package:revo/services/cubit/mobile_service.dart';
8 | import 'package:revo/utils/center_text.dart';
9 |
10 | Future simChooserDialog(BuildContext context, String number) {
11 | // If only one sim card, skip dialog
12 | if (context.read().getSimInfo.length == 1) {
13 | return ActivityService().makePhoneCall(number, 1);
14 | }
15 | return showDialog(
16 | context: context,
17 | builder: (context) => BlocBuilder>(
18 | builder: (context, state) {
19 | return Dialog(
20 | backgroundColor: context.colorScheme.surfaceContainer,
21 | shape: RoundedRectangleBorder(
22 | borderRadius: BorderRadius.circular(24),
23 | ),
24 | alignment: Alignment.bottomCenter,
25 | child: Padding(
26 | padding: const EdgeInsets.all(24),
27 | child: Column(
28 | mainAxisSize: MainAxisSize.min,
29 | crossAxisAlignment: CrossAxisAlignment.start,
30 | children: [
31 | CenterText(
32 | text: "Choose SIM for call",
33 | size: 24,
34 | ),
35 | const SizedBox(height: 8),
36 | Column(
37 | children:
38 | context.read().getSimInfo.map((sim) {
39 | return _buildSimCard(context, sim, number);
40 | }).toList(),
41 | ),
42 | ],
43 | ),
44 | ),
45 | );
46 | },
47 | ));
48 | }
49 |
50 | Widget _buildSimCard(BuildContext context, SimCard sim, String number) {
51 | return Card(
52 | elevation: 0,
53 | margin: const EdgeInsets.symmetric(vertical: 4),
54 | shape: RoundedRectangleBorder(
55 | borderRadius: BorderRadius.circular(24),
56 | ),
57 | color: context.colorScheme.primaryContainer,
58 | child: InkWell(
59 | onTap: () async {
60 | ActivityService().makePhoneCall(number, sim.simSlotIndex);
61 | Navigator.of(context).pop();
62 | },
63 | borderRadius: BorderRadius.circular(20),
64 | child: Padding(
65 | padding: const EdgeInsets.all(12.0),
66 | child: Row(
67 | children: [
68 | CircleAvatar(
69 | backgroundColor: Theme.of(context).colorScheme.primary,
70 | child: Text(
71 | "${sim.simSlotIndex + 1}",
72 | style: GoogleFonts.raleway(
73 | color: context.colorScheme.onSecondary,
74 | fontSize: 22,
75 | ),
76 | ),
77 | ),
78 | const SizedBox(width: 16),
79 | Column(
80 | crossAxisAlignment: CrossAxisAlignment.start,
81 | children: [
82 | CenterText(
83 | text: "${sim.carrierName} (${sim.countryCode.toUpperCase()})",
84 | size: 18,
85 | ),
86 | const SizedBox(height: 2),
87 | CenterText(
88 | text: sim.phoneNumber,
89 | size: 12,
90 | ),
91 | ],
92 | ),
93 | ],
94 | ),
95 | ),
96 | ),
97 | );
98 | }
99 |
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: revo
2 | description: "An android dialer application made with Flutter following MDY MD3 guidelines."
3 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev
4 |
5 | version: 1.0.0+1
6 |
7 | environment:
8 | sdk: '>=3.6.0 <4.0.0'
9 | flutter: '>=3.27.0'
10 |
11 | dependencies:
12 | flutter:
13 | sdk: flutter
14 |
15 | dynamic_color: 1.7.0
16 | google_fonts: ^6.2.1
17 | flutter_contacts: ^1.1.9+2
18 | permission_handler: ^11.3.1
19 | call_e_log: ^0.0.4
20 | intl: ^0.20.1
21 | share_plus: ^11.1.0
22 | qr_flutter: ^4.1.0
23 | bloc: ^9.0.0
24 | flutter_bloc: ^9.1.1
25 | provider: ^6.1.2
26 | flutter_sim_data: ^1.0.5
27 | url_launcher: ^6.3.1
28 | phone_state: ^2.1.1
29 | flutter_native_splash: ^2.4.4
30 | hugeicons: ^0.0.7
31 | shared_preferences: ^2.5.1
32 | font_awesome_flutter: ^10.8.0
33 | phosphor_flutter: ^2.1.0
34 | flutter_callkit_incoming: ^2.5.0
35 | simple_barcode_scanner: ^0.3.0
36 | flutter_dtmf:
37 | git:
38 | url: https://github.com/eopeter/flutter_dtmf.git
39 | ref: master
40 | android_intent_plus: ^5.3.1
41 | sticky_az_list:
42 | git:
43 | url: https://github.com/TetrixGauss/sticky_az_list_plus
44 | ref: '118f056'
45 |
46 | flutter_native_splash:
47 | image: 'assets/splash.png'
48 | color: "#FFFFFF"
49 | image_dark: 'assets/splash.png'
50 | color_dark: "#000000"
51 | # branding: 'assets/branding.png'
52 |
53 | android_12:
54 | image: 'assets/splash.png'
55 | color: "#FFFFFF"
56 | image_dark: 'assets/splash.png'
57 | color_dark: "#000000"
58 | # branding: 'assets/branding.png'
59 |
60 | dev_dependencies:
61 | flutter_test:
62 | sdk: flutter
63 |
64 | flutter_lints: ^5.0.0
65 |
66 | # For information on the generic Dart part of this file, see the
67 | # following page: https://dart.dev/tools/pub/pubspec
68 |
69 | # The following section is specific to Flutter packages.
70 | flutter:
71 |
72 | # The following line ensures that the Material Icons font is
73 | # included with your application, so that you can use the icons in
74 | # the material Icons class.
75 | uses-material-design: true
76 |
77 | # To add assets to your application, add an assets section, like this:
78 | assets:
79 | - assets/
80 | - assets/dialpad/
81 | - assets/icon.png
82 | - assets/branding.png
83 | - assets/static-bg.jpg
84 | - assets/dialpad/1.mp3
85 | - assets/dialpad/2.mp3
86 | - assets/dialpad/3.mp3
87 | - assets/dialpad/4.mp3
88 | - assets/dialpad/5.mp3
89 | - assets/dialpad/6.mp3
90 | - assets/dialpad/7.mp3
91 | - assets/dialpad/8.mp3
92 | - assets/dialpad/9.mp3
93 | - assets/dialpad/0.mp3
94 | - assets/dialpad/star.mp3
95 | - assets/dialpad/hash.mp3
96 |
97 | # An image asset can refer to one or more resolution-specific "variants", see
98 | # https://flutter.dev/to/resolution-aware-images
99 |
100 | # For details regarding adding assets from package dependencies, see
101 | # https://flutter.dev/to/asset-from-package
102 |
103 | # To add custom fonts to your application, add a fonts section here,
104 | # in this "flutter" section. Each entry in this list should have a
105 | # "family" key with the font family name, and a "fonts" key with a
106 | # list giving the asset and other descriptors for the font. For
107 | # example:
108 | # fonts:
109 | # - family: Schyler
110 | # fonts:
111 | # - asset: fonts/Schyler-Regular.ttf
112 | # - asset: fonts/Schyler-Italic.ttf
113 | # style: italic
114 | # - family: Trajan Pro
115 | # fonts:
116 | # - asset: fonts/TrajanPro.ttf
117 | # - asset: fonts/TrajanPro_Bold.ttf
118 | # weight: 700
119 | #
120 | # For details regarding fonts from package dependencies,
121 | # see https://flutter.dev/to/font-from-package
122 |
--------------------------------------------------------------------------------
/lib/ui/views/history_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_bloc/flutter_bloc.dart';
3 | import 'package:google_fonts/google_fonts.dart';
4 | import 'package:hugeicons/hugeicons.dart';
5 | import 'package:revo/extensions/datetime.dart';
6 | import 'package:revo/extensions/theme.dart';
7 | import 'package:revo/model/call_log.dart';
8 | import 'package:revo/model/call_type.dart';
9 | import 'package:revo/services/cubit/call_log_service.dart';
10 | import 'package:revo/utils/center_text.dart';
11 | import 'package:revo/utils/utils.dart';
12 |
13 | class HistoryView extends StatefulWidget {
14 | final List numbers;
15 |
16 | const HistoryView({super.key, required this.numbers});
17 |
18 | @override
19 | State createState() => _HistoryViewState();
20 | }
21 |
22 | class _HistoryViewState extends State {
23 | late ScrollController _controller;
24 |
25 | @override
26 | void initState() {
27 | _controller = ScrollController();
28 | super.initState();
29 | }
30 |
31 | @override
32 | void dispose() {
33 | _controller.dispose();
34 | super.dispose();
35 | }
36 |
37 | @override
38 | Widget build(BuildContext context) {
39 | return Scaffold(
40 | appBar: AppBar(
41 | leading: IconButton(
42 | icon: Icon(HugeIcons.strokeRoundedArrowLeft01),
43 | onPressed: () => Navigator.of(context).pop(),
44 | ),
45 | title: const Text('Call History'),
46 | ),
47 | body: BlocBuilder>(
48 | builder: (BuildContext context, List state) {
49 | var logs =
50 | context.read().filterByNumber(widget.numbers);
51 | if (logs.isEmpty) {
52 | return CenterText(
53 | text: 'No call logs found.',
54 | );
55 | }
56 | return ListView.builder(
57 | itemCount: logs.length,
58 | itemBuilder: (context, i) => _displayHistory(context, logs[i]),
59 | );
60 | },
61 | ),
62 | );
63 | }
64 |
65 | Widget _displayHistory(BuildContext context, CallLog history) {
66 | String underlineText =
67 | '${history.type.getText()} ${convertSecondsToHMS(int.parse(history.duration))}';
68 |
69 | return Padding(
70 | padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 16),
71 | child: Container(
72 | decoration: BoxDecoration(
73 | color: context.colorScheme.secondaryContainer.withAlpha(100),
74 | shape: BoxShape.rectangle,
75 | borderRadius: BorderRadius.circular(25),
76 | ),
77 | child: ListTile(
78 | leading: Container(
79 | width: 50,
80 | height: 50,
81 | decoration: BoxDecoration(
82 | color: context.colorScheme.primary.withAlpha(25),
83 | shape: BoxShape.circle,
84 | ),
85 | child: Icon(history.type.getIcon(),
86 | color: history.type.getColor(), size: 28),
87 | ),
88 | title: Text(
89 | history.date.getContextAwareDateTime(),
90 | style: GoogleFonts.raleway(fontSize: 16),
91 | ),
92 | subtitle: Column(
93 | crossAxisAlignment: CrossAxisAlignment.start,
94 | children: [
95 | Text(
96 | history.simDisplayName,
97 | style: const TextStyle(color: Colors.grey),
98 | ),
99 | Text(
100 | underlineText,
101 | style: const TextStyle(color: Colors.grey),
102 | ),
103 | Text(
104 | history.number,
105 | style: const TextStyle(color: Colors.grey),
106 | ),
107 | ],
108 | ),
109 | ),
110 | ),
111 | );
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
23 |
31 |
32 |
33 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/lib/services/backgroundservice.dart2:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:convert';
3 | import 'dart:ui';
4 |
5 | import 'package:contest_flow/modal/contestdata.dart';
6 | import 'package:contest_flow/services/notificationservice.dart';
7 | import 'package:contest_flow/services/prefservice.dart';
8 | import 'package:flutter/material.dart';
9 | import 'package:flutter_background_service/flutter_background_service.dart';
10 | import 'package:intl/intl.dart';
11 |
12 | class BackgroundService {
13 | static Future init() async {
14 | final service = FlutterBackgroundService();
15 |
16 | await service.configure(
17 | iosConfiguration: IosConfiguration(
18 | autoStart: true,
19 | onForeground: onStart,
20 | onBackground: onIosBackground,
21 | ),
22 | androidConfiguration: AndroidConfiguration(
23 | autoStart: true,
24 | onStart: onStart,
25 | isForegroundMode: false,
26 | autoStartOnBoot: true,
27 | ),
28 | );
29 | }
30 | }
31 |
32 | @pragma('vm:entry-point')
33 | Future onIosBackground(ServiceInstance service) async {
34 | WidgetsFlutterBinding.ensureInitialized();
35 | DartPluginRegistrant.ensureInitialized();
36 |
37 | return true;
38 | }
39 |
40 | void scheduleDailyNotification() {
41 | DateTime now = DateTime.now();
42 | DateTime next8AM = DateTime(now.year, now.month, now.day, 8, 0, 0);
43 |
44 | if (now.isAfter(next8AM)) {
45 | next8AM = next8AM.add(const Duration(days: 1));
46 | }
47 |
48 | Timer.periodic(const Duration(days: 1), (timer) {
49 | if (SharedPrefService().getBool('daily_update')) {
50 | String? contestsJson = SharedPrefService().getString('contests');
51 | if (contestsJson != null) {
52 | List contestsData = jsonDecode(contestsJson);
53 | List contests =
54 | contestsData.map((e) => ContestData.fromJson(e)).toList();
55 |
56 | String str = "";
57 | for (var e in contests) {
58 | final DateTime startTime =
59 | DateTime.fromMillisecondsSinceEpoch(e.startTimeSeconds * 1000);
60 | final DateTime endTime =
61 | startTime.add(Duration(seconds: e.durationSeconds));
62 |
63 | final String formattedDate =
64 | DateFormat('EE, dd MMM').format(startTime);
65 | final String formattedTime =
66 | "${DateFormat('h:mm a ').format(startTime)} - ${DateFormat('h:mm a').format(endTime)}";
67 | str += "$formattedDate ($formattedTime)\n";
68 | }
69 | NotificationService.showSimpleNotification(
70 | id: 2000,
71 | title: "Upcoming Contests",
72 | body: str,
73 | payload: "payload",
74 | );
75 | }
76 | }
77 | });
78 | }
79 |
80 | void scheduleContestReminders() {
81 | Timer.periodic(const Duration(minutes: 30), (timer) async {
82 | if (SharedPrefService().getBool('contest_reminder')) {
83 | String? contestsJson = SharedPrefService().getString('contests');
84 | if (contestsJson != null) {
85 | List contestsData = jsonDecode(contestsJson);
86 | List contests =
87 | contestsData.map((e) => ContestData.fromJson(e)).toList();
88 |
89 | DateTime now = DateTime.now();
90 |
91 | for (var e in contests) {
92 | final DateTime startTime = DateTime.fromMillisecondsSinceEpoch(
93 | e.startTimeSeconds * 1000 - 45 * 60 * 1000); // before 45 mins
94 | final DateTime endTime =
95 | startTime.add(Duration(seconds: e.durationSeconds));
96 | final String formattedTime =
97 | "${DateFormat('h:mm a ').format(startTime)} - ${DateFormat('h:mm a').format(endTime)}";
98 |
99 | if (now.isAfter(startTime) && now.isBefore(endTime)) {
100 | NotificationService.showFullScreenNotification(
101 | title: "Contest Reminder",
102 | body:
103 | "A contest is going to start soon.\n ${e.name}\n$formattedTime",
104 | payload: "payload",
105 | );
106 | }
107 | }
108 | }
109 | }
110 | });
111 | }
112 |
113 | @pragma('vm:entry-point')
114 | void onStart(ServiceInstance service) async {
115 | service.on("stop").listen((event) {
116 | service.stopSelf();
117 | });
118 |
119 | await SharedPrefService().init();
120 | scheduleDailyNotification();
121 | scheduleContestReminders();
122 | }
123 |
--------------------------------------------------------------------------------
/lib/ui/views/home_view/contacts_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_bloc/flutter_bloc.dart';
3 | import 'package:google_fonts/google_fonts.dart';
4 | import 'package:revo/constants/routes.dart';
5 | import 'package:revo/extensions/theme.dart';
6 | import 'package:revo/model/contact.dart';
7 | import 'package:revo/services/cubit/contact_service.dart';
8 | import 'package:revo/utils/center_text.dart';
9 | import 'package:revo/utils/circle_profile.dart';
10 | import 'package:sticky_az_list/sticky_az_list.dart';
11 |
12 | class ContactsView extends StatefulWidget {
13 | const ContactsView({super.key});
14 |
15 | @override
16 | State createState() => _ContactsViewState();
17 | }
18 |
19 | class _ContactsViewState extends State {
20 | late final ScrollController _controller;
21 |
22 | @override
23 | void initState() {
24 | _controller = ScrollController();
25 | super.initState();
26 | }
27 |
28 | @override
29 | void dispose() {
30 | _controller.dispose();
31 | super.dispose();
32 | }
33 |
34 | Future _refreshContacts(BuildContext context) async {
35 | context.read().refresh();
36 | }
37 |
38 | @override
39 | Widget build(BuildContext context) {
40 | return BlocBuilder>(
41 | builder: (context, state) {
42 | if (state.isEmpty) {
43 | return RefreshIndicator(
44 | onRefresh: () => _refreshContacts(context),
45 | child: ListView(
46 | physics: AlwaysScrollableScrollPhysics(),
47 | children: [
48 | CenterText(text: 'No contacts found'),
49 | ],
50 | ),
51 | );
52 | }
53 |
54 | return RefreshIndicator(
55 | onRefresh: () => _refreshContacts(context),
56 | child: StickyAzList(
57 | controller: _controller,
58 | items: state,
59 | builder: (context, index, item) => Builder(builder: (context) {
60 | return _displayContact(context, item);
61 | }),
62 | options: StickyAzOptions(
63 | scrollBarOptions: ScrollBarOptions(
64 | // TODO: Fix look on AMOLED dark mode
65 | ),
66 | listOptions: ListOptions(
67 | listHeaderBuilder: (context, symbol) => Container(
68 | margin: EdgeInsets.only(top: 30, bottom: 10),
69 | padding: EdgeInsets.fromLTRB(30, 10, 0, 10),
70 | decoration: BoxDecoration(
71 | borderRadius: BorderRadius.circular(15),
72 | color: Theme.of(context).colorScheme.surface),
73 | child: Text(
74 | symbol,
75 | style: GoogleFonts.raleway(
76 | fontSize: 20,
77 | color: context.colorScheme.onSurface.withAlpha(200),
78 | ),
79 | ),
80 | ),
81 | )),
82 | ),
83 | // Scrollbar(
84 | // trackVisibility: true,
85 | // thickness: 2.5,
86 | // interactive: true,
87 | // radius: Radius.circular(30),
88 | // controller: _controller,
89 | // child: ListView.builder(
90 | // itemCount: state.length,
91 | // controller: _controller,
92 | // physics: AlwaysScrollableScrollPhysics(),
93 | // itemBuilder: (context, i) => _displayContact(context, state, i),
94 | // ),
95 | // ),
96 | );
97 | },
98 | );
99 | }
100 |
101 | Widget _displayContact(BuildContext context, Contact contact) => ListTile(
102 | shape: RoundedRectangleBorder(
103 | borderRadius: BorderRadius.circular(20),
104 | ),
105 | title: Row(
106 | children: [
107 | const SizedBox(width: 10),
108 | CircleProfile(
109 | name: contact.displayName,
110 | profile: contact.photo,
111 | size: 30,
112 | ),
113 | const SizedBox(width: 10),
114 | Flexible(
115 | child: Text(
116 | contact.displayName,
117 | style: GoogleFonts.raleway(
118 | fontSize: 16,
119 | color: context.colorScheme.onSurface,
120 | ),
121 | overflow: TextOverflow.ellipsis,
122 | ),
123 | ),
124 | ],
125 | ),
126 | onTap: () async {
127 | await Navigator.of(context).pushNamed(
128 | contactInfoRoute,
129 | arguments: contact,
130 | );
131 | },
132 | );
133 | }
134 |
--------------------------------------------------------------------------------
/lib/ui/views/settings_view/about.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:google_fonts/google_fonts.dart';
3 | import 'package:hugeicons/hugeicons.dart';
4 | import 'package:revo/extensions/theme.dart';
5 | import 'package:revo/ui/views/common/constants.dart';
6 | import 'package:revo/utils/menu_tile.dart';
7 | import 'package:revo/utils/utils.dart';
8 |
9 | class AboutView extends StatelessWidget {
10 | const AboutView({super.key});
11 |
12 | final String githubUrl = "https://github.com/user-grinch/Rivo";
13 | final String patreonUrl = "https://www.patreon.com/grinch_";
14 |
15 | @override
16 | Widget build(BuildContext context) {
17 | return Scaffold(
18 | appBar: AppBar(
19 | leading: IconButton(
20 | icon: Icon(HugeIcons.strokeRoundedArrowLeft01),
21 | onPressed: () => Navigator.of(context).pop(),
22 | ),
23 | title: Text(
24 | 'About',
25 | style: GoogleFonts.raleway(
26 | fontSize: 20,
27 | fontWeight: FontWeight.w600,
28 | color: context.colorScheme.onSurface,
29 | ),
30 | ),
31 | ),
32 | body: Padding(
33 | padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
34 | child: Column(
35 | crossAxisAlignment: CrossAxisAlignment.center,
36 | children: [
37 | Image.asset(
38 | 'assets/icon.png',
39 | width: 100,
40 | height: 100,
41 | fit: BoxFit.cover,
42 | ),
43 | const SizedBox(height: 12),
44 | Text(
45 | 'Rivo',
46 | style: GoogleFonts.raleway(
47 | fontSize: 26,
48 | fontWeight: FontWeight.w700,
49 | color: context.colorScheme.onSurface,
50 | ),
51 | ),
52 | const SizedBox(height: 20),
53 | Container(
54 | decoration: BoxDecoration(
55 | color: Theme.of(context)
56 | .colorScheme
57 | .secondaryContainer
58 | .withAlpha(110),
59 | borderRadius: BorderRadius.all(
60 | Radius.circular(15),
61 | ),
62 | ),
63 | child: Padding(
64 | padding: const EdgeInsets.all(16.0),
65 | child: Column(
66 | crossAxisAlignment: CrossAxisAlignment.start,
67 | children: [
68 | Text(
69 | 'About the App',
70 | style: GoogleFonts.raleway(
71 | fontSize: 18,
72 | fontWeight: FontWeight.w600,
73 | color: context.colorScheme.onSurface,
74 | ),
75 | ),
76 | const SizedBox(height: 8),
77 | Text(
78 | 'Rivo is a modern dialer app that brings simplicity and elegance to calling. '
79 | 'Designed with Material You, it adapts seamlessly to your theme while ensuring a smooth and intuitive experience.',
80 | style: GoogleFonts.raleway(
81 | fontSize: 15,
82 | color: context.colorScheme.onSurfaceVariant,
83 | ),
84 | ),
85 | ],
86 | ),
87 | ),
88 | ),
89 | const SizedBox(height: 10),
90 | MenuTile(
91 | title: 'Author',
92 | subtitle: 'Grinch_',
93 | icon: HugeIcons.strokeRoundedUser,
94 | onTap: () {},
95 | isFirst: true,
96 | ),
97 | MenuTile(
98 | title: 'Version',
99 | subtitle: version,
100 | icon: HugeIcons.strokeRoundedInformationCircle,
101 | onTap: () {},
102 | isLast: true,
103 | ),
104 | const SizedBox(height: 10),
105 | MenuTile(
106 | title: 'Source Code',
107 | subtitle: 'View the source code on GitHub',
108 | icon: HugeIcons.strokeRoundedGithub01,
109 | onTap: () async =>
110 | await launchURL('https://github.com/user-grinch/Rivo'),
111 | isFirst: true,
112 | ),
113 | MenuTile(
114 | title: 'Support Us on Patreon',
115 | subtitle: 'Contribute to our development',
116 | icon: HugeIcons.strokeRoundedFavourite,
117 | onTap: () async =>
118 | await launchURL('https://www.patreon.com/grinch_'),
119 | isLast: true,
120 | ),
121 | ],
122 | ),
123 | ),
124 | );
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/lib/services/cubit/contact_service.dart:
--------------------------------------------------------------------------------
1 | import 'package:bloc/bloc.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:flutter_bloc/flutter_bloc.dart';
4 | import 'package:flutter_contacts/flutter_contacts.dart' as fc;
5 | import 'package:permission_handler/permission_handler.dart';
6 | import 'package:revo/model/contact.dart';
7 | import 'package:revo/services/activity_service.dart';
8 | import 'package:revo/utils/utils.dart';
9 | import 'package:flutter_contacts/flutter_contacts.dart' as lib;
10 |
11 | class ContactService extends Cubit> {
12 | ContactService() : super([]) {
13 | _initialize();
14 | }
15 |
16 | Future _initialize() async {
17 | await ActivityService().requestPermissions();
18 | if (await Permission.contacts.status.isGranted) {
19 | var contact = (await fc.FlutterContacts.getContacts(
20 | withProperties: true,
21 | withAccounts: true,
22 | withGroups: true,
23 | withPhoto: true,
24 | withThumbnail: true,
25 | ))
26 | .toList();
27 |
28 | contact = contact.where((e) => e.phones.isNotEmpty).toList();
29 | emit(contact.map((e) => Contact.fromInternal(e)).toList());
30 | }
31 | }
32 |
33 | List filterByStars() {
34 | return state.where((e) => e.isStarred).toList();
35 | }
36 |
37 | ContactService refresh() {
38 | _initialize();
39 | return this;
40 | }
41 |
42 | Contact findByNumber(String number) {
43 | String target = normalizePhoneNumber(number);
44 | try {
45 | return state.firstWhere((f) {
46 | return f.phones.any((g) {
47 | String contactNumber = normalizePhoneNumber(g);
48 | return contactNumber.endsWith(target) ||
49 | // target.endsWith(contactNumber) ||
50 | target == contactNumber;
51 | });
52 | });
53 | } catch (_) {
54 | return Contact(
55 | id: '"Unknown"',
56 | displayName: "Unknown",
57 | fullName: "Unknown",
58 | phones: [number]);
59 | }
60 | }
61 |
62 | Contact findByName(String name) {
63 | try {
64 | return state.firstWhere((f) {
65 | return name.isNotEmpty &&
66 | f.phones.isNotEmpty &&
67 | f.fullName.toLowerCase().contains(name.toLowerCase());
68 | });
69 | } catch (_) {
70 | return Contact(id: '"Unknown"', displayName: "Unknown", fullName: name);
71 | }
72 | }
73 |
74 | List findAllByNameOrNumber(String name, String number) {
75 | String target = normalizePhoneNumber(number);
76 | try {
77 | return state.where((f) {
78 | bool nameMatches = name.isNotEmpty &&
79 | f.fullName.toLowerCase().contains(name.toLowerCase());
80 |
81 | bool isNumber = number.isNotEmpty && num.tryParse(number) != null;
82 | bool numberMatches = isNumber &&
83 | f.phones.any((g) {
84 | String contactNumber = normalizePhoneNumber(g);
85 | return contactNumber.contains(target) || target == contactNumber;
86 | });
87 | return nameMatches || numberMatches;
88 | }).toList();
89 | } catch (_) {
90 | return [];
91 | }
92 | }
93 |
94 | Future createNewContact({String? number}) async {
95 | if (await Permission.contacts.status.isGranted) {
96 | if (number == null) {
97 | await fc.FlutterContacts.openExternalInsert();
98 | } else {
99 | await fc.FlutterContacts.openExternalInsert(
100 | fc.Contact(phones: [fc.Phone(number)]));
101 | }
102 | }
103 | }
104 |
105 | Future insertContact(Contact contact) async {
106 | if (await Permission.contacts.status.isGranted) {
107 | await fc.FlutterContacts.insertContact(contact.toInternal());
108 | }
109 | }
110 |
111 | Future insertContactFromVCard(String data) async {
112 | if (await Permission.contacts.status.isGranted) {
113 | try {
114 | await fc.FlutterContacts.insertContact(lib.Contact.fromVCard(data));
115 | debugPrint('Contact added successfully!');
116 | } catch (e) {
117 | debugPrint('Error adding contact: $e');
118 | }
119 | } else {
120 | debugPrint('Permission to access contacts denied!');
121 | }
122 | }
123 |
124 | Future editContact(Contact contact) async {
125 | if (await Permission.contacts.status.isGranted) {
126 | await fc.FlutterContacts.openExternalEdit(contact.id);
127 | } else {
128 | debugPrint("Permission denied to access contacts");
129 | }
130 | }
131 |
132 | void updateContact({
133 | required Contact contact,
134 | bool withGroups = false,
135 | }) async {
136 | if (await Permission.contacts.status.isGranted) {
137 | fc.FlutterContacts.updateContact(
138 | contact.toInternal(),
139 | withGroups: withGroups,
140 | );
141 | } else {
142 | debugPrint("Permission denied to access contacts");
143 | }
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/lib/ui/views/settings_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:google_fonts/google_fonts.dart';
3 | import 'package:hugeicons/hugeicons.dart';
4 | import 'package:revo/extensions/theme.dart';
5 | import 'package:revo/ui/views/settings_view/about.dart';
6 | import 'package:revo/ui/views/settings_view/call.dart';
7 | import 'package:revo/ui/views/settings_view/sound.dart';
8 | import 'package:revo/ui/views/settings_view/user_interface.dart';
9 | import 'package:revo/utils/center_text.dart';
10 | import 'package:revo/utils/menu_tile.dart';
11 | import 'package:revo/utils/utils.dart';
12 |
13 | class SettingsView extends StatelessWidget {
14 | const SettingsView({super.key});
15 |
16 | @override
17 | Widget build(BuildContext context) {
18 | return Scaffold(
19 | appBar: AppBar(
20 | leading: IconButton(
21 | icon: Icon(HugeIcons.strokeRoundedArrowLeft01),
22 | onPressed: () => Navigator.of(context).pop(),
23 | ),
24 | title: Text(
25 | 'Settings',
26 | style: GoogleFonts.raleway(
27 | fontSize: 20,
28 | fontWeight: FontWeight.w600,
29 | color: context.colorScheme.onSurface,
30 | ),
31 | ),
32 | ),
33 | body: ListView(
34 | padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
35 | children: [
36 | Container(
37 | decoration: BoxDecoration(
38 | color: Theme.of(context).colorScheme.primaryContainer,
39 | borderRadius: BorderRadius.all(Radius.circular(15)),
40 | ),
41 | child: ListTile(
42 | onTap: () async {
43 | await launchURL('https://www.patreon.com/grinch_');
44 | },
45 | shape: RoundedRectangleBorder(
46 | borderRadius: BorderRadius.all(Radius.circular(15)),
47 | ),
48 | leading: Icon(
49 | HugeIcons.strokeRoundedFavourite,
50 | size: 40,
51 | ),
52 | title: Column(
53 | children: [
54 | CenterText(
55 | text: "Help keep Rivo free for everyone.",
56 | ),
57 | CenterText(text: "Consider Donating!"),
58 | ],
59 | ),
60 | ),
61 | ),
62 | const SizedBox(
63 | height: 30,
64 | ),
65 | CenterText(text: "This section is work in progress"),
66 | MenuTile(
67 | title: 'User Interface',
68 | subtitle: 'Customize looks & behaviors',
69 | icon: HugeIcons.strokeRoundedImage02,
70 | onTap: () {
71 | Navigator.of(context).push(
72 | MaterialPageRoute(builder: (context) => UserInterfaceView()),
73 | );
74 | },
75 | isFirst: true,
76 | ),
77 | MenuTile(
78 | title: 'Sound & Vibration',
79 | subtitle: 'Manage ringtones & volume',
80 | icon: HugeIcons.strokeRoundedVolumeHigh,
81 | onTap: () {
82 | Navigator.of(context).push(
83 | MaterialPageRoute(builder: (context) => SoundView()),
84 | );
85 | },
86 | isLast: true,
87 | ),
88 | const SizedBox(height: 10.0),
89 | MenuTile(
90 | title: 'Blocklist',
91 | subtitle: 'Block calls from people',
92 | icon: HugeIcons.strokeRoundedCallBlocked02,
93 | onTap: null,
94 | // () {}
95 | isFirst: true,
96 | ),
97 | MenuTile(
98 | title: 'Call Settings',
99 | subtitle: 'Incoming call settings',
100 | icon: HugeIcons.strokeRoundedCallIncoming03,
101 | onTap: null,
102 | // () {
103 | // // Navigator.of(context).push(
104 | // // MaterialPageRoute(builder: (context) => CallView()),
105 | // // );
106 | // },
107 | isLast: true,
108 | ),
109 | const SizedBox(height: 10.0),
110 | MenuTile(
111 | title: 'About',
112 | subtitle: 'Information about the dialer app',
113 | icon: HugeIcons.strokeRoundedInformationCircle,
114 | onTap: () {
115 | Navigator.of(context).push(
116 | MaterialPageRoute(builder: (context) => AboutView()),
117 | );
118 | },
119 | isFirst: true,
120 | isLast: true,
121 | ),
122 | const SizedBox(height: 12.0),
123 | Center(
124 | child: Text(
125 | '© Copyright Grinch_ 2025',
126 | style: TextStyle(
127 | fontSize: 14,
128 | color: Colors.grey,
129 | ),
130 | ),
131 | ),
132 | ],
133 | ),
134 | );
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/lib/ui/views/settings_view/user_interface.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_bloc/flutter_bloc.dart';
3 | import 'package:google_fonts/google_fonts.dart';
4 | import 'package:hugeicons/hugeicons.dart';
5 | import 'package:revo/constants/pref.dart';
6 | import 'package:revo/extensions/theme.dart';
7 | import 'package:revo/services/prefservice.dart';
8 | import 'package:revo/ui/theme/handler.dart';
9 | import 'package:revo/utils/switch_tile.dart';
10 |
11 | class UserInterfaceView extends StatefulWidget {
12 | const UserInterfaceView({super.key});
13 |
14 | @override
15 | State createState() => _UserInterfaceViewState();
16 | }
17 |
18 | class _UserInterfaceViewState extends State {
19 | @override
20 | Widget build(BuildContext context) {
21 | return Scaffold(
22 | appBar: AppBar(
23 | leading: IconButton(
24 | icon: Icon(HugeIcons.strokeRoundedArrowLeft01),
25 | onPressed: () => Navigator.of(context).pop(),
26 | ),
27 | title: Text(
28 | 'User Interface',
29 | style: GoogleFonts.raleway(
30 | fontSize: 20,
31 | fontWeight: FontWeight.w600,
32 | color: context.colorScheme.onSurface,
33 | ),
34 | ),
35 | ),
36 | body: ListView(
37 | padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
38 | children: [
39 | SwitchTileWidget(
40 | title: "Material You theming",
41 | subtitle:
42 | "Wallpaper based app color theming. Restart is required.",
43 | value: context.read().isDynamic,
44 | onChanged: (value) {
45 | setState(() {
46 | context.read().toggleDynamicColors();
47 | setState(() {});
48 | });
49 | },
50 | isFirst: true),
51 | SwitchTileWidget(
52 | title: "Amoled dark mode",
53 | subtitle:
54 | "Uses pitch black for UI elements. This may save some battery life on OLED screens.",
55 | value: context.read().isAmoled,
56 | onChanged: (value) {
57 | context.read().toggleAmoledColors();
58 | setState(() {});
59 | },
60 | isLast: true),
61 | const SizedBox(
62 | height: 10,
63 | ),
64 | SwitchTileWidget(
65 | title: "Show first letter in avatar",
66 | subtitle:
67 | "Displays the first letter of the contact name when a profile picture isn't available",
68 | value: SharedPrefService().getBool(PREF_SHOW_FIRST_LETTER),
69 | onChanged: (value) {
70 | SharedPrefService().saveBool(PREF_SHOW_FIRST_LETTER, value);
71 | setState(() {});
72 | },
73 | isFirst: true),
74 | SwitchTileWidget(
75 | title: "Show picture in avatar",
76 | subtitle: "Shows the contact picture if available",
77 | value: SharedPrefService().getBool(PREF_SHOW_PICTURE_IN_AVATAR),
78 | onChanged: (value) {
79 | SharedPrefService()
80 | .saveBool(PREF_SHOW_PICTURE_IN_AVATAR, value);
81 | setState(() {});
82 | },
83 | isLast: true),
84 | const SizedBox(
85 | height: 10,
86 | ),
87 | SwitchTileWidget(
88 | title: "Icon-only bottom sheet",
89 | subtitle:
90 | "Only shows navigation icons in the bottom navigation bar",
91 | value: SharedPrefService().getBool(PREF_ICON_ONLY_BOTTOMSHEET),
92 | onChanged: (value) {
93 | SharedPrefService().saveBool(PREF_ICON_ONLY_BOTTOMSHEET, value);
94 | setState(() {});
95 | },
96 | isFirst: true,
97 | ),
98 | SwitchTileWidget(
99 | title: "Selected icon in bottom sheet",
100 | subtitle: "Always shows the icon for the selected tab",
101 | value: SharedPrefService()
102 | .getBool(PREF_ALWAYS_SHOW_SELECTED_IN_BOTTOMSHEET),
103 | onChanged: (value) {
104 | SharedPrefService()
105 | .saveBool(PREF_ALWAYS_SHOW_SELECTED_IN_BOTTOMSHEET, value);
106 | setState(() {});
107 | },
108 | isLast: true,
109 | ),
110 | const SizedBox(
111 | height: 10,
112 | ),
113 | SwitchTileWidget(
114 | title: "Dialpad letters",
115 | subtitle: "Show letters on the dialpad buttons",
116 | value: SharedPrefService().getBool(PREF_DIALPAD_LETTERS),
117 | onChanged: (value) {
118 | SharedPrefService().saveBool(PREF_DIALPAD_LETTERS, value);
119 | setState(() {});
120 | },
121 | isFirst: true,
122 | isLast: true,
123 | ),
124 | ],
125 | ),
126 | );
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/lib/ui/views/home_view/recents_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:google_fonts/google_fonts.dart';
3 | import 'package:hugeicons/hugeicons.dart';
4 | import 'package:revo/constants/routes.dart';
5 | import 'package:revo/extensions/datetime.dart';
6 | import 'package:revo/extensions/theme.dart';
7 | import 'package:revo/model/call_log.dart';
8 | import 'package:revo/model/call_type.dart';
9 | import 'package:revo/model/contact.dart';
10 | import 'package:revo/services/cubit/call_log_service.dart';
11 | import 'package:revo/services/cubit/contact_service.dart';
12 | import 'package:revo/ui/popups/sim_choose_popup.dart';
13 | import 'package:revo/utils/circle_profile.dart';
14 | import 'package:flutter_bloc/flutter_bloc.dart';
15 | import 'package:revo/utils/rounded_icon_btn.dart';
16 | import 'package:revo/utils/utils.dart';
17 |
18 | class RecentsView extends StatefulWidget {
19 | const RecentsView({super.key});
20 |
21 | @override
22 | State createState() => _RecentsViewState();
23 | }
24 |
25 | class _RecentsViewState extends State {
26 | late final ScrollController _controller;
27 |
28 | @override
29 | void initState() {
30 | _controller = ScrollController();
31 | super.initState();
32 | }
33 |
34 | @override
35 | void dispose() {
36 | _controller.dispose();
37 | super.dispose();
38 | }
39 |
40 | Future _refreshLogs(BuildContext context) async {
41 | // Call a method in your CallLogService to refresh the call logs.
42 | context.read().refresh(); // Example method
43 | }
44 |
45 | @override
46 | Widget build(BuildContext context) {
47 | return Scrollbar(
48 | trackVisibility: true,
49 | thickness: 2.5,
50 | interactive: true,
51 | radius: Radius.circular(30),
52 | controller: _controller,
53 | child: RefreshIndicator(
54 | onRefresh: () => _refreshLogs(context),
55 | child: BlocBuilder>(
56 | builder: (BuildContext context, List state) {
57 | if (state.isEmpty) {
58 | return ListView(
59 | physics: AlwaysScrollableScrollPhysics(),
60 | children: const [
61 | Center(
62 | child: Padding(
63 | padding: EdgeInsets.all(20.0),
64 | child: Text('No call logs found.'),
65 | ),
66 | ),
67 | ],
68 | );
69 | }
70 |
71 | return ListView.builder(
72 | itemCount: state.length,
73 | controller: _controller,
74 | physics: AlwaysScrollableScrollPhysics(),
75 | itemBuilder: (context, i) {
76 | return _buildLog(
77 | context,
78 | state[i],
79 | _shouldShowHeader(state, i),
80 | );
81 | },
82 | );
83 | },
84 | ),
85 | ),
86 | );
87 | }
88 |
89 | bool _shouldShowHeader(List logs, int i) {
90 | return i == 0 || logs[i].date.weekday != logs[i - 1].date.weekday;
91 | }
92 |
93 | Widget _buildLog(BuildContext context, CallLog log, bool showDateHeader) {
94 | return Column(
95 | mainAxisAlignment: MainAxisAlignment.start,
96 | crossAxisAlignment: CrossAxisAlignment.start,
97 | children: [
98 | if (showDateHeader)
99 | Padding(
100 | padding: const EdgeInsets.fromLTRB(20, 50, 0, 0),
101 | child: Text(
102 | log.date.getContextAwareDate(),
103 | style: GoogleFonts.raleway(
104 | fontSize: 20,
105 | color: context.colorScheme.onSurface,
106 | ),
107 | ),
108 | ),
109 | ListTile(
110 | onTap: () async {
111 | simChooserDialog(context, log.number);
112 | },
113 | shape: RoundedRectangleBorder(
114 | borderRadius: BorderRadius.circular(20),
115 | ),
116 | leading: CircleProfile(
117 | name: log.name,
118 | profile: log.profile,
119 | size: 30,
120 | ),
121 | title: Text(
122 | log.displayName,
123 | style: GoogleFonts.raleway(
124 | fontSize: 16,
125 | color: context.colorScheme.onSurface,
126 | ),
127 | ),
128 | trailing: RoundedIconButton(
129 | context,
130 | icon: HugeIcons.strokeRoundedArrowRight01,
131 | size: 30,
132 | onTap: () async {
133 | Contact contact =
134 | context.read().findByName(log.name);
135 | if (contact.phones.isEmpty) {
136 | contact =
137 | context.read().findByNumber(log.number);
138 | }
139 | await Navigator.of(context)
140 | .pushNamed(contactInfoRoute, arguments: contact);
141 | },
142 | ),
143 | subtitle: Column(
144 | mainAxisAlignment: MainAxisAlignment.start,
145 | crossAxisAlignment: CrossAxisAlignment.start,
146 | children: [
147 | Row(
148 | children: [
149 | Icon(
150 | log.type.getIcon(),
151 | color: log.type.getColor(),
152 | size: 16,
153 | ),
154 | SizedBox(width: 5),
155 | Text(
156 | log.date.getContextAwareDateTime(),
157 | style: GoogleFonts.raleway(
158 | fontSize: 12,
159 | color: log.type.getColor(),
160 | ),
161 | ),
162 | ],
163 | ),
164 | Text(
165 | convertSecondsToHMS(int.parse(log.duration)),
166 | style: GoogleFonts.raleway(
167 | fontSize: 12,
168 | color: context.colorScheme.onSurface.withAlpha(200),
169 | ),
170 | ),
171 | ],
172 | ),
173 | ),
174 | ],
175 | );
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/lib/ui/views/dialpad_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter/services.dart';
3 | import 'package:flutter_bloc/flutter_bloc.dart';
4 | import 'package:google_fonts/google_fonts.dart';
5 | import 'package:hugeicons/hugeicons.dart';
6 | import 'package:revo/constants/pref.dart';
7 | import 'package:revo/extensions/theme.dart';
8 | import 'package:revo/services/cubit/contact_service.dart';
9 | import 'package:revo/services/prefservice.dart';
10 | import 'package:revo/ui/popups/sim_choose_popup.dart';
11 | import 'package:revo/ui/views/common/matched_view.dart';
12 | import 'package:revo/ui/views/dialpad_view/action_btn.dart';
13 | import 'package:revo/ui/views/dialpad_view/dial_btn.dart';
14 | import 'package:revo/utils/rounded_icon_btn.dart';
15 | import 'package:revo/utils/utils.dart';
16 |
17 | class DialPadView extends StatefulWidget {
18 | const DialPadView({super.key});
19 |
20 | @override
21 | State createState() => _DialPadViewState();
22 | }
23 |
24 | class _DialPadViewState extends State {
25 | String _number = '';
26 |
27 | late final ScrollController _scrollController;
28 | late final FocusNode _focusNode;
29 |
30 | @override
31 | void initState() {
32 | _scrollController = ScrollController();
33 | _focusNode = FocusNode();
34 | WidgetsBinding.instance.addPostFrameCallback((_) {
35 | _focusNode.requestFocus();
36 | });
37 | SharedPrefService().onPreferenceChanged.listen((key) {
38 | if (key == PREF_DIALPAD_LETTERS) {
39 | setState(() {});
40 | }
41 | });
42 | super.initState();
43 | }
44 |
45 | @override
46 | void dispose() {
47 | _scrollController.dispose();
48 | _focusNode.dispose();
49 | super.dispose();
50 | }
51 |
52 | final List keys = [
53 | '1',
54 | '2',
55 | '3',
56 | '4',
57 | '5',
58 | '6',
59 | '7',
60 | '8',
61 | '9',
62 | '*',
63 | '0',
64 | '#',
65 | ];
66 |
67 | final Map subKeys = {
68 | '2': 'ABC',
69 | '3': 'DEF',
70 | '4': 'GHI',
71 | '5': 'JKL',
72 | '6': 'MNO',
73 | '7': 'PQRS',
74 | '8': 'TUV',
75 | '9': 'WXYZ',
76 | '0': '+',
77 | };
78 |
79 | @override
80 | Widget build(BuildContext context) {
81 | return Scaffold(
82 | body: SafeArea(
83 | child: Column(
84 | mainAxisAlignment: MainAxisAlignment.end,
85 | children: [
86 | Expanded(
87 | child: Padding(
88 | padding: const EdgeInsets.all(8.0),
89 | child: MatchedView(
90 | scrollController: _scrollController,
91 | number: _number,
92 | ),
93 | ),
94 | ),
95 | Container(
96 | color: context.colorScheme.secondaryContainer.withAlpha(50),
97 | child: Column(
98 | mainAxisSize: MainAxisSize.min,
99 | children: [
100 | _number.isNotEmpty
101 | ? Padding(
102 | padding: const EdgeInsets.symmetric(
103 | horizontal: 20,
104 | vertical: 15,
105 | ),
106 | child: Text(
107 | _number,
108 | style: GoogleFonts.raleway(
109 | fontSize: 30,
110 | color: context.colorScheme.onSurface,
111 | ),
112 | ),
113 | )
114 | : SizedBox(height: 30),
115 | GridView.builder(
116 | shrinkWrap: true,
117 | physics: NeverScrollableScrollPhysics(),
118 | itemCount: keys.length,
119 | gridDelegate:
120 | const SliverGridDelegateWithFixedCrossAxisCount(
121 | crossAxisCount: 3,
122 | mainAxisSpacing: 8,
123 | crossAxisSpacing: 8,
124 | childAspectRatio: 1.75,
125 | ),
126 | padding: const EdgeInsets.symmetric(horizontal: 20),
127 | itemBuilder: (context, index) {
128 | String key = keys[index];
129 | return DialPadButton(
130 | mainText: key,
131 | subText: SharedPrefService()
132 | .getBool(PREF_DIALPAD_LETTERS, def: true)
133 | ? subKeys[key]
134 | : null,
135 | onUpdate: (String str) {
136 | setState(() {
137 | _number += str;
138 | });
139 | },
140 | );
141 | },
142 | ),
143 | SizedBox(height: 20),
144 | Padding(
145 | padding: const EdgeInsets.symmetric(horizontal: 25),
146 | child: Row(
147 | mainAxisAlignment: MainAxisAlignment.spaceBetween,
148 | children: [
149 | if (_number.isNotEmpty)
150 | RoundedIconButton(
151 | context,
152 | icon: HugeIcons.strokeRoundedUserAdd01,
153 | size: 40,
154 | onTap: () {
155 | hapticVibration();
156 | context
157 | .read()
158 | .createNewContact(number: _number);
159 | },
160 | ),
161 | Spacer(),
162 | DialActionButton(
163 | icon: HugeIcons.strokeRoundedSimcard01,
164 | label: 'Call',
165 | func: () {
166 | hapticVibration();
167 | simChooserDialog(context, _number);
168 | },
169 | ),
170 | Spacer(),
171 | if (_number.isNotEmpty)
172 | RoundedIconButton(
173 | context,
174 | icon: HugeIcons.strokeRoundedArrowLeft01,
175 | size: 40,
176 | onTap: () {
177 | hapticVibration();
178 | setState(() {
179 | if (_number.isNotEmpty) {
180 | _number = _number.substring(
181 | 0,
182 | _number.length - 1,
183 | );
184 | }
185 | });
186 | },
187 | onLongPress: () {
188 | HapticFeedback.vibrate();
189 | setState(() {
190 | if (_number.isNotEmpty) {
191 | _number = '';
192 | }
193 | });
194 | },
195 | ),
196 | ],
197 | ),
198 | ),
199 | SizedBox(height: 30),
200 | ],
201 | ),
202 | ),
203 | ],
204 | ),
205 | ),
206 | );
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/lib/ui/views/call_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:google_fonts/google_fonts.dart';
3 | import 'package:revo/extensions/theme.dart';
4 |
5 | class CallScreenView extends StatelessWidget {
6 | const CallScreenView({super.key});
7 |
8 | @override
9 | Widget build(BuildContext context) {
10 | return Scaffold(
11 | backgroundColor: context.colorScheme.surface,
12 | body: SafeArea(
13 | child: Stack(
14 | children: [
15 | Align(
16 | alignment: Alignment.topCenter,
17 | child: Padding(
18 | padding:
19 | const EdgeInsets.symmetric(vertical: 40, horizontal: 16),
20 | child: Column(
21 | children: [
22 | CircleAvatar(
23 | radius: 90,
24 | backgroundColor: context.colorScheme.secondaryContainer,
25 | child: Icon(
26 | Icons.person,
27 | size: 100,
28 | color: context.colorScheme.onSecondaryContainer,
29 | ),
30 | ),
31 | const SizedBox(height: 20),
32 | Text(
33 | "John Doe",
34 | style: GoogleFonts.raleway(
35 | fontSize: 30,
36 | fontWeight: FontWeight.bold,
37 | color: context.colorScheme.onSurface,
38 | ),
39 | ),
40 | Text(
41 | "+1 (123) 456-7890",
42 | style: GoogleFonts.raleway(
43 | fontSize: 15,
44 | color: context.colorScheme.onSurface.withAlpha(150),
45 | ),
46 | ),
47 | const SizedBox(height: 16),
48 | Text(
49 | "01:25",
50 | style: GoogleFonts.raleway(
51 | fontSize: 20,
52 | fontWeight: FontWeight.bold,
53 | color: context.colorScheme.primary.withAlpha(150),
54 | ),
55 | ),
56 | ],
57 | ),
58 | ),
59 | ),
60 | Align(
61 | alignment: Alignment.bottomCenter,
62 | child: Container(
63 | padding:
64 | const EdgeInsets.symmetric(horizontal: 20, vertical: 30),
65 | decoration: BoxDecoration(
66 | color: context.colorScheme.surface,
67 | borderRadius:
68 | const BorderRadius.vertical(top: Radius.circular(30)),
69 | ),
70 | child: Column(
71 | mainAxisSize: MainAxisSize.min,
72 | children: [
73 | Row(
74 | mainAxisAlignment: MainAxisAlignment.spaceAround,
75 | children: [
76 | _CallActionButton(
77 | icon: Icons.record_voice_over,
78 | label: "Record",
79 | color: context.colorScheme.secondaryContainer,
80 | textColor: context.colorScheme.onSecondaryContainer,
81 | size: 65,
82 | ),
83 | _CallActionButton(
84 | icon: Icons.mic_off,
85 | label: "Mute",
86 | color: context.colorScheme.secondaryContainer,
87 | textColor: context.colorScheme.onSecondaryContainer,
88 | size: 65,
89 | ),
90 | _CallActionButton(
91 | icon: Icons.pause,
92 | label: "Hold",
93 | color: context.colorScheme.secondaryContainer,
94 | textColor: context.colorScheme.onSecondaryContainer,
95 | size: 65,
96 | ),
97 | ],
98 | ),
99 | const SizedBox(height: 20),
100 | Row(
101 | mainAxisAlignment: MainAxisAlignment.spaceAround,
102 | children: [
103 | _CallActionButton(
104 | icon: Icons.add_call,
105 | label: "Add Call",
106 | color: context.colorScheme.secondaryContainer,
107 | textColor: context.colorScheme.onSecondaryContainer,
108 | size: 65,
109 | ),
110 | _CallActionButton(
111 | icon: Icons.volume_up,
112 | label: "Speaker",
113 | color: context.colorScheme.secondaryContainer,
114 | textColor: context.colorScheme.onSecondaryContainer,
115 | size: 65,
116 | ),
117 | _CallActionButton(
118 | icon: Icons.dialpad,
119 | label: "Dialpad",
120 | color: context.colorScheme.secondaryContainer,
121 | textColor: context.colorScheme.onSecondaryContainer,
122 | size: 65,
123 | ),
124 | ],
125 | ),
126 | const SizedBox(height: 50),
127 | Row(
128 | mainAxisAlignment: MainAxisAlignment.center,
129 | children: [
130 | _CallActionButton(
131 | icon: Icons.call_end,
132 | label: "End",
133 | color: Colors.redAccent,
134 | textColor: context.colorScheme.onError,
135 | size: 70,
136 | onPressed: () {
137 | // Add your end call logic here
138 | },
139 | showLabel: false,
140 | ),
141 | ],
142 | ),
143 | ],
144 | ),
145 | ),
146 | ),
147 | ],
148 | ),
149 | ),
150 | );
151 | }
152 | }
153 |
154 | class _CallActionButton extends StatelessWidget {
155 | final IconData icon;
156 | final String label;
157 | final Color color;
158 | final Color textColor;
159 | final double size;
160 | final VoidCallback? onPressed;
161 | final bool showLabel;
162 |
163 | const _CallActionButton({
164 | required this.icon,
165 | required this.label,
166 | required this.color,
167 | required this.textColor,
168 | this.size = 60,
169 | this.onPressed,
170 | this.showLabel = true,
171 | });
172 |
173 | @override
174 | Widget build(BuildContext context) {
175 | return Column(
176 | children: [
177 | GestureDetector(
178 | onTap: onPressed,
179 | child: Container(
180 | width: size,
181 | height: size,
182 | decoration: BoxDecoration(
183 | color: color,
184 | shape: BoxShape.circle,
185 | ),
186 | child: Icon(
187 | icon,
188 | size: size * 0.35,
189 | color: Colors.white,
190 | ),
191 | ),
192 | ),
193 | const SizedBox(height: 10),
194 | if (showLabel)
195 | Text(
196 | label,
197 | style: TextStyle(
198 | fontSize: 12,
199 | fontWeight: FontWeight.w500,
200 | color: textColor,
201 | ),
202 | ),
203 | ],
204 | );
205 | }
206 | }
207 |
--------------------------------------------------------------------------------
/lib/ui/views/contactinfo_view.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 | import 'package:flutter/material.dart';
3 | import 'package:flutter_bloc/flutter_bloc.dart';
4 | import 'package:google_fonts/google_fonts.dart';
5 | import 'package:hugeicons/hugeicons.dart';
6 | import 'package:revo/constants/routes.dart';
7 | import 'package:revo/extensions/theme.dart';
8 | import 'package:revo/model/contact.dart';
9 | import 'package:revo/services/activity_service.dart';
10 | import 'package:revo/services/cubit/contact_service.dart';
11 | import 'package:revo/ui/popups/number_choose_popup.dart';
12 | import 'package:revo/ui/popups/qr_popup.dart';
13 | import 'package:revo/ui/popups/sim_choose_popup.dart';
14 | import 'package:revo/utils/rounded_icon_btn.dart';
15 | import 'package:revo/utils/share.dart';
16 | import 'package:share_plus/share_plus.dart';
17 |
18 | class ContactInfoView extends StatefulWidget {
19 | final Contact contact;
20 | const ContactInfoView(this.contact, {super.key});
21 |
22 | @override
23 | State createState() => _ContactInfoViewState();
24 | }
25 |
26 | class _ContactInfoViewState extends State {
27 | @override
28 | Widget build(BuildContext context) {
29 | return Scaffold(
30 | appBar: AppBar(
31 | leading: IconButton(
32 | icon: Icon(HugeIcons.strokeRoundedArrowLeft01),
33 | onPressed: () => Navigator.of(context).pop(),
34 | ),
35 | elevation: 0,
36 | ),
37 | floatingActionButton: FloatingActionButton.extended(
38 | elevation: 1,
39 | onPressed: () async {
40 | await context.read().editContact(widget.contact);
41 | },
42 | backgroundColor: context.colorScheme.secondaryContainer,
43 | label: Text(
44 | "Edit",
45 | style: TextStyle(color: context.colorScheme.onSecondaryContainer),
46 | ),
47 | icon: Icon(HugeIcons.strokeRoundedEdit02,
48 | color: context.colorScheme.onSecondaryContainer),
49 | ),
50 | body: SingleChildScrollView(
51 | padding: EdgeInsets.only(
52 | top: MediaQuery.of(context).padding.top, left: 16, right: 16),
53 | child: Column(
54 | crossAxisAlignment: CrossAxisAlignment.center,
55 | children: [
56 | _buildProfilePicture(context),
57 | const SizedBox(height: 16),
58 | Text(
59 | widget.contact.fullName,
60 | style: GoogleFonts.raleway(
61 | fontSize: 28,
62 | color: context.colorScheme.onSurface,
63 | ),
64 | textAlign: TextAlign.center,
65 | ),
66 |
67 | Padding(
68 | padding: const EdgeInsets.all(16),
69 | child: Wrap(
70 | alignment: WrapAlignment.center,
71 | spacing: 20,
72 | children: [
73 | RoundedIconButton(
74 | context,
75 | size: 45,
76 | icon: HugeIcons.strokeRoundedQrCode,
77 | text: 'QR Code',
78 | onTap: () {
79 | showDialog(
80 | context: context,
81 | builder: (context) => qrCodePopup(
82 | context,
83 | generateVCardString(widget.contact),
84 | ),
85 | );
86 | },
87 | ),
88 | RoundedIconButton(
89 | context,
90 | icon: HugeIcons.strokeRoundedShare08,
91 | size: 45,
92 | text: 'Share',
93 | onTap: () {
94 | SharePlus.instance.share(ShareParams(files: [
95 | XFile.fromData(
96 | utf8.encode(generateVCardString(widget.contact)),
97 | mimeType: 'text/plain')
98 | ], fileNameOverrides: [
99 | 'contact.vcf'
100 | ]));
101 | },
102 | ),
103 | RoundedIconButton(
104 | context,
105 | icon: HugeIcons.strokeRoundedClock04,
106 | size: 45,
107 | text: 'Call History',
108 | onTap: () {
109 | Navigator.of(context).pushNamed(
110 | callHistoryRoute,
111 | arguments: widget.contact.phones,
112 | );
113 | },
114 | ),
115 | RoundedIconButton(
116 | context,
117 | icon: widget.contact.isStarred
118 | ? HugeIcons.strokeRoundedHeartCheck
119 | : HugeIcons.strokeRoundedHeartAdd,
120 | size: 45,
121 | text: 'Favorite',
122 | onTap: () {
123 | setState(() {
124 | widget.contact.isStarred = !widget.contact.isStarred;
125 | });
126 | context
127 | .read()
128 | .updateContact(contact: widget.contact);
129 | },
130 | ),
131 | ],
132 | ),
133 | ),
134 |
135 | _buildContactInfoSection(context),
136 | const SizedBox(height: 16),
137 |
138 | // External Apps Section
139 | Card(
140 | elevation: 0,
141 | margin: const EdgeInsets.symmetric(vertical: 16),
142 | shape: RoundedRectangleBorder(
143 | borderRadius: BorderRadius.circular(16),
144 | ),
145 | color: context.colorScheme.secondaryContainer.withAlpha(100),
146 | child: Padding(
147 | padding: const EdgeInsets.all(16),
148 | child: Column(
149 | crossAxisAlignment: CrossAxisAlignment.start,
150 | children: [
151 | Text(
152 | "External Apps",
153 | style: GoogleFonts.raleway(
154 | fontSize: 20,
155 | color: context.colorScheme.onSurface,
156 | ),
157 | ),
158 | const SizedBox(height: 16),
159 | Column(
160 | children: [
161 | _buildListTile(context, HugeIcons.strokeRoundedTelegram,
162 | 'Telegram', () {
163 | showDialog(
164 | context: context,
165 | builder: (context) => numberChooserDialog(
166 | context, widget.contact.phones,
167 | (String num) async {
168 | ActivityService().openTelegram(num);
169 | }),
170 | );
171 | }),
172 | _buildListTile(context, HugeIcons.strokeRoundedVideo01,
173 | 'Video Call', () {
174 | showDialog(
175 | context: context,
176 | builder: (context) => numberChooserDialog(
177 | context, widget.contact.phones,
178 | (String num) async {
179 | ActivityService().makeVideoCall(num);
180 | }),
181 | );
182 | }),
183 | _buildListTile(context, HugeIcons.strokeRoundedWhatsapp,
184 | 'WhatsApp', () {
185 | showDialog(
186 | context: context,
187 | builder: (context) => numberChooserDialog(
188 | context, widget.contact.phones,
189 | (String num) async {
190 | ActivityService().openWhatsApp(num);
191 | }),
192 | );
193 | }),
194 | ],
195 | ),
196 | ],
197 | ),
198 | ),
199 | ),
200 | ],
201 | ),
202 | ),
203 | );
204 | }
205 |
206 | Widget _buildListTile(
207 | BuildContext context, IconData icon, String label, VoidCallback onTap) {
208 | return ListTile(
209 | leading: Container(
210 | width: 35,
211 | height: 35,
212 | decoration: BoxDecoration(
213 | color: context.colorScheme.secondaryContainer,
214 | shape: BoxShape.circle,
215 | ),
216 | child: Icon(icon, color: context.colorScheme.onSecondaryContainer),
217 | ),
218 | title: Text(
219 | label,
220 | style: context.textTheme.bodyLarge?.copyWith(
221 | color: context.colorScheme.onSurface,
222 | ),
223 | ),
224 | onTap: onTap,
225 | );
226 | }
227 |
228 | Widget _buildContactInfoSection(BuildContext context) {
229 | return Card(
230 | elevation: 0,
231 | margin: const EdgeInsets.symmetric(vertical: 16),
232 | shape: RoundedRectangleBorder(
233 | borderRadius: BorderRadius.circular(16),
234 | ),
235 | color: context.colorScheme.secondaryContainer.withAlpha(100),
236 | child: Padding(
237 | padding: const EdgeInsets.all(16),
238 | child: Column(
239 | crossAxisAlignment: CrossAxisAlignment.start,
240 | children: [
241 | Text(
242 | "Phone Numbers",
243 | style: GoogleFonts.raleway(
244 | fontSize: 20,
245 | color: context.colorScheme.onSurface,
246 | ),
247 | ),
248 | const SizedBox(height: 16),
249 | if (widget.contact.phones.isNotEmpty)
250 | ...widget.contact.phones
251 | .map((phone) => _buildPhoneWithActionIcons(context, phone)),
252 | ],
253 | ),
254 | ),
255 | );
256 | }
257 |
258 | Widget _buildPhoneWithActionIcons(BuildContext context, var phone) {
259 | return Padding(
260 | padding: const EdgeInsets.only(bottom: 16),
261 | child: Row(
262 | children: [
263 | Expanded(
264 | child: Text(
265 | phone,
266 | style: GoogleFonts.raleway(
267 | textStyle: context.textTheme.bodyLarge,
268 | color: context.colorScheme.onSurface,
269 | ),
270 | ),
271 | ),
272 | Wrap(
273 | spacing: 12,
274 | children: [
275 | RoundedIconButton(
276 | context,
277 | icon: HugeIcons.strokeRoundedCall02,
278 | onTap: () {
279 | simChooserDialog(context, phone);
280 | },
281 | size: 36,
282 | ),
283 | RoundedIconButton(
284 | context,
285 | icon: HugeIcons.strokeRoundedMessage01,
286 | onTap: () {
287 | ActivityService().sendSMS(phone);
288 | },
289 | size: 36,
290 | ),
291 | ],
292 | ),
293 | ],
294 | ),
295 | );
296 | }
297 |
298 | Widget _buildProfilePicture(BuildContext context) {
299 | return CircleAvatar(
300 | backgroundColor: context.colorScheme.secondaryContainer,
301 | radius: 70,
302 | backgroundImage: widget.contact.photo != null
303 | ? MemoryImage(widget.contact.photo!)
304 | : null,
305 | child: widget.contact.photo == null
306 | ? Icon(
307 | HugeIcons.strokeRoundedUser,
308 | size: 100,
309 | color: context.colorScheme.onSecondaryContainer,
310 | )
311 | : null,
312 | );
313 | }
314 | }
315 |
--------------------------------------------------------------------------------
/pubspec.lock:
--------------------------------------------------------------------------------
1 | # Generated by pub
2 | # See https://dart.dev/tools/pub/glossary#lockfile
3 | packages:
4 | android_intent_plus:
5 | dependency: "direct main"
6 | description:
7 | name: android_intent_plus
8 | sha256: "2329378af63f49b985cb2e110ac784d08374f1e2b1984be77ba9325b1c8cce11"
9 | url: "https://pub.dev"
10 | source: hosted
11 | version: "5.3.1"
12 | ansicolor:
13 | dependency: transitive
14 | description:
15 | name: ansicolor
16 | sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f"
17 | url: "https://pub.dev"
18 | source: hosted
19 | version: "2.0.3"
20 | archive:
21 | dependency: transitive
22 | description:
23 | name: archive
24 | sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
25 | url: "https://pub.dev"
26 | source: hosted
27 | version: "4.0.7"
28 | args:
29 | dependency: transitive
30 | description:
31 | name: args
32 | sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
33 | url: "https://pub.dev"
34 | source: hosted
35 | version: "2.7.0"
36 | async:
37 | dependency: transitive
38 | description:
39 | name: async
40 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
41 | url: "https://pub.dev"
42 | source: hosted
43 | version: "2.11.0"
44 | bloc:
45 | dependency: "direct main"
46 | description:
47 | name: bloc
48 | sha256: "52c10575f4445c61dd9e0cafcc6356fdd827c4c64dd7945ef3c4105f6b6ac189"
49 | url: "https://pub.dev"
50 | source: hosted
51 | version: "9.0.0"
52 | boolean_selector:
53 | dependency: transitive
54 | description:
55 | name: boolean_selector
56 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
57 | url: "https://pub.dev"
58 | source: hosted
59 | version: "2.1.1"
60 | call_e_log:
61 | dependency: "direct main"
62 | description:
63 | name: call_e_log
64 | sha256: "4e8ef87330e0b1208fe4e3ca586f45053180c8451d5c5359dc0e6a34951886c4"
65 | url: "https://pub.dev"
66 | source: hosted
67 | version: "0.0.4"
68 | characters:
69 | dependency: transitive
70 | description:
71 | name: characters
72 | sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
73 | url: "https://pub.dev"
74 | source: hosted
75 | version: "1.3.0"
76 | clock:
77 | dependency: transitive
78 | description:
79 | name: clock
80 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
81 | url: "https://pub.dev"
82 | source: hosted
83 | version: "1.1.1"
84 | collection:
85 | dependency: transitive
86 | description:
87 | name: collection
88 | sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
89 | url: "https://pub.dev"
90 | source: hosted
91 | version: "1.19.0"
92 | cross_file:
93 | dependency: transitive
94 | description:
95 | name: cross_file
96 | sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
97 | url: "https://pub.dev"
98 | source: hosted
99 | version: "0.3.4+2"
100 | crypto:
101 | dependency: transitive
102 | description:
103 | name: crypto
104 | sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
105 | url: "https://pub.dev"
106 | source: hosted
107 | version: "3.0.6"
108 | csslib:
109 | dependency: transitive
110 | description:
111 | name: csslib
112 | sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
113 | url: "https://pub.dev"
114 | source: hosted
115 | version: "1.0.2"
116 | dynamic_color:
117 | dependency: "direct main"
118 | description:
119 | name: dynamic_color
120 | sha256: eae98052fa6e2826bdac3dd2e921c6ce2903be15c6b7f8b6d8a5d49b5086298d
121 | url: "https://pub.dev"
122 | source: hosted
123 | version: "1.7.0"
124 | fake_async:
125 | dependency: transitive
126 | description:
127 | name: fake_async
128 | sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
129 | url: "https://pub.dev"
130 | source: hosted
131 | version: "1.3.1"
132 | ffi:
133 | dependency: transitive
134 | description:
135 | name: ffi
136 | sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
137 | url: "https://pub.dev"
138 | source: hosted
139 | version: "2.1.3"
140 | file:
141 | dependency: transitive
142 | description:
143 | name: file
144 | sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
145 | url: "https://pub.dev"
146 | source: hosted
147 | version: "7.0.1"
148 | fixnum:
149 | dependency: transitive
150 | description:
151 | name: fixnum
152 | sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
153 | url: "https://pub.dev"
154 | source: hosted
155 | version: "1.1.1"
156 | flutter:
157 | dependency: "direct main"
158 | description: flutter
159 | source: sdk
160 | version: "0.0.0"
161 | flutter_bloc:
162 | dependency: "direct main"
163 | description:
164 | name: flutter_bloc
165 | sha256: cf51747952201a455a1c840f8171d273be009b932c75093020f9af64f2123e38
166 | url: "https://pub.dev"
167 | source: hosted
168 | version: "9.1.1"
169 | flutter_callkit_incoming:
170 | dependency: "direct main"
171 | description:
172 | name: flutter_callkit_incoming
173 | sha256: "993fb0f0cd990961072f0d13ff815a91773f92bfa1895be17d3366b2225ec9cd"
174 | url: "https://pub.dev"
175 | source: hosted
176 | version: "2.5.8"
177 | flutter_contacts:
178 | dependency: "direct main"
179 | description:
180 | name: flutter_contacts
181 | sha256: "388d32cd33f16640ee169570128c933b45f3259bddbfae7a100bb49e5ffea9ae"
182 | url: "https://pub.dev"
183 | source: hosted
184 | version: "1.1.9+2"
185 | flutter_dtmf:
186 | dependency: "direct main"
187 | description:
188 | path: "."
189 | ref: master
190 | resolved-ref: "26779739c7c263d09a8cd452cdec67c6cbd2879b"
191 | url: "https://github.com/eopeter/flutter_dtmf.git"
192 | source: git
193 | version: "3.1.0"
194 | flutter_lints:
195 | dependency: "direct dev"
196 | description:
197 | name: flutter_lints
198 | sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
199 | url: "https://pub.dev"
200 | source: hosted
201 | version: "5.0.0"
202 | flutter_native_splash:
203 | dependency: "direct main"
204 | description:
205 | name: flutter_native_splash
206 | sha256: "7062602e0dbd29141fb8eb19220b5871ca650be5197ab9c1f193a28b17537bc7"
207 | url: "https://pub.dev"
208 | source: hosted
209 | version: "2.4.4"
210 | flutter_plugin_android_lifecycle:
211 | dependency: transitive
212 | description:
213 | name: flutter_plugin_android_lifecycle
214 | sha256: "6382ce712ff69b0f719640ce957559dde459e55ecd433c767e06d139ddf16cab"
215 | url: "https://pub.dev"
216 | source: hosted
217 | version: "2.0.29"
218 | flutter_sim_data:
219 | dependency: "direct main"
220 | description:
221 | name: flutter_sim_data
222 | sha256: "41706faabc297b7254542ea3be6b5c597c795b852ad2cb7be89f244a8d2744dd"
223 | url: "https://pub.dev"
224 | source: hosted
225 | version: "1.0.5"
226 | flutter_sticky_header:
227 | dependency: transitive
228 | description:
229 | name: flutter_sticky_header
230 | sha256: "7f76d24d119424ca0c95c146b8627a457e8de8169b0d584f766c2c545db8f8be"
231 | url: "https://pub.dev"
232 | source: hosted
233 | version: "0.7.0"
234 | flutter_test:
235 | dependency: "direct dev"
236 | description: flutter
237 | source: sdk
238 | version: "0.0.0"
239 | flutter_web_plugins:
240 | dependency: transitive
241 | description: flutter
242 | source: sdk
243 | version: "0.0.0"
244 | font_awesome_flutter:
245 | dependency: "direct main"
246 | description:
247 | name: font_awesome_flutter
248 | sha256: b738e35f8bb4957896c34957baf922f99c5d415b38ddc8b070d14b7fa95715d4
249 | url: "https://pub.dev"
250 | source: hosted
251 | version: "10.9.1"
252 | google_fonts:
253 | dependency: "direct main"
254 | description:
255 | name: google_fonts
256 | sha256: df9763500dadba0155373e9cb44e202ce21bd9ed5de6bdbd05c5854e86839cb8
257 | url: "https://pub.dev"
258 | source: hosted
259 | version: "6.3.0"
260 | html:
261 | dependency: transitive
262 | description:
263 | name: html
264 | sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
265 | url: "https://pub.dev"
266 | source: hosted
267 | version: "0.15.6"
268 | http:
269 | dependency: transitive
270 | description:
271 | name: http
272 | sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
273 | url: "https://pub.dev"
274 | source: hosted
275 | version: "1.5.0"
276 | http_parser:
277 | dependency: transitive
278 | description:
279 | name: http_parser
280 | sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
281 | url: "https://pub.dev"
282 | source: hosted
283 | version: "4.1.2"
284 | hugeicons:
285 | dependency: "direct main"
286 | description:
287 | name: hugeicons
288 | sha256: cf172bae0c3ff2e2114324d05e79872081f4cd3c009f5979285fec73a693096a
289 | url: "https://pub.dev"
290 | source: hosted
291 | version: "0.0.11"
292 | image:
293 | dependency: transitive
294 | description:
295 | name: image
296 | sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
297 | url: "https://pub.dev"
298 | source: hosted
299 | version: "4.5.4"
300 | intl:
301 | dependency: "direct main"
302 | description:
303 | name: intl
304 | sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
305 | url: "https://pub.dev"
306 | source: hosted
307 | version: "0.20.2"
308 | json_annotation:
309 | dependency: transitive
310 | description:
311 | name: json_annotation
312 | sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
313 | url: "https://pub.dev"
314 | source: hosted
315 | version: "4.9.0"
316 | leak_tracker:
317 | dependency: transitive
318 | description:
319 | name: leak_tracker
320 | sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
321 | url: "https://pub.dev"
322 | source: hosted
323 | version: "10.0.7"
324 | leak_tracker_flutter_testing:
325 | dependency: transitive
326 | description:
327 | name: leak_tracker_flutter_testing
328 | sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
329 | url: "https://pub.dev"
330 | source: hosted
331 | version: "3.0.8"
332 | leak_tracker_testing:
333 | dependency: transitive
334 | description:
335 | name: leak_tracker_testing
336 | sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
337 | url: "https://pub.dev"
338 | source: hosted
339 | version: "3.0.1"
340 | lints:
341 | dependency: transitive
342 | description:
343 | name: lints
344 | sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
345 | url: "https://pub.dev"
346 | source: hosted
347 | version: "5.1.1"
348 | matcher:
349 | dependency: transitive
350 | description:
351 | name: matcher
352 | sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
353 | url: "https://pub.dev"
354 | source: hosted
355 | version: "0.12.16+1"
356 | material_color_utilities:
357 | dependency: transitive
358 | description:
359 | name: material_color_utilities
360 | sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
361 | url: "https://pub.dev"
362 | source: hosted
363 | version: "0.11.1"
364 | meta:
365 | dependency: transitive
366 | description:
367 | name: meta
368 | sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
369 | url: "https://pub.dev"
370 | source: hosted
371 | version: "1.15.0"
372 | mime:
373 | dependency: transitive
374 | description:
375 | name: mime
376 | sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
377 | url: "https://pub.dev"
378 | source: hosted
379 | version: "2.0.0"
380 | nested:
381 | dependency: transitive
382 | description:
383 | name: nested
384 | sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
385 | url: "https://pub.dev"
386 | source: hosted
387 | version: "1.0.0"
388 | path:
389 | dependency: transitive
390 | description:
391 | name: path
392 | sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
393 | url: "https://pub.dev"
394 | source: hosted
395 | version: "1.9.0"
396 | path_provider:
397 | dependency: transitive
398 | description:
399 | name: path_provider
400 | sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
401 | url: "https://pub.dev"
402 | source: hosted
403 | version: "2.1.5"
404 | path_provider_android:
405 | dependency: transitive
406 | description:
407 | name: path_provider_android
408 | sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9
409 | url: "https://pub.dev"
410 | source: hosted
411 | version: "2.2.17"
412 | path_provider_foundation:
413 | dependency: transitive
414 | description:
415 | name: path_provider_foundation
416 | sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
417 | url: "https://pub.dev"
418 | source: hosted
419 | version: "2.4.1"
420 | path_provider_linux:
421 | dependency: transitive
422 | description:
423 | name: path_provider_linux
424 | sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
425 | url: "https://pub.dev"
426 | source: hosted
427 | version: "2.2.1"
428 | path_provider_platform_interface:
429 | dependency: transitive
430 | description:
431 | name: path_provider_platform_interface
432 | sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
433 | url: "https://pub.dev"
434 | source: hosted
435 | version: "2.1.2"
436 | path_provider_windows:
437 | dependency: transitive
438 | description:
439 | name: path_provider_windows
440 | sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
441 | url: "https://pub.dev"
442 | source: hosted
443 | version: "2.3.0"
444 | permission_handler:
445 | dependency: "direct main"
446 | description:
447 | name: permission_handler
448 | sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849"
449 | url: "https://pub.dev"
450 | source: hosted
451 | version: "11.4.0"
452 | permission_handler_android:
453 | dependency: transitive
454 | description:
455 | name: permission_handler_android
456 | sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc
457 | url: "https://pub.dev"
458 | source: hosted
459 | version: "12.1.0"
460 | permission_handler_apple:
461 | dependency: transitive
462 | description:
463 | name: permission_handler_apple
464 | sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
465 | url: "https://pub.dev"
466 | source: hosted
467 | version: "9.4.7"
468 | permission_handler_html:
469 | dependency: transitive
470 | description:
471 | name: permission_handler_html
472 | sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
473 | url: "https://pub.dev"
474 | source: hosted
475 | version: "0.1.3+5"
476 | permission_handler_platform_interface:
477 | dependency: transitive
478 | description:
479 | name: permission_handler_platform_interface
480 | sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
481 | url: "https://pub.dev"
482 | source: hosted
483 | version: "4.3.0"
484 | permission_handler_windows:
485 | dependency: transitive
486 | description:
487 | name: permission_handler_windows
488 | sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
489 | url: "https://pub.dev"
490 | source: hosted
491 | version: "0.2.1"
492 | petitparser:
493 | dependency: transitive
494 | description:
495 | name: petitparser
496 | sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
497 | url: "https://pub.dev"
498 | source: hosted
499 | version: "6.0.2"
500 | phone_state:
501 | dependency: "direct main"
502 | description:
503 | name: phone_state
504 | sha256: "291f7444e68f9a504afa8c7ceee42a5bbfe7131f059439e4e8d23d9d347f1157"
505 | url: "https://pub.dev"
506 | source: hosted
507 | version: "2.1.1"
508 | phosphor_flutter:
509 | dependency: "direct main"
510 | description:
511 | name: phosphor_flutter
512 | sha256: "8a14f238f28a0b54842c5a4dc20676598dd4811fcba284ed828bd5a262c11fde"
513 | url: "https://pub.dev"
514 | source: hosted
515 | version: "2.1.0"
516 | platform:
517 | dependency: transitive
518 | description:
519 | name: platform
520 | sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
521 | url: "https://pub.dev"
522 | source: hosted
523 | version: "3.1.6"
524 | plugin_platform_interface:
525 | dependency: transitive
526 | description:
527 | name: plugin_platform_interface
528 | sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
529 | url: "https://pub.dev"
530 | source: hosted
531 | version: "2.1.8"
532 | posix:
533 | dependency: transitive
534 | description:
535 | name: posix
536 | sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
537 | url: "https://pub.dev"
538 | source: hosted
539 | version: "6.0.3"
540 | provider:
541 | dependency: "direct main"
542 | description:
543 | name: provider
544 | sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
545 | url: "https://pub.dev"
546 | source: hosted
547 | version: "6.1.5+1"
548 | qr:
549 | dependency: transitive
550 | description:
551 | name: qr
552 | sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
553 | url: "https://pub.dev"
554 | source: hosted
555 | version: "3.0.2"
556 | qr_flutter:
557 | dependency: "direct main"
558 | description:
559 | name: qr_flutter
560 | sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097"
561 | url: "https://pub.dev"
562 | source: hosted
563 | version: "4.1.0"
564 | share_plus:
565 | dependency: "direct main"
566 | description:
567 | name: share_plus
568 | sha256: d7dc0630a923883c6328ca31b89aa682bacbf2f8304162d29f7c6aaff03a27a1
569 | url: "https://pub.dev"
570 | source: hosted
571 | version: "11.1.0"
572 | share_plus_platform_interface:
573 | dependency: transitive
574 | description:
575 | name: share_plus_platform_interface
576 | sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a"
577 | url: "https://pub.dev"
578 | source: hosted
579 | version: "6.1.0"
580 | shared_preferences:
581 | dependency: "direct main"
582 | description:
583 | name: shared_preferences
584 | sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
585 | url: "https://pub.dev"
586 | source: hosted
587 | version: "2.5.3"
588 | shared_preferences_android:
589 | dependency: transitive
590 | description:
591 | name: shared_preferences_android
592 | sha256: "5bcf0772a761b04f8c6bf814721713de6f3e5d9d89caf8d3fe031b02a342379e"
593 | url: "https://pub.dev"
594 | source: hosted
595 | version: "2.4.11"
596 | shared_preferences_foundation:
597 | dependency: transitive
598 | description:
599 | name: shared_preferences_foundation
600 | sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
601 | url: "https://pub.dev"
602 | source: hosted
603 | version: "2.5.4"
604 | shared_preferences_linux:
605 | dependency: transitive
606 | description:
607 | name: shared_preferences_linux
608 | sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
609 | url: "https://pub.dev"
610 | source: hosted
611 | version: "2.4.1"
612 | shared_preferences_platform_interface:
613 | dependency: transitive
614 | description:
615 | name: shared_preferences_platform_interface
616 | sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
617 | url: "https://pub.dev"
618 | source: hosted
619 | version: "2.4.1"
620 | shared_preferences_web:
621 | dependency: transitive
622 | description:
623 | name: shared_preferences_web
624 | sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
625 | url: "https://pub.dev"
626 | source: hosted
627 | version: "2.4.3"
628 | shared_preferences_windows:
629 | dependency: transitive
630 | description:
631 | name: shared_preferences_windows
632 | sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
633 | url: "https://pub.dev"
634 | source: hosted
635 | version: "2.4.1"
636 | simple_barcode_scanner:
637 | dependency: "direct main"
638 | description:
639 | name: simple_barcode_scanner
640 | sha256: "2b6ec05e10fbf4f07687f3687c5cf46d3dcf873492e0a5758211bd957c854113"
641 | url: "https://pub.dev"
642 | source: hosted
643 | version: "0.3.0"
644 | sky_engine:
645 | dependency: transitive
646 | description: flutter
647 | source: sdk
648 | version: "0.0.0"
649 | source_span:
650 | dependency: transitive
651 | description:
652 | name: source_span
653 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
654 | url: "https://pub.dev"
655 | source: hosted
656 | version: "1.10.0"
657 | sprintf:
658 | dependency: transitive
659 | description:
660 | name: sprintf
661 | sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
662 | url: "https://pub.dev"
663 | source: hosted
664 | version: "7.0.0"
665 | stack_trace:
666 | dependency: transitive
667 | description:
668 | name: stack_trace
669 | sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
670 | url: "https://pub.dev"
671 | source: hosted
672 | version: "1.12.0"
673 | sticky_az_list:
674 | dependency: "direct main"
675 | description:
676 | path: "."
677 | ref: "118f056"
678 | resolved-ref: "118f056a701ee914a600659c9fdec1b2f2805c6a"
679 | url: "https://github.com/TetrixGauss/sticky_az_list_plus"
680 | source: git
681 | version: "0.0.7"
682 | stream_channel:
683 | dependency: transitive
684 | description:
685 | name: stream_channel
686 | sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
687 | url: "https://pub.dev"
688 | source: hosted
689 | version: "2.1.2"
690 | string_scanner:
691 | dependency: transitive
692 | description:
693 | name: string_scanner
694 | sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
695 | url: "https://pub.dev"
696 | source: hosted
697 | version: "1.3.0"
698 | term_glyph:
699 | dependency: transitive
700 | description:
701 | name: term_glyph
702 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
703 | url: "https://pub.dev"
704 | source: hosted
705 | version: "1.2.1"
706 | test_api:
707 | dependency: transitive
708 | description:
709 | name: test_api
710 | sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
711 | url: "https://pub.dev"
712 | source: hosted
713 | version: "0.7.3"
714 | typed_data:
715 | dependency: transitive
716 | description:
717 | name: typed_data
718 | sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
719 | url: "https://pub.dev"
720 | source: hosted
721 | version: "1.4.0"
722 | universal_io:
723 | dependency: transitive
724 | description:
725 | name: universal_io
726 | sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad"
727 | url: "https://pub.dev"
728 | source: hosted
729 | version: "2.2.2"
730 | url_launcher:
731 | dependency: "direct main"
732 | description:
733 | name: url_launcher
734 | sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
735 | url: "https://pub.dev"
736 | source: hosted
737 | version: "6.3.2"
738 | url_launcher_android:
739 | dependency: transitive
740 | description:
741 | name: url_launcher_android
742 | sha256: "0aedad096a85b49df2e4725fa32118f9fa580f3b14af7a2d2221896a02cd5656"
743 | url: "https://pub.dev"
744 | source: hosted
745 | version: "6.3.17"
746 | url_launcher_ios:
747 | dependency: transitive
748 | description:
749 | name: url_launcher_ios
750 | sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb"
751 | url: "https://pub.dev"
752 | source: hosted
753 | version: "6.3.3"
754 | url_launcher_linux:
755 | dependency: transitive
756 | description:
757 | name: url_launcher_linux
758 | sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
759 | url: "https://pub.dev"
760 | source: hosted
761 | version: "3.2.1"
762 | url_launcher_macos:
763 | dependency: transitive
764 | description:
765 | name: url_launcher_macos
766 | sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2"
767 | url: "https://pub.dev"
768 | source: hosted
769 | version: "3.2.2"
770 | url_launcher_platform_interface:
771 | dependency: transitive
772 | description:
773 | name: url_launcher_platform_interface
774 | sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
775 | url: "https://pub.dev"
776 | source: hosted
777 | version: "2.3.2"
778 | url_launcher_web:
779 | dependency: transitive
780 | description:
781 | name: url_launcher_web
782 | sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
783 | url: "https://pub.dev"
784 | source: hosted
785 | version: "2.4.1"
786 | url_launcher_windows:
787 | dependency: transitive
788 | description:
789 | name: url_launcher_windows
790 | sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
791 | url: "https://pub.dev"
792 | source: hosted
793 | version: "3.1.4"
794 | uuid:
795 | dependency: transitive
796 | description:
797 | name: uuid
798 | sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
799 | url: "https://pub.dev"
800 | source: hosted
801 | version: "4.5.1"
802 | value_layout_builder:
803 | dependency: transitive
804 | description:
805 | name: value_layout_builder
806 | sha256: c02511ea91ca5c643b514a33a38fa52536f74aa939ec367d02938b5ede6807fa
807 | url: "https://pub.dev"
808 | source: hosted
809 | version: "0.4.0"
810 | vector_math:
811 | dependency: transitive
812 | description:
813 | name: vector_math
814 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
815 | url: "https://pub.dev"
816 | source: hosted
817 | version: "2.1.4"
818 | vm_service:
819 | dependency: transitive
820 | description:
821 | name: vm_service
822 | sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
823 | url: "https://pub.dev"
824 | source: hosted
825 | version: "14.3.0"
826 | web:
827 | dependency: transitive
828 | description:
829 | name: web
830 | sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
831 | url: "https://pub.dev"
832 | source: hosted
833 | version: "1.1.1"
834 | webview_windows:
835 | dependency: transitive
836 | description:
837 | name: webview_windows
838 | sha256: "47fcad5875a45db29dbb5c9e6709bf5c88dcc429049872701343f91ed7255730"
839 | url: "https://pub.dev"
840 | source: hosted
841 | version: "0.4.0"
842 | win32:
843 | dependency: transitive
844 | description:
845 | name: win32
846 | sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e
847 | url: "https://pub.dev"
848 | source: hosted
849 | version: "5.10.1"
850 | xdg_directories:
851 | dependency: transitive
852 | description:
853 | name: xdg_directories
854 | sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
855 | url: "https://pub.dev"
856 | source: hosted
857 | version: "1.1.0"
858 | xml:
859 | dependency: transitive
860 | description:
861 | name: xml
862 | sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
863 | url: "https://pub.dev"
864 | source: hosted
865 | version: "6.5.0"
866 | yaml:
867 | dependency: transitive
868 | description:
869 | name: yaml
870 | sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
871 | url: "https://pub.dev"
872 | source: hosted
873 | version: "3.1.3"
874 | sdks:
875 | dart: ">=3.6.0 <4.0.0"
876 | flutter: ">=3.27.0"
877 |
--------------------------------------------------------------------------------