├── 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 | Preview 1 21 | Preview 2 22 | Preview 3 23 | Preview 4 24 | Preview 5 25 | Preview 5 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 | --------------------------------------------------------------------------------